Browse Source

Merge branch 'master' into dapiintegration

pull/7983/head
NiteKat 10 months ago
parent
commit
33e1ecb0c8
  1. 6
      Source/CMakeLists.txt
  2. 77
      Source/control.cpp
  3. 4
      Source/control.h
  4. 28
      Source/encrypt.cpp
  5. 2
      Source/encrypt.h
  6. 2
      Source/engine/assets.cpp
  7. 6
      Source/engine/render/scrollrt.cpp
  8. 19
      Source/items/validation.cpp
  9. 6
      Source/items/validation.h
  10. 18
      Source/levels/setmaps.h
  11. 4
      Source/loadsave.cpp
  12. 39
      Source/misdat.h
  13. 192
      Source/missiles.cpp
  14. 155
      Source/missiles.h
  15. 24
      Source/monster.cpp
  16. 42
      Source/monsters/validation.cpp
  17. 15
      Source/monsters/validation.hpp
  18. 762
      Source/msg.cpp
  19. 3
      Source/msg.h
  20. 5
      Source/multi.cpp
  21. 20
      Source/panels/spell_book.cpp
  22. 2
      Source/panels/spell_list.cpp
  23. 2
      Source/portal.cpp
  24. 49
      Source/portals/validation.cpp
  25. 16
      Source/portals/validation.hpp
  26. 46
      Source/quests/validation.cpp
  27. 17
      Source/quests/validation.hpp
  28. 44
      Source/sync.cpp
  29. 5
      Source/sync.h
  30. 3
      Source/textdat.cpp
  31. 2
      Source/textdat.h
  32. 6
      test/missiles_test.cpp

6
Source/CMakeLists.txt

@ -130,6 +130,8 @@ set(libdevilutionx_SRCS
lua/modules/towners.cpp
lua/repl.cpp
monsters/validation.cpp
panels/charpanel.cpp
panels/console.cpp
panels/info_box.cpp
@ -140,6 +142,8 @@ set(libdevilutionx_SRCS
platform/locale.cpp
portals/validation.cpp
qol/autopickup.cpp
qol/chatlog.cpp
qol/floatingnumbers.cpp
@ -148,6 +152,8 @@ set(libdevilutionx_SRCS
qol/stash.cpp
qol/xpbar.cpp
quests/validation.cpp
storm/storm_net.cpp
storm/storm_svid.cpp

77
Source/control.cpp

@ -151,8 +151,8 @@ Rectangle BeltRect { { 205, 5 }, BeltSize };
Rectangle SpellButtonRect { { 565, 64 }, { 56, 56 } };
Rectangle FlaskTopRect { { 13, 3 }, { 60, 13 } };
Rectangle FlaskBottomRect { { 0, 16 }, { 84, 69 } };
Rectangle FlaskTopRect { { 11, 3 }, { 62, 13 } };
Rectangle FlaskBottomRect { { 0, 16 }, { 88, 69 } };
int MuteButtons = 3;
int MuteButtonPadding = 2;
@ -214,14 +214,12 @@ const char *const PanBtnStr[8] = {
* It draws a rectangle of fixed width 59 and height 'h' from the source buffer
* into the target buffer.
* @param out The target buffer.
* @param celBuf Buffer of the empty flask cel.
* @param sourcePosition Source buffer start coordinate.
* @param celBuf Buffer of the flask cel.
* @param targetPosition Target buffer coordinate.
* @param h How many lines of the source buffer that will be copied.
*/
void DrawFlaskAbovePanel(const Surface &out, const Surface &celBuf, Point sourcePosition, Point targetPosition, int h)
void DrawFlaskAbovePanel(const Surface &out, const Surface &celBuf, Point targetPosition)
{
out.BlitFromSkipColorIndexZero(celBuf, MakeSdlRect(sourcePosition.x, sourcePosition.y, FlaskTopRect.size.width, h), targetPosition);
out.BlitFromSkipColorIndexZero(celBuf, MakeSdlRect(0, 0, celBuf.w(), celBuf.h()), targetPosition);
}
/**
@ -234,15 +232,20 @@ void DrawFlaskAbovePanel(const Surface &out, const Surface &celBuf, Point source
*/
void DrawFlaskUpper(const Surface &out, const Surface &sourceBuffer, int offset, int fillPer)
{
int emptyRows = std::clamp(81 - fillPer, 0, FlaskTopRect.size.height);
int filledRows = FlaskTopRect.size.height - emptyRows;
const Rectangle &rect = FlaskTopRect;
const int emptyRows = std::clamp(81 - fillPer, 0, rect.size.height);
const int filledRows = rect.size.height - emptyRows;
// Draw the empty part of the flask
DrawFlaskAbovePanel(out, sourceBuffer, FlaskTopRect.position, GetMainPanel().position + Displacement { offset, -FlaskTopRect.size.height }, FlaskTopRect.size.height);
DrawFlaskAbovePanel(out,
sourceBuffer.subregion(rect.position.x, rect.position.y, rect.size.width, rect.size.height),
GetMainPanel().position + Displacement { offset, -rect.size.height });
// Draw the filled part of the flask over the empty part
if (filledRows > 0) {
DrawFlaskAbovePanel(out, *BottomBuffer, { offset, FlaskTopRect.position.y + emptyRows }, GetMainPanel().position + Displacement { offset, -FlaskTopRect.size.height + emptyRows }, filledRows);
DrawFlaskAbovePanel(out,
BottomBuffer->subregion(offset, rect.position.y + emptyRows, rect.size.width, filledRows),
GetMainPanel().position + Displacement { offset, -rect.size.height + emptyRows });
}
}
@ -251,14 +254,12 @@ void DrawFlaskUpper(const Surface &out, const Surface &sourceBuffer, int offset,
* of the flask getting empty. This function takes a cel and draws a
* horizontal stripe of height (max-min) onto the given buffer.
* @param out Target buffer.
* @param position Buffer coordinate.
* @param celBuf Buffer of the empty flask cel.
* @param y0 Top of the flask cel section to draw.
* @param y1 Bottom of the flask cel section to draw.
* @param celBuf Buffer of the flask cel.
* @param targetPosition Target buffer coordinate.
*/
void DrawFlaskOnPanel(const Surface &out, Point position, const Surface &celBuf, int y0, int y1)
void DrawFlaskOnPanel(const Surface &out, const Surface &celBuf, Point targetPosition)
{
out.BlitFrom(celBuf, MakeSdlRect(0, static_cast<decltype(SDL_Rect {}.y)>(y0), celBuf.w(), y1 - y0), position);
out.BlitFrom(celBuf, MakeSdlRect(0, 0, celBuf.w(), celBuf.h()), targetPosition);
}
/**
@ -268,13 +269,27 @@ void DrawFlaskOnPanel(const Surface &out, Point position, const Surface &celBuf,
* @param sourceBuffer A sprite representing the appropriate background/empty flask style
* @param offset X coordinate offset for where the flask should be drawn
* @param fillPer How full the flask is (a value from 0 to 80)
* @param drawFilledPortion Indicates whether to draw the filled portion of the flask
*/
void DrawFlaskLower(const Surface &out, const Surface &sourceBuffer, int offset, int fillPer)
void DrawFlaskLower(const Surface &out, const Surface &sourceBuffer, int offset, int fillPer, bool drawFilledPortion)
{
int filled = std::clamp(fillPer, 0, FlaskBottomRect.size.height);
const Rectangle &rect = FlaskBottomRect;
const int filledRows = std::clamp(fillPer, 0, rect.size.height);
const int emptyRows = rect.size.height - filledRows;
// Draw the empty part of the flask
if (emptyRows > 0) {
DrawFlaskOnPanel(out,
sourceBuffer.subregion(rect.position.x, rect.position.y, rect.size.width, emptyRows),
GetMainPanel().position + Displacement { offset, 0 });
}
if (filled < FlaskBottomRect.size.height)
DrawFlaskOnPanel(out, GetMainPanel().position + Displacement { offset, 0 }, sourceBuffer, FlaskBottomRect.position.y, FlaskBottomRect.position.y + FlaskBottomRect.size.height - filled);
// Draw the filled part of the flask
if (drawFilledPortion && filledRows > 0) {
DrawFlaskOnPanel(out,
BottomBuffer->subregion(offset, rect.position.y + emptyRows, rect.size.width, filledRows),
GetMainPanel().position + Displacement { offset, emptyRows });
}
}
void SetMainPanelButtonDown(int btnId)
@ -422,12 +437,6 @@ void AppendArenaOverview(std::string &ret)
}
}
const dungeon_type DungeonTypeForArena[] = {
dungeon_type::DTYPE_CATHEDRAL, // SL_ARENA_CHURCH
dungeon_type::DTYPE_HELL, // SL_ARENA_HELL
dungeon_type::DTYPE_HELL, // SL_ARENA_CIRCLE_OF_LIFE
};
std::string TextCmdArena(const std::string_view parameter)
{
std::string ret;
@ -455,7 +464,7 @@ std::string TextCmdArena(const std::string_view parameter)
return ret;
}
setlvltype = DungeonTypeForArena[arenaLevel - SL_FIRST_ARENA];
setlvltype = GetArenaLevelType(arenaLevel);
StartNewLvl(*MyPlayer, WM_DIABSETLVL, arenaLevel);
return ret;
}
@ -835,7 +844,7 @@ void DrawPanelBox(const Surface &out, SDL_Rect srcRect, Point targetPosition)
void DrawLifeFlaskUpper(const Surface &out)
{
constexpr int LifeFlaskUpperOffset = 109;
constexpr int LifeFlaskUpperOffset = 107;
DrawFlaskUpper(out, *pLifeBuff, LifeFlaskUpperOffset, MyPlayer->_pHPPer);
}
@ -845,16 +854,16 @@ void DrawManaFlaskUpper(const Surface &out)
DrawFlaskUpper(out, *pManaBuff, ManaFlaskUpperOffset, MyPlayer->_pManaPer);
}
void DrawLifeFlaskLower(const Surface &out)
void DrawLifeFlaskLower(const Surface &out, bool drawFilledPortion)
{
constexpr int LifeFlaskLowerOffset = 96;
DrawFlaskLower(out, *pLifeBuff, LifeFlaskLowerOffset, MyPlayer->_pHPPer);
DrawFlaskLower(out, *pLifeBuff, LifeFlaskLowerOffset, MyPlayer->_pHPPer, drawFilledPortion);
}
void DrawManaFlaskLower(const Surface &out)
void DrawManaFlaskLower(const Surface &out, bool drawFilledPortion)
{
constexpr int ManaFlaskLowerOffeset = 464;
DrawFlaskLower(out, *pManaBuff, ManaFlaskLowerOffeset, MyPlayer->_pManaPer);
constexpr int ManaFlaskLowerOffset = 464;
DrawFlaskLower(out, *pManaBuff, ManaFlaskLowerOffset, MyPlayer->_pManaPer, drawFilledPortion);
}
void DrawFlaskValues(const Surface &out, Point pos, int currValue, int maxValue)

4
Source/control.h

@ -100,7 +100,7 @@ void DrawLifeFlaskUpper(const Surface &out);
* First sets the fill amount then draws the empty flask cel portion then the filled
* flask portion.
*/
void DrawLifeFlaskLower(const Surface &out);
void DrawLifeFlaskLower(const Surface &out, bool drawFilledPortion);
/**
* Draws the top dome of the mana flask (that part that protrudes out of the control panel).
@ -112,7 +112,7 @@ void DrawManaFlaskUpper(const Surface &out);
/**
* Controls the drawing of the area of the mana flask within the control panel.
*/
void DrawManaFlaskLower(const Surface &out);
void DrawManaFlaskLower(const Surface &out, bool drawFilledPortion);
/**
* Controls drawing of current / max values (health, mana) within the control panel.

28
Source/encrypt.cpp

@ -21,9 +21,11 @@ namespace {
struct TDataInfo {
std::byte *srcData;
uint32_t srcOffset;
uint32_t srcSize;
std::byte *destData;
uint32_t destOffset;
uint32_t size;
size_t destSize;
bool error;
};
unsigned int PkwareBufferRead(char *buf, unsigned int *size, void *param) // NOLINT(readability-non-const-parameter)
@ -31,8 +33,8 @@ unsigned int PkwareBufferRead(char *buf, unsigned int *size, void *param) // NOL
auto *pInfo = reinterpret_cast<TDataInfo *>(param);
uint32_t sSize;
if (*size >= pInfo->size - pInfo->srcOffset) {
sSize = pInfo->size - pInfo->srcOffset;
if (*size >= pInfo->srcSize - pInfo->srcOffset) {
sSize = pInfo->srcSize - pInfo->srcOffset;
} else {
sSize = *size;
}
@ -47,6 +49,11 @@ void PkwareBufferWrite(char *buf, unsigned int *size, void *param) // NOLINT(rea
{
auto *pInfo = reinterpret_cast<TDataInfo *>(param);
pInfo->error = pInfo->error || pInfo->destOffset + *size > pInfo->destSize;
if (pInfo->error) {
return;
}
memcpy(pInfo->destData + pInfo->destOffset, buf, *size);
pInfo->destOffset += *size;
}
@ -66,9 +73,11 @@ uint32_t PkwareCompress(std::byte *srcData, uint32_t size)
TDataInfo param;
param.srcData = srcData;
param.srcOffset = 0;
param.srcSize = size;
param.destData = destData.get();
param.destOffset = 0;
param.size = size;
param.destSize = destSize;
param.error = false;
unsigned type = 0;
unsigned dsize = 4096;
@ -82,7 +91,7 @@ uint32_t PkwareCompress(std::byte *srcData, uint32_t size)
return size;
}
void PkwareDecompress(std::byte *inBuff, uint32_t recvSize, int maxBytes)
uint32_t PkwareDecompress(std::byte *inBuff, uint32_t recvSize, size_t maxBytes)
{
std::unique_ptr<char[]> ptr = std::make_unique<char[]>(CMP_BUFFER_SIZE);
std::unique_ptr<std::byte[]> outBuff { new std::byte[maxBytes] };
@ -90,12 +99,19 @@ void PkwareDecompress(std::byte *inBuff, uint32_t recvSize, int maxBytes)
TDataInfo info;
info.srcData = inBuff;
info.srcOffset = 0;
info.srcSize = recvSize;
info.destData = outBuff.get();
info.destOffset = 0;
info.size = recvSize;
info.destSize = maxBytes;
info.error = false;
explode(PkwareBufferRead, PkwareBufferWrite, ptr.get(), &info);
if (info.error) {
return 0;
}
memcpy(inBuff, outBuff.get(), info.destOffset);
return info.destOffset;
}
} // namespace devilution

2
Source/encrypt.h

@ -11,6 +11,6 @@
namespace devilution {
uint32_t PkwareCompress(std::byte *srcData, uint32_t size);
void PkwareDecompress(std::byte *inBuff, uint32_t recvSize, int maxBytes);
uint32_t PkwareDecompress(std::byte *inBuff, uint32_t recvSize, size_t maxBytes);
} // namespace devilution

2
Source/engine/assets.cpp

@ -491,7 +491,7 @@ void LoadModArchives(std::span<const std::string_view> modnames)
if (FileExists(targetPath)) {
OverridePaths.emplace_back(targetPath);
}
targetPath = StrCat(SDL_GetBasePath(), "mods" DIRECTORY_SEPARATOR_STR, modname, DIRECTORY_SEPARATOR_STR);
targetPath = StrCat(paths::BasePath(), "mods" DIRECTORY_SEPARATOR_STR, modname, DIRECTORY_SEPARATOR_STR);
if (FileExists(targetPath)) {
OverridePaths.emplace_back(targetPath);
}

6
Source/engine/render/scrollrt.cpp

@ -1700,7 +1700,7 @@ void DrawAndBlit()
const Rectangle &mainPanel = GetMainPanel();
if (gnScreenWidth > mainPanel.size.width || IsRedrawEverything()) {
if (gnScreenWidth > mainPanel.size.width || IsRedrawEverything() || *GetOptions().Gameplay.enableFloatingNumbers != FloatingNumbers::Off) {
drawHealth = true;
drawMana = true;
drawControlButtons = true;
@ -1724,10 +1724,10 @@ void DrawAndBlit()
DrawMainPanel(out);
}
if (drawHealth) {
DrawLifeFlaskLower(out);
DrawLifeFlaskLower(out, !drawCtrlPan);
}
if (drawMana) {
DrawManaFlaskLower(out);
DrawManaFlaskLower(out, !drawCtrlPan);
DrawSpell(out);
}

19
Source/items/validation.cpp

@ -10,6 +10,7 @@
#include "items.h"
#include "monstdat.h"
#include "msg.h"
#include "player.h"
#include "spells.h"
#include "utils/is_of.hpp"
@ -175,4 +176,22 @@ bool IsItemValid(const Player &player, const Item &item)
return IsDungeonItemValid(item._iCreateInfo, item.dwBuff);
}
bool IsItemDeltaValid(const TCmdPItem &itemDelta)
{
if (itemDelta.bCmd == CMD_INVALID)
return true;
if (IsNoneOf(itemDelta.bCmd, TCmdPItem::FloorItem, TCmdPItem::PickedUpItem, TCmdPItem::DroppedItem))
return false;
if (!InDungeonBounds({ itemDelta.x, itemDelta.y }))
return false;
_item_indexes idx = static_cast<_item_indexes>(SDL_SwapLE16(itemDelta.def.wIndx));
if (idx == IDI_EAR)
return true;
if (!IsItemAvailable(idx))
return false;
Item item = {};
RecreateItem(*MyPlayer, itemDelta.item, item);
return IsItemValid(*MyPlayer, item);
}
} // namespace devilution

6
Source/items/validation.h

@ -7,17 +7,19 @@
#include <cstdint>
namespace devilution {
// Forward declared structs to avoid circular dependencies
struct Item;
struct TCmdPItem;
struct Player;
namespace devilution {
bool IsCreationFlagComboValid(uint16_t iCreateInfo);
bool IsTownItemValid(uint16_t iCreateInfo, const Player &player);
bool IsShopPriceValid(const Item &item);
bool IsUniqueMonsterItemValid(uint16_t iCreateInfo, uint32_t dwBuff);
bool IsDungeonItemValid(uint16_t iCreateInfo, uint32_t dwBuff);
bool IsItemValid(const Player &player, const Item &item);
bool IsItemDeltaValid(const TCmdPItem &itemDelta);
} // namespace devilution

18
Source/levels/setmaps.h

@ -5,8 +5,26 @@
*/
#pragma once
#include "levels/gendung.h"
namespace devilution {
/**
* @brief Get the tile type used to render the given arena level
*/
inline dungeon_type GetArenaLevelType(_setlevels arenaLevel)
{
constexpr dungeon_type DungeonTypeForArena[] = {
dungeon_type::DTYPE_CATHEDRAL, // SL_ARENA_CHURCH
dungeon_type::DTYPE_HELL, // SL_ARENA_HELL
dungeon_type::DTYPE_HELL, // SL_ARENA_CIRCLE_OF_LIFE
};
constexpr size_t arenaCount = sizeof(DungeonTypeForArena) / sizeof(dungeon_type);
const size_t index = arenaLevel - SL_FIRST_ARENA;
return index < arenaCount ? DungeonTypeForArena[index] : DTYPE_NONE;
}
/**
* @brief Load a quest map, the given map is specified via the global setlvlnum
*/

4
Source/loadsave.cpp

@ -771,7 +771,7 @@ void LoadMissile(LoadHelper *file)
missile.position.start.y = file->NextLE<int32_t>();
missile.position.traveled.deltaX = file->NextLE<int32_t>();
missile.position.traveled.deltaY = file->NextLE<int32_t>();
missile._mimfnum = file->NextLE<int32_t>();
missile.setFrameGroupRaw(file->NextLE<int32_t>());
missile._mispllvl = file->NextLE<int32_t>();
missile._miDelFlag = file->NextBool32();
missile._miAnimType = static_cast<MissileGraphicID>(file->NextLE<uint8_t>());
@ -1541,7 +1541,7 @@ void SaveMissile(SaveHelper *file, const Missile &missile)
file->WriteLE<int32_t>(missile.position.start.y);
file->WriteLE<int32_t>(missile.position.traveled.deltaX);
file->WriteLE<int32_t>(missile.position.traveled.deltaY);
file->WriteLE<int32_t>(missile._mimfnum);
file->WriteLE<int32_t>(missile.getFrameGroupRaw());
file->WriteLE<int32_t>(missile._mispllvl);
file->WriteLE<uint32_t>(missile._miDelFlag ? 1 : 0);
file->WriteLE<uint8_t>(static_cast<uint8_t>(missile._miAnimType));

39
Source/misdat.h

@ -129,6 +129,41 @@ enum class MissileDataFlags : uint8_t {
};
use_enum_as_flags(MissileDataFlags);
/**
* Represent a more fine-grained direction than the 8 value Direction enum.
*
* This is used when rendering projectiles like arrows which have additional sprites for "half-winds" on a 16-point compass.
* The sprite sheets are typically 0-indexed and use the following layout (relative to the screen projection)
*
* W WSW SW SSW S
* ^
* WNW | SSE
* |
* NW -------+------> SE
* |
* NNW | ESE
* |
* N NNE NE ENE E
*/
enum class Direction16 : uint8_t {
South,
South_SouthWest,
SouthWest,
West_SouthWest,
West,
West_NorthWest,
NorthWest,
North_NorthWest,
North,
North_NorthEast,
NorthEast,
East_NorthEast,
East,
East_SouthEast,
SouthEast,
South_SouthEast,
};
struct MissileData {
using AddFn = void (*)(Missile &, AddMissileParameter &);
using ProcessFn = void (*)(Missile &);
@ -198,11 +233,11 @@ struct MissileFileData {
* @param direction One of the 16 directions. Valid range: [0, 15].
* @return OptionalClxSpriteList
*/
[[nodiscard]] OptionalClxSpriteList spritesForDirection(size_t direction) const
[[nodiscard]] OptionalClxSpriteList spritesForDirection(Direction16 direction) const
{
if (!sprites)
return std::nullopt;
return sprites->isSheet() ? sprites->sheet()[direction] : sprites->list();
return sprites->isSheet() ? sprites->sheet()[static_cast<size_t>(direction)] : sprites->list();
}
};

192
Source/missiles.cpp

@ -40,6 +40,38 @@ namespace devilution {
std::list<Missile> Missiles;
bool MissilePreFlag;
void Missile::setAnimation(MissileGraphicID animtype)
{
const int dir = _mimfnum;
if (animtype >= MissileGraphicID::None) {
_miAnimType = MissileGraphicID::None;
_miAnimData = std::nullopt;
_miAnimWidth = 0;
_miAnimWidth2 = 0;
_miAnimFlags = MissileGraphicsFlags::None;
_miAnimDelay = 0;
_miAnimLen = 0;
_miAnimCnt = 0;
_miAnimFrame = 1;
return;
}
const MissileFileData &missileData = GetMissileSpriteData(animtype);
_miAnimType = animtype;
_miAnimFlags = missileData.flags;
if (!HeadlessMode) {
_miAnimData = missileData.spritesForDirection(static_cast<Direction16>(dir));
}
_miAnimDelay = missileData.animDelay(dir);
_miAnimLen = missileData.animLen(dir);
_miAnimWidth = missileData.animWidth;
_miAnimWidth2 = missileData.animWidth2;
_miAnimCnt = 0;
_miAnimFrame = 1;
}
namespace {
int AddClassHealingBonus(int hp, HeroClass heroClass)
@ -175,7 +207,7 @@ void MoveMissilePos(Missile &missile)
{
Direction moveDirection;
switch (static_cast<Direction>(missile._mimfnum)) {
switch (missile.getDirection()) {
case Direction::East:
moveDirection = Direction::SouthEast;
break;
@ -399,14 +431,14 @@ void RotateBlockedMissile(Missile &missile)
return;
}
int dir = missile._mimfnum + rotation;
int dir = missile.getFrameGroupRaw() + rotation;
int mAnimFAmt = GetMissileSpriteData(missile._miAnimType).animFAmt;
if (dir < 0)
dir = mAnimFAmt - 1;
else if (dir >= mAnimFAmt)
dir = 0;
SetMissDir(missile, dir);
missile.setFrameGroupRaw(dir);
}
void CheckMissileCol(Missile &missile, DamageType damageType, int minDamage, int maxDamage, bool isDamageShifted, Point position, bool dontDeleteOnCollision)
@ -598,38 +630,6 @@ void MoveMissileAndCheckMissileCol(Missile &missile, DamageType damageType, int
missile.lastCollisionTargetHash = tileTargetHash;
}
void SetMissAnim(Missile &missile, MissileGraphicID animtype)
{
const int dir = missile._mimfnum;
if (animtype >= MissileGraphicID::None) {
missile._miAnimType = MissileGraphicID::None;
missile._miAnimData = std::nullopt;
missile._miAnimWidth = 0;
missile._miAnimWidth2 = 0;
missile._miAnimFlags = MissileGraphicsFlags::None;
missile._miAnimDelay = 0;
missile._miAnimLen = 0;
missile._miAnimCnt = 0;
missile._miAnimFrame = 1;
return;
}
const MissileFileData &missileData = GetMissileSpriteData(animtype);
missile._miAnimType = animtype;
missile._miAnimFlags = missileData.flags;
if (!HeadlessMode) {
missile._miAnimData = missileData.spritesForDirection(static_cast<size_t>(dir));
}
missile._miAnimDelay = missileData.animDelay(dir);
missile._miAnimLen = missileData.animLen(dir);
missile._miAnimWidth = missileData.animWidth;
missile._miAnimWidth2 = missileData.animWidth2;
missile._miAnimCnt = 0;
missile._miAnimFrame = 1;
}
void AddRune(Missile &missile, Point dst, MissileID missileID)
{
if (LineClearMissile(missile.position.start, dst)) {
@ -692,7 +692,7 @@ bool GuardianTryFireAt(Missile &missile, Point target)
Direction dir = GetDirection(position, target);
AddMissile(position, target, dir, MissileID::Firebolt, TARGET_MONSTERS, missile._misource, missile._midam, missile.sourcePlayer()->GetSpellLevel(SpellID::Guardian), &missile);
SetMissDir(missile, 2);
missile.setFrameGroup<GuardianFrame>(GuardianFrame::Attack);
missile.var2 = 3;
return true;
@ -1122,12 +1122,6 @@ bool PlayerMHit(Player &player, Monster *monster, int dist, int mind, int maxd,
return true;
}
void SetMissDir(Missile &missile, int dir)
{
missile._mimfnum = dir;
SetMissAnim(missile, missile._miAnimType);
}
void InitMissiles()
{
Player &myPlayer = *MyPlayer;
@ -1540,7 +1534,7 @@ void AddBigExplosion(Missile &missile, AddMissileParameter & /*parameter*/)
CheckMissileCol(missile, damageType, dmg, dmg, false, position, true);
}
missile._mlid = AddLight(missile.position.start, 8);
SetMissDir(missile, 0);
missile.setDefaultFrameGroup();
missile.duration = missile._miAnimLen - 1;
}
@ -1555,7 +1549,7 @@ void AddImmolation(Missile &missile, AddMissileParameter &parameter)
sp += std::min(missile._mispllvl, 34);
}
UpdateMissileVelocity(missile, dst, sp);
SetMissDir(missile, GetDirection16(missile.position.start, dst));
missile.setDirection(GetDirection16(missile.position.start, dst));
missile.duration = 256;
missile._mlid = AddLight(missile.position.start, 8);
}
@ -1693,7 +1687,7 @@ void AddElementalArrow(Missile &missile, AddMissileParameter &parameter)
}
UpdateMissileVelocity(missile, dst, av);
SetMissDir(missile, GetDirection16(missile.position.start, dst));
missile.setDirection(GetDirection16(missile.position.start, dst));
missile.duration = 256;
missile.var1 = missile.position.start.x;
missile.var2 = missile.position.start.y;
@ -1802,7 +1796,7 @@ void AddFirebolt(Missile &missile, AddMissileParameter &parameter)
}
}
UpdateMissileVelocity(missile, dst, sp);
SetMissDir(missile, GetDirection16(missile.position.start, dst));
missile.setDirection(GetDirection16(missile.position.start, dst));
missile.duration = 256;
missile.var1 = missile.position.start.x;
missile.var2 = missile.position.start.y;
@ -1913,7 +1907,7 @@ void AddFireball(Missile &missile, AddMissileParameter &parameter)
missile._midam = ScaleSpellEffect(dmg, missile._mispllvl);
}
UpdateMissileVelocity(missile, dst, sp);
SetMissDir(missile, GetDirection16(missile.position.start, dst));
missile.setDirection(GetDirection16(missile.position.start, dst));
missile.duration = 256;
missile.var1 = missile.position.start.x;
missile.var2 = missile.position.start.y;
@ -1953,16 +1947,16 @@ void AddMissileExplosion(Missile &missile, AddMissileParameter &parameter)
if (missile._micaster != TARGET_MONSTERS && missile._misource >= 0) {
switch (Monsters[missile._misource].type().type) {
case MT_SUCCUBUS:
SetMissAnim(missile, MissileGraphicID::BloodStarExplosion);
missile.setAnimation(MissileGraphicID::BloodStarExplosion);
break;
case MT_SNOWWICH:
SetMissAnim(missile, MissileGraphicID::BloodStarBlueExplosion);
missile.setAnimation(MissileGraphicID::BloodStarBlueExplosion);
break;
case MT_HLSPWN:
SetMissAnim(missile, MissileGraphicID::BloodStarRedExplosion);
missile.setAnimation(MissileGraphicID::BloodStarRedExplosion);
break;
case MT_SOLBRNR:
SetMissAnim(missile, MissileGraphicID::BloodStarYellowExplosion);
missile.setAnimation(MissileGraphicID::BloodStarYellowExplosion);
break;
default:
break;
@ -1982,9 +1976,9 @@ void AddWeaponExplosion(Missile &missile, AddMissileParameter &parameter)
{
missile.var2 = parameter.dst.x;
if (parameter.dst.x == 1)
SetMissAnim(missile, MissileGraphicID::MagmaBallExplosion);
missile.setAnimation(MissileGraphicID::MagmaBallExplosion);
else
SetMissAnim(missile, MissileGraphicID::ChargedBolt);
missile.setAnimation(MissileGraphicID::ChargedBolt);
missile.duration = missile._miAnimLen - 1;
}
@ -2164,7 +2158,7 @@ namespace {
void InitMissileAnimationFromMonster(Missile &mis, Direction midir, const Monster &mon, MonsterGraphic graphic)
{
const AnimStruct &anim = mon.type().getAnimData(graphic);
mis._mimfnum = static_cast<int32_t>(midir);
mis.setDirection(midir);
mis._miAnimFlags = MissileGraphicsFlags::None;
ClxSpriteList sprites = *anim.spritesForDirection(midir);
const uint16_t width = sprites[0].width();
@ -2215,17 +2209,17 @@ void AddGenericMagicMissile(Missile &missile, AddMissileParameter &parameter)
if (missile._micaster != TARGET_MONSTERS && missile._misource > 0) {
Monster &monster = Monsters[missile._misource];
if (monster.type().type == MT_SUCCUBUS)
SetMissAnim(missile, MissileGraphicID::BloodStar);
missile.setAnimation(MissileGraphicID::BloodStar);
if (monster.type().type == MT_SNOWWICH)
SetMissAnim(missile, MissileGraphicID::BloodStarBlue);
missile.setAnimation(MissileGraphicID::BloodStarBlue);
if (monster.type().type == MT_HLSPWN)
SetMissAnim(missile, MissileGraphicID::BloodStarRed);
missile.setAnimation(MissileGraphicID::BloodStarRed);
if (monster.type().type == MT_SOLBRNR)
SetMissAnim(missile, MissileGraphicID::BloodStarYellow);
missile.setAnimation(MissileGraphicID::BloodStarYellow);
}
if (GetMissileSpriteData(missile._miAnimType).animFAmt == 16) {
SetMissDir(missile, GetDirection16(missile.position.start, dst));
missile.setDirection(GetDirection16(missile.position.start, dst));
}
if (missile._midam == 0) {
@ -2248,7 +2242,7 @@ void AddGenericMagicMissile(Missile &missile, AddMissileParameter &parameter)
void AddAcid(Missile &missile, AddMissileParameter &parameter)
{
UpdateMissileVelocity(missile, parameter.dst, 16);
SetMissDir(missile, GetDirection16(missile.position.start, parameter.dst));
missile.setDirection(GetDirection16(missile.position.start, parameter.dst));
if (!gbIsHellfire || (missile.position.velocity.deltaX & 0xFFFF0000) != 0 || (missile.position.velocity.deltaY & 0xFFFF0000) != 0)
missile.duration = 5 * (Monsters[missile._misource].intelligence + 4);
else
@ -2427,7 +2421,7 @@ void AddElemental(Missile &missile, AddMissileParameter &parameter)
missile._midam = ScaleSpellEffect(dmg, missile._mispllvl) / 2;
UpdateMissileVelocity(missile, dst, 16);
SetMissDir(missile, GetDirection(missile.position.start, dst));
missile.setDirection(GetDirection(missile.position.start, dst));
missile.duration = 256;
missile.var1 = missile.position.start.x;
missile.var2 = missile.position.start.y;
@ -2654,7 +2648,7 @@ void AddHolyBolt(Missile &missile, AddMissileParameter &parameter)
Player &player = Players[missile._misource];
UpdateMissileVelocity(missile, dst, sp);
SetMissDir(missile, GetDirection16(missile.position.start, dst));
missile.setDirection(GetDirection16(missile.position.start, dst));
missile.duration = 256;
missile.var1 = missile.position.start.x;
missile.var2 = missile.position.start.y;
@ -2697,7 +2691,7 @@ void AddBoneSpirit(Missile &missile, AddMissileParameter &parameter)
dst += parameter.midir;
}
UpdateMissileVelocity(missile, dst, 16);
SetMissDir(missile, GetDirection(missile.position.start, dst));
missile.setDirection(GetDirection(missile.position.start, dst));
missile.duration = 256;
missile.var1 = missile.position.start.x;
missile.var2 = missile.position.start.y;
@ -2760,9 +2754,9 @@ Missile *AddMissile(WorldTilePosition src, WorldTilePosition dst, Direction midi
}
if (missile._miAnimType == MissileGraphicID::None || GetMissileSpriteData(missile._miAnimType).animFAmt < 8)
SetMissDir(missile, 0);
missile.setDefaultFrameGroup();
else
SetMissDir(missile, midir);
missile.setDirection(midir);
if (!lSFX) {
lSFX = missileData.castSound;
@ -2809,7 +2803,7 @@ void ProcessElementalArrow(Missile &missile)
}
MoveMissileAndCheckMissileCol(missile, DamageType::Physical, mind, maxd, true, false);
if (missile.duration == 0) {
missile._mimfnum = 0;
missile.setDefaultFrameGroup();
missile.duration = missile._miAnimLen - 1;
missile.position.StopMissile();
@ -2848,7 +2842,7 @@ void ProcessElementalArrow(Missile &missile)
app_fatal(StrCat("wrong missile ID ", static_cast<int>(missile._mitype)));
break;
}
SetMissAnim(missile, eAnim);
missile.setAnimation(eAnim);
CheckMissileCol(missile, damageType, eMind, eMaxd, false, missile.position.tile, true);
} else {
if (missile.position.tile != Point { missile.var1, missile.var2 }) {
@ -2904,7 +2898,7 @@ void ProcessGenericProjectile(Missile &missile)
if (missile.duration == 0) {
missile._miDelFlag = true;
Point dst = { 0, 0 };
auto dir = static_cast<Direction>(missile._mimfnum);
Direction dir = missile.getDirection();
switch (missile._mitype) {
case MissileID::Firebolt:
case MissileID::MagmaBall:
@ -2975,10 +2969,10 @@ void ProcessAcidPuddle(Missile &missile)
CheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, true, missile.position.tile, false);
missile.duration = range;
if (range == 0) {
if (missile._mimfnum != 0) {
if (missile.getFrameGroup<AcidPuddleFrame>() != AcidPuddleFrame::Idle) {
missile._miDelFlag = true;
} else {
SetMissDir(missile, 1);
missile.setFrameGroup<AcidPuddleFrame>(AcidPuddleFrame::End);
missile.duration = missile._miAnimLen;
}
}
@ -2992,11 +2986,11 @@ void ProcessFireWall(Missile &missile)
missile.duration--;
if (missile.duration == missile.var1) {
SetMissDir(missile, 1);
missile.setFrameGroup<FireWallFrame>(FireWallFrame::Idle);
missile._miAnimFrame = GenerateRnd(11) + 1;
}
if (missile.duration == missile._miAnimLen - 1) {
SetMissDir(missile, 0);
missile.setFrameGroup<FireWallFrame>(FireWallFrame::Start);
missile._miAnimFrame = 13;
missile._miAnimAdd = -1;
}
@ -3006,7 +3000,7 @@ void ProcessFireWall(Missile &missile)
AddUnLight(missile._mlid);
}
constexpr int MaxExpLightIndex = ExpLightLen - 1;
if (missile._mimfnum == 0 && missile.duration != 0 && missile.var2 <= MaxExpLightIndex * 2) {
if (missile.getFrameGroup<FireWallFrame>() == FireWallFrame::Start && missile.duration != 0 && missile.var2 <= MaxExpLightIndex * 2) {
if (missile.var2 == 0)
missile._mlid = AddLight(missile.position.tile, ExpLight[0]);
int expLightIndex = MaxExpLightIndex - std::abs(missile.var2 - MaxExpLightIndex);
@ -3071,8 +3065,8 @@ void ProcessFireball(Missile &missile)
|| (TransList[dTransVal[missilePosition.x][missilePosition.y - 1]] && TileHasAny(missilePosition + Direction::NorthEast, TileProperties::Solid)))) {
missile.position.offset.deltaX -= 32;
}
missile._mimfnum = 0;
SetMissAnim(missile, MissileGraphicID::BigExplosion);
missile.setDefaultFrameGroup();
missile.setAnimation(MissileGraphicID::BigExplosion);
missile.duration = missile._miAnimLen - 1;
missile.position.velocity = {};
} else if (missile.position.tile != Point { missile.var1, missile.var2 }) {
@ -3314,8 +3308,8 @@ void ProcessTownPortal(Missile &missile)
if (missile.duration > 1)
missile.duration--;
if (missile.duration == missile.var1)
SetMissDir(missile, 1);
if (leveltype != DTYPE_TOWN && missile._mimfnum != 1 && missile.duration != 0) {
missile.setFrameGroup<PortalFrame>(PortalFrame::Idle);
if (leveltype != DTYPE_TOWN && missile.getFrameGroup<PortalFrame>() != PortalFrame::Idle && missile.duration != 0) {
if (missile.var2 == 0)
missile._mlid = AddLight(missile.position.tile, 1);
ChangeLight(missile._mlid, missile.position.tile, expLight[missile.var2]);
@ -3405,7 +3399,7 @@ void ProcessFlameWave(Missile &missile)
missile.var1++;
if (missile.var1 == missile._miAnimLen) {
SetMissDir(missile, 1);
missile.setFrameGroup<FireWallFrame>(FireWallFrame::Idle);
missile._miAnimFrame = GenerateRnd(11) + 1;
}
int j = missile.duration;
@ -3435,8 +3429,8 @@ void ProcessGuardian(Missile &missile)
if (missile.var2 > 0) {
missile.var2--;
}
if (missile.duration == missile.var1 || (missile._mimfnum == 2 && missile.var2 == 0)) {
SetMissDir(missile, 1);
if (missile.duration == missile.var1 || (missile.getFrameGroup<GuardianFrame>() == GuardianFrame::Attack && missile.var2 == 0)) {
missile.setFrameGroup<GuardianFrame>(GuardianFrame::Idle);
}
Point position = missile.position.tile;
@ -3478,7 +3472,7 @@ void ProcessGuardian(Missile &missile)
}
if (missile.duration == 14) {
SetMissDir(missile, 0);
missile.setFrameGroup<GuardianFrame>(GuardianFrame::Start);
missile._miAnimFrame = 15;
missile._miAnimAdd = -1;
}
@ -3633,9 +3627,9 @@ void ProcessStoneCurse(Missile &missile)
missile.duration--;
Monster &monster = Monsters[missile.var2];
if (monster.hitPoints == 0 && missile._miAnimType != MissileGraphicID::StoneCurseShatter) {
missile._mimfnum = 0;
missile.setDefaultFrameGroup();
missile._miDrawFlag = true;
SetMissAnim(missile, MissileGraphicID::StoneCurseShatter);
missile.setAnimation(MissileGraphicID::StoneCurseShatter);
missile.duration = 11;
}
if (monster.mode != MonsterMode::Petrified) {
@ -3965,10 +3959,10 @@ void ProcessChargedBolt(Missile &missile)
MoveMissileAndCheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, false, false);
if (missile._miHitFlag) {
missile.var1 = 8;
missile._mimfnum = 0;
missile.setDefaultFrameGroup();
missile.position.offset = { 0, 0 };
missile.position.velocity = {};
SetMissAnim(missile, MissileGraphicID::Lightning);
missile.setAnimation(MissileGraphicID::Lightning);
missile.duration = missile._miAnimLen;
}
ChangeLight(missile._mlid, missile.position.tile, missile.var1);
@ -3987,8 +3981,8 @@ void ProcessHolyBolt(Missile &missile)
int dam = missile._midam;
MoveMissileAndCheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), dam, dam, true, true);
if (missile.duration == 0) {
missile._mimfnum = 0;
SetMissAnim(missile, MissileGraphicID::HolyBoltExplosion);
missile.setDefaultFrameGroup();
missile.setAnimation(MissileGraphicID::HolyBoltExplosion);
missile.duration = missile._miAnimLen - 1;
missile.position.StopMissile();
} else {
@ -4047,11 +4041,11 @@ void ProcessElemental(Missile &missile)
auto *nextMonster = FindClosest(missilePosition, 19);
if (nextMonster != nullptr) {
Direction sd = GetDirection(missilePosition, nextMonster->position.tile);
SetMissDir(missile, sd);
missile.setDirection(sd);
UpdateMissileVelocity(missile, nextMonster->position.tile, 16);
} else {
Direction sd = Players[missile._misource]._pdir;
SetMissDir(missile, sd);
missile.setDirection(sd);
UpdateMissileVelocity(missile, missilePosition + sd, 16);
}
}
@ -4061,8 +4055,8 @@ void ProcessElemental(Missile &missile)
ChangeLight(missile._mlid, missilePosition, 8);
}
if (missile.duration == 0) {
missile._mimfnum = 0;
SetMissAnim(missile, MissileGraphicID::BigExplosion);
missile.setDefaultFrameGroup();
missile.setAnimation(MissileGraphicID::BigExplosion);
missile.duration = missile._miAnimLen - 1;
missile.position.StopMissile();
}
@ -4074,7 +4068,7 @@ void ProcessBoneSpirit(Missile &missile)
{
missile.duration--;
int dam = missile._midam;
if (missile._mimfnum == 8) {
if (missile.getDirection() == Direction::NoDirection) {
ChangeLight(missile._mlid, missile.position.tile, missile._miAnimFrame);
if (missile.duration == 0) {
missile._miDelFlag = true;
@ -4092,11 +4086,11 @@ void ProcessBoneSpirit(Missile &missile)
auto *monster = FindClosest(c, 19);
if (monster != nullptr) {
missile._midam = monster->hitPoints >> 7;
SetMissDir(missile, GetDirection(c, monster->position.tile));
missile.setDirection(GetDirection(c, monster->position.tile));
UpdateMissileVelocity(missile, monster->position.tile, 16);
} else {
Direction sd = Players[missile._misource]._pdir;
SetMissDir(missile, sd);
missile.setDirection(sd);
UpdateMissileVelocity(missile, c + sd, 16);
}
}
@ -4106,7 +4100,7 @@ void ProcessBoneSpirit(Missile &missile)
ChangeLight(missile._mlid, c, 8);
}
if (missile.duration == 0) {
SetMissDir(missile, 8);
missile.setDirection(Direction::NoDirection);
missile.position.velocity = {};
missile.duration = 7;
}
@ -4129,9 +4123,9 @@ void ProcessRedPortal(Missile &missile)
if (missile.duration > 1)
missile.duration--;
if (missile.duration == missile.var1)
SetMissDir(missile, 1);
missile.setFrameGroup<RedPortalFrame>(RedPortalFrame::Idle);
if (leveltype != DTYPE_TOWN && missile._mimfnum != 1 && missile.duration != 0) {
if (leveltype != DTYPE_TOWN && missile.getFrameGroup<RedPortalFrame>() != RedPortalFrame::Idle && missile.duration != 0) {
if (missile.var2 == 0)
missile._mlid = AddLight(missile.position.tile, 1);
ChangeLight(missile._mlid, missile.position.tile, expLight[missile.var2]);
@ -4203,7 +4197,7 @@ void SetUpMissileAnimationData()
continue;
if (missile._mitype != MissileID::Rhino) {
missile._miAnimData = GetMissileSpriteData(missile._miAnimType).spritesForDirection(missile._mimfnum);
missile._miAnimData = GetMissileSpriteData(missile._miAnimType).spritesForDirection(missile.getDirection16());
continue;
}
@ -4217,7 +4211,7 @@ void SetUpMissileAnimationData()
} else {
graphic = MonsterGraphic::Walk;
}
missile._miAnimData = mon.getAnimData(graphic).spritesForDirection(static_cast<Direction>(missile._mimfnum));
missile._miAnimData = mon.getAnimData(graphic).spritesForDirection(missile.getDirection());
}
}

155
Source/missiles.h

@ -54,52 +54,47 @@ struct MissilePosition {
}
};
/**
* Represent a more fine-grained direction than the 8 value Direction enum.
*
* This is used when rendering projectiles like arrows which have additional sprites for "half-winds" on a 16-point compass.
* The sprite sheets are typically 0-indexed and use the following layout (relative to the screen projection)
*
* W WSW SW SSW S
* ^
* WNW | SSE
* |
* NW -------+------> SE
* |
* NNW | ESE
* |
* N NNE NE ENE E
*/
enum class Direction16 : uint8_t {
South,
South_SouthWest,
SouthWest,
West_SouthWest,
West,
West_NorthWest,
NorthWest,
North_NorthWest,
North,
North_NorthEast,
NorthEast,
East_NorthEast,
East,
East_SouthEast,
SouthEast,
South_SouthEast,
};
enum class MissileSource : uint8_t {
Player,
Monster,
Trap,
};
enum class GuardianFrame : uint8_t {
Start = 0,
Idle = 1,
Attack = 2,
};
enum class AcidPuddleFrame : uint8_t {
Idle = 0,
End = 1,
};
enum class FireWallFrame : uint8_t {
Start = 0,
Idle = 1,
};
enum class PortalFrame : uint8_t {
Start = 0,
Idle = 1,
};
enum class RedPortalFrame : uint8_t {
Start = 0,
Idle = 1,
};
struct Missile {
/** Type of projectile */
MissileID _mitype;
MissilePosition position;
private:
int _mimfnum; // The direction of the missile (direction enum)
public:
int _mispllvl;
bool _miDelFlag; // Indicate whether the missile should be deleted
MissileGraphicID _miAnimType;
@ -175,6 +170,69 @@ struct Missile {
return MissileSource::Monster;
return MissileSource::Player;
}
void setAnimation(MissileGraphicID animtype);
/**
* @brief Sets the missile sprite to the given sheet frame
* @param dir Sprite frame
*/
void setFrameGroupRaw(int frameGroup)
{
_mimfnum = frameGroup;
setAnimation(_miAnimType);
}
void setDefaultFrameGroup()
{
setFrameGroupRaw(0);
}
template <typename FrameEnum>
void setFrameGroup(FrameEnum frameGroup)
{
setFrameGroupRaw(static_cast<int>(frameGroup));
}
/**
* @brief Sets the sprite for this missile so it matches the given Direction
* @param dir Desired facing
*/
void setDirection(Direction dir)
{
setFrameGroupRaw(static_cast<int>(dir));
}
/**
* @brief Sets the sprite for this missile so it matches the given Direction16
* @param dir Desired facing at a 22.8125 degree resolution
*/
void setDirection(Direction16 dir)
{
setFrameGroupRaw(static_cast<int>(dir));
}
int getFrameGroupRaw() const
{
return _mimfnum;
}
template <typename FrameEnum>
FrameEnum getFrameGroup() const
{
static_assert(std::is_enum_v<FrameEnum>, "Frame group must be an enum");
return static_cast<FrameEnum>(_mimfnum);
}
[[nodiscard]] Direction getDirection() const
{
return static_cast<Direction>(_mimfnum);
}
[[nodiscard]] Direction16 getDirection16() const
{
return static_cast<Direction16>(_mimfnum);
}
};
extern std::list<Missile> Missiles;
@ -214,33 +272,6 @@ bool PlayerMHit(Player &player, Monster *monster, int dist, int mind, int maxd,
*/
bool IsMissileBlockedByTile(Point position);
/**
* @brief Sets the missile sprite to the given sheet frame
* @param missile this object
* @param dir Sprite frame, typically representing a direction but there are some exceptions (arrows being 1 indexed, directionless spells)
*/
void SetMissDir(Missile &missile, int dir);
/**
* @brief Sets the sprite for this missile so it matches the given Direction
* @param missile this object
* @param dir Desired facing
*/
inline void SetMissDir(Missile &missile, Direction dir)
{
SetMissDir(missile, static_cast<int>(dir));
}
/**
* @brief Sets the sprite for this missile so it matches the given Direction16
* @param missile this object
* @param dir Desired facing at a 22.8125 degree resolution
*/
inline void SetMissDir(Missile &missile, Direction16 dir)
{
SetMissDir(missile, static_cast<int>(dir));
}
void InitMissiles();
struct AddMissileParameter {

24
Source/monster.cpp

@ -1747,7 +1747,7 @@ bool AiPlanPath(Monster &monster)
}
bool clear = LineClear(
[&monster](Point position) { return IsTileAvailable(monster, position); },
[&monster](Point position) { return (IsTileWalkable(position) && IsTileSafe(monster, position)); },
monster.position.tile,
monster.enemyPosition);
if (!clear || (monster.pathCount >= 5 && monster.pathCount < 8)) {
@ -3577,15 +3577,17 @@ tl::expected<void, std::string> InitMonsters()
numscattypes++;
}
}
while (ActiveMonsterCount < totalmonsters) {
const size_t typeIndex = scattertypes[GenerateRnd(numscattypes)];
if (currlevel == 1 || FlipCoin())
na = 1;
else if (currlevel == 2 || leveltype == DTYPE_CRYPT)
na = GenerateRnd(2) + 2;
else
na = GenerateRnd(3) + 3;
PlaceGroup(typeIndex, na);
if (numscattypes > 0) {
while (ActiveMonsterCount < totalmonsters) {
const size_t typeIndex = scattertypes[GenerateRnd(numscattypes)];
if (currlevel == 1 || FlipCoin())
na = 1;
else if (currlevel == 2 || leveltype == DTYPE_CRYPT)
na = GenerateRnd(2) + 2;
else
na = GenerateRnd(3) + 3;
PlaceGroup(typeIndex, na);
}
}
}
for (int i = 0; i < nt; i++) {
@ -4452,7 +4454,7 @@ void MissToMonst(Missile &missile, Point position)
Point oldPosition = missile.position.tile;
monster.occupyTile(position, false);
monster.direction = static_cast<Direction>(missile._mimfnum);
monster.direction = missile.getDirection();
monster.position.tile = position;
M_StartStand(monster, monster.direction);
M_StartHit(monster, 0);

42
Source/monsters/validation.cpp

@ -0,0 +1,42 @@
/**
* @file monsters/validation.cpp
*
* Implementation of functions for validation of monster data.
*/
#include "monsters/validation.hpp"
#include <cstddef>
#include "monster.h"
#include "player.h"
namespace devilution {
namespace {
bool IsEnemyValid(size_t enemyId, bool checkMonsterTable)
{
if (enemyId < MaxMonsters)
return !checkMonsterTable || Monsters[enemyId].hitPoints > 0;
const size_t playerId = enemyId - MaxMonsters;
return playerId < Players.size() && Players[playerId].plractive;
}
} // namespace
bool IsEnemyIdValid(size_t enemyId)
{
return IsEnemyValid(enemyId, false);
}
bool IsEnemyValid(size_t monsterId, size_t enemyId)
{
if (monsterId >= MaxMonsters)
return false;
if (monsterId == enemyId)
return false;
return IsEnemyValid(enemyId, true);
}
} // namespace devilution

15
Source/monsters/validation.hpp

@ -0,0 +1,15 @@
/**
* @file monsters/validation.hpp
*
* Interface of functions for validation of monster data.
*/
#pragma once
#include <cstddef>
namespace devilution {
bool IsEnemyIdValid(size_t enemyId);
bool IsEnemyValid(size_t monsterId, size_t enemyId);
} // namespace devilution

762
Source/msg.cpp

File diff suppressed because it is too large Load Diff

3
Source/msg.h

@ -756,6 +756,7 @@ void NetSendCmdDamage(bool bHiPri, const Player &player, uint32_t dwDam, DamageT
void NetSendCmdMonDmg(bool bHiPri, uint16_t wMon, uint32_t dwDam);
void NetSendCmdString(uint32_t pmask, const char *pszStr);
void delta_close_portal(const Player &player);
size_t ParseCmd(uint8_t pnum, const TCmd *pCmd);
bool ValidateCmdSize(size_t requiredCmdSize, size_t maxCmdSize, size_t playerId);
size_t ParseCmd(uint8_t pnum, const TCmd *pCmd, size_t maxCmdSize);
} // namespace devilution

5
Source/multi.cpp

@ -3,6 +3,7 @@
*
* Implementation of functions for keeping multiplaye games in sync.
*/
#include "multi.h"
#include <cstddef>
#include <cstdint>
@ -20,9 +21,11 @@
#include "engine/random.hpp"
#include "engine/world_tile.hpp"
#include "menu.h"
#include "msg.h"
#include "nthread.h"
#include "options.h"
#include "pfile.h"
#include "player.h"
#include "plrmsg.h"
#include "qol/chatlog.h"
#include "storm/storm_net.hpp"
@ -342,7 +345,7 @@ void BeginTimeout()
void HandleAllPackets(uint8_t pnum, const std::byte *data, size_t size)
{
for (size_t offset = 0; offset < size;) {
size_t messageSize = ParseCmd(pnum, reinterpret_cast<const TCmd *>(&data[offset]));
size_t messageSize = ParseCmd(pnum, reinterpret_cast<const TCmd *>(&data[offset]), size - offset);
if (messageSize == 0) {
break;
}

20
Source/panels/spell_book.cpp

@ -55,22 +55,8 @@ const SpellID SpellPages[SpellBookPages][SpellBookPageEntries] = {
SpellID GetSpellFromSpellPage(size_t page, size_t entry)
{
assert(page <= SpellBookPages && entry <= SpellBookPageEntries);
if (page == 0 && entry == 0) {
switch (InspectPlayer->_pClass) {
case HeroClass::Warrior:
return SpellID::ItemRepair;
case HeroClass::Rogue:
return SpellID::TrapDisarm;
case HeroClass::Sorcerer:
return SpellID::StaffRecharge;
case HeroClass::Monk:
return SpellID::Search;
case HeroClass::Bard:
return SpellID::Identify;
case HeroClass::Barbarian:
return SpellID::Rage;
}
}
if (page == 0 && entry == 0)
return GetPlayerStartingLoadoutForClass(InspectPlayer->_pClass).skill;
return SpellPages[page][entry];
}
@ -89,7 +75,7 @@ void PrintSBookStr(const Surface &out, Point position, std::string_view text, Ui
SpellType GetSBookTrans(SpellID ii, bool townok)
{
Player &player = *InspectPlayer;
if ((player._pClass == HeroClass::Monk) && (ii == SpellID::Search))
if (ii == GetPlayerStartingLoadoutForClass(player._pClass).skill)
return SpellType::Skill;
SpellType st = SpellType::Spell;
if ((player._pISpells & GetSpellBitmask(ii)) != 0) {

2
Source/panels/spell_list.cpp

@ -57,7 +57,7 @@ bool GetSpellListSelection(SpellID &pSpell, SpellType &pSplType)
if (spellListItem.isSelected) {
pSpell = spellListItem.id;
pSplType = spellListItem.type;
if (myPlayer._pClass == HeroClass::Monk && spellListItem.id == SpellID::Search)
if (spellListItem.id == GetPlayerStartingLoadoutForClass(myPlayer._pClass).skill)
pSplType = SpellType::Skill;
return true;
}

2
Source/portal.cpp

@ -53,7 +53,7 @@ void AddPortalMissile(const Player &player, Point position, bool sync)
if (missile != nullptr) {
// Don't show portal opening animation if we sync existing portals
if (sync)
SetMissDir(*missile, 1);
missile->setFrameGroup<PortalFrame>(PortalFrame::Idle);
if (leveltype != DTYPE_TOWN)
missile->_mlid = AddLight(missile->position.tile, 15);

49
Source/portals/validation.cpp

@ -0,0 +1,49 @@
/**
* @file portals/validation.cpp
*
* Implementation of functions for validation of portal data.
*/
#include "portals/validation.hpp"
#include <cstdint>
#include "engine/world_tile.hpp"
#include "levels/gendung.h"
#include "levels/setmaps.h"
#include "quests.h"
namespace devilution {
namespace {
dungeon_type GetQuestLevelType(_setlevels questLevel)
{
for (const Quest &quest : Quests) {
if (quest._qslvl == questLevel)
return quest._qlvltype;
}
return DTYPE_NONE;
}
dungeon_type GetSetLevelType(_setlevels setLevel)
{
bool isArenaLevel = setLevel >= SL_FIRST_ARENA && setLevel <= SL_LAST;
return isArenaLevel ? GetArenaLevelType(setLevel) : GetQuestLevelType(setLevel);
}
} // namespace
bool IsPortalDeltaValid(WorldTilePosition location, uint8_t level, uint8_t ltype, bool isOnSetLevel)
{
if (!InDungeonBounds(location))
return false;
dungeon_type levelType = static_cast<dungeon_type>(ltype);
if (levelType == DTYPE_NONE)
return false;
if (isOnSetLevel)
return levelType == GetSetLevelType(static_cast<_setlevels>(level));
return levelType == GetLevelType(level);
}
} // namespace devilution

16
Source/portals/validation.hpp

@ -0,0 +1,16 @@
/**
* @file portals/validation.hpp
*
* Interface of functions for validation of portal data.
*/
#pragma once
#include <cstdint>
#include "engine/world_tile.hpp"
namespace devilution {
bool IsPortalDeltaValid(WorldTilePosition location, uint8_t level, uint8_t levelType, bool isOnSetLevel);
} // namespace devilution

46
Source/quests/validation.cpp

@ -0,0 +1,46 @@
/**
* @file quests/validation.cpp
*
* Implementation of functions for validation of quest data.
*/
#include "quests/validation.hpp"
#include <cstdint>
#include "objdat.h"
#include "quests.h"
#include "textdat.h"
#include "utils/is_of.hpp"
namespace devilution {
bool IsQuestDeltaValid(quest_id qidx, quest_state qstate, uint8_t qlog, int16_t qmsg)
{
if (IsNoneOf(qlog, 0, 1))
return false;
if (qmsg < 0 || static_cast<size_t>(qmsg) >= SpeechCount)
return false;
switch (qstate) {
case QUEST_NOTAVAIL:
case QUEST_INIT:
case QUEST_ACTIVE:
case QUEST_DONE:
return true;
case QUEST_HIVE_TEASE1:
case QUEST_HIVE_TEASE2:
case QUEST_HIVE_ACTIVE:
return qidx == Q_JERSEY;
case QUEST_HIVE_DONE:
return IsAnyOf(qidx, Q_FARMER, Q_JERSEY);
default:
return false;
}
}
} // namespace devilution

17
Source/quests/validation.hpp

@ -0,0 +1,17 @@
/**
* @file quests/validation.hpp
*
* Interface of functions for validation of quest data.
*/
#pragma once
#include <cstdint>
#include "objdat.h"
#include "quests.h"
namespace devilution {
bool IsQuestDeltaValid(quest_id qidx, quest_state qstate, uint8_t qlog, int16_t qmsg);
} // namespace devilution

44
Source/sync.cpp

@ -10,6 +10,7 @@
#include "levels/gendung.h"
#include "lighting.h"
#include "monster.h"
#include "monsters/validation.hpp"
#include "player.h"
#include "utils/is_of.hpp"
@ -205,29 +206,7 @@ void SyncMonster(bool isOwner, const TSyncMonster &monsterSync)
monster.whoHit |= monsterSync.mWhoHit;
}
bool IsEnemyIdValid(const Monster &monster, uint8_t enemyId)
{
if (enemyId > MaxMonsters) {
enemyId -= MaxMonsters;
if (enemyId >= Players.size())
return false;
return Players[enemyId].plractive;
}
const Monster &enemy = Monsters[enemyId];
if (&enemy == &monster) {
return false;
}
if (enemy.hitPoints <= 0) {
return false;
}
return true;
}
bool IsTSyncMonsterValidate(const TSyncMonster &monsterSync)
bool IsTSyncMonsterValid(const TSyncMonster &monsterSync)
{
const size_t monsterId = monsterSync._mndx;
@ -237,12 +216,17 @@ bool IsTSyncMonsterValidate(const TSyncMonster &monsterSync)
if (!InDungeonBounds({ monsterSync._mx, monsterSync._my }))
return false;
if (!IsEnemyIdValid(Monsters[monsterId], monsterSync._menemy))
if (!IsEnemyIdValid(monsterSync._menemy))
return false;
return true;
}
bool IsTSyncEnemyValid(const TSyncMonster &monsterSync)
{
return IsEnemyValid(monsterSync._mndx, monsterSync._menemy);
}
} // namespace
size_t sync_all_monsters(std::byte *pbBuf, size_t dwMaxLen)
@ -289,11 +273,13 @@ size_t sync_all_monsters(std::byte *pbBuf, size_t dwMaxLen)
return dwMaxLen;
}
uint32_t OnSyncData(const TCmd *pCmd, const Player &player)
size_t OnSyncData(const TSyncHeader &header, size_t maxCmdSize, const Player &player)
{
const auto &header = *reinterpret_cast<const TSyncHeader *>(pCmd);
const uint16_t wLen = SDL_SwapLE16(header.wLen);
if (!ValidateCmdSize(wLen + sizeof(header), maxCmdSize, player.getId()))
return maxCmdSize;
assert(gbBufferMsgs != 2);
if (gbBufferMsgs == 1) {
@ -310,14 +296,16 @@ uint32_t OnSyncData(const TCmd *pCmd, const Player &player)
bool syncLocalLevel = !MyPlayer->_pLvlChanging && GetLevelForMultiplayer(*MyPlayer) == level;
if (IsValidLevelForMultiplayer(level)) {
const auto *monsterSyncs = reinterpret_cast<const TSyncMonster *>(pCmd + sizeof(header));
const auto *monsterSyncs = reinterpret_cast<const TSyncMonster *>(&header + sizeof(header));
bool isOwner = player.getId() > MyPlayerId;
for (int i = 0; i < monsterCount; i++) {
if (!IsTSyncMonsterValidate(monsterSyncs[i]))
if (!IsTSyncMonsterValid(monsterSyncs[i]))
continue;
if (syncLocalLevel) {
if (!IsTSyncEnemyValid(monsterSyncs[i]))
continue;
SyncMonster(isOwner, monsterSyncs[i]);
}

5
Source/sync.h

@ -8,10 +8,13 @@
#include <cstddef>
#include <cstdint>
#include "msg.h"
#include "player.h"
namespace devilution {
size_t sync_all_monsters(std::byte *pbBuf, size_t dwMaxLen);
uint32_t OnSyncData(const TCmd *pCmd, const Player &player);
size_t OnSyncData(const TSyncHeader &header, size_t maxCmdSize, const Player &player);
void sync_init();
} // namespace devilution

3
Source/textdat.cpp

@ -916,4 +916,7 @@ const Speech Speeches[] = {
1, 5, SfxID::Adria48 },
*/
};
const size_t SpeechCount = sizeof(Speeches) / sizeof(Speech);
} // namespace devilution

2
Source/textdat.h

@ -5,6 +5,7 @@
*/
#pragma once
#include <cstddef>
#include <cstdint>
#include "sound_effect_enums.h"
@ -430,6 +431,7 @@ struct Speech {
SfxID sfxnr;
};
extern const size_t SpeechCount;
extern const Speech Speeches[];
} // namespace devilution

6
test/missiles_test.cpp

@ -29,9 +29,9 @@ void TestAnimatedMissileRotatesUniformly(Missile &missile, int startingDir, int
{
ankerl::unordered_dense::map<int, unsigned> observed {};
for (auto i = 0; i < 100; i++) {
missile._mimfnum = startingDir;
missile.setFrameGroupRaw(startingDir);
TestRotateBlockedMissile(missile);
observed[missile._mimfnum]++;
observed[missile.getFrameGroupRaw()]++;
}
EXPECT_THAT(observed, UnorderedElementsAre(Pair(leftDir, AllOf(Gt(30U), Lt(70U))), Pair(rightDir, AllOf(Gt(30U), Lt(70U))))) << "Animated missiles should rotate either direction roughly 50% of the time";
@ -58,7 +58,7 @@ TEST(Missiles, RotateBlockedMissileArrow)
// All other missiles use the number of 0-indexed sprites defined in MissileSpriteData
missile = *AddMissile({ 0, 0 }, { 0, 0 }, Direction::South, MissileID::Firebolt, TARGET_MONSTERS, player, 0, 0);
EXPECT_EQ(missile._mimfnum, 0);
EXPECT_EQ(missile.getFrameGroupRaw(), 0);
TestAnimatedMissileRotatesUniformly(missile, 5, 4, 6);
TestAnimatedMissileRotatesUniformly(missile, 0, 15, 1);
TestAnimatedMissileRotatesUniformly(missile, 15, 14, 0);

Loading…
Cancel
Save