diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index ae263a5cd..949614a92 100644 --- a/Source/CMakeLists.txt +++ b/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 diff --git a/Source/control.cpp b/Source/control.cpp index ca861e65d..62be5da82 100644 --- a/Source/control.cpp +++ b/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(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) diff --git a/Source/control.h b/Source/control.h index ffb66300d..bfe84b76f 100644 --- a/Source/control.h +++ b/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. diff --git a/Source/encrypt.cpp b/Source/encrypt.cpp index 3af23e2af..2ec05309e 100644 --- a/Source/encrypt.cpp +++ b/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(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(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 ptr = std::make_unique(CMP_BUFFER_SIZE); std::unique_ptr 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 diff --git a/Source/encrypt.h b/Source/encrypt.h index 66f841a3f..1ab49d43b 100644 --- a/Source/encrypt.h +++ b/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 diff --git a/Source/engine/assets.cpp b/Source/engine/assets.cpp index ffc800904..c803fe631 100644 --- a/Source/engine/assets.cpp +++ b/Source/engine/assets.cpp @@ -491,7 +491,7 @@ void LoadModArchives(std::span 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); } diff --git a/Source/engine/render/scrollrt.cpp b/Source/engine/render/scrollrt.cpp index e9e5b92e6..6656182c2 100644 --- a/Source/engine/render/scrollrt.cpp +++ b/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); } diff --git a/Source/items/validation.cpp b/Source/items/validation.cpp index 1762922ca..a04a15b86 100644 --- a/Source/items/validation.cpp +++ b/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 diff --git a/Source/items/validation.h b/Source/items/validation.h index 98bbabc6b..1ef732d7d 100644 --- a/Source/items/validation.h +++ b/Source/items/validation.h @@ -7,17 +7,19 @@ #include +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 diff --git a/Source/levels/setmaps.h b/Source/levels/setmaps.h index 0d2a8fe51..59cc0a93a 100644 --- a/Source/levels/setmaps.h +++ b/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 */ diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index a57e7c9ed..a4f645cae 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -771,7 +771,7 @@ void LoadMissile(LoadHelper *file) missile.position.start.y = file->NextLE(); missile.position.traveled.deltaX = file->NextLE(); missile.position.traveled.deltaY = file->NextLE(); - missile._mimfnum = file->NextLE(); + missile.setFrameGroupRaw(file->NextLE()); missile._mispllvl = file->NextLE(); missile._miDelFlag = file->NextBool32(); missile._miAnimType = static_cast(file->NextLE()); @@ -1541,7 +1541,7 @@ void SaveMissile(SaveHelper *file, const Missile &missile) file->WriteLE(missile.position.start.y); file->WriteLE(missile.position.traveled.deltaX); file->WriteLE(missile.position.traveled.deltaY); - file->WriteLE(missile._mimfnum); + file->WriteLE(missile.getFrameGroupRaw()); file->WriteLE(missile._mispllvl); file->WriteLE(missile._miDelFlag ? 1 : 0); file->WriteLE(static_cast(missile._miAnimType)); diff --git a/Source/misdat.h b/Source/misdat.h index 4d50bb9b7..3be438c6a 100644 --- a/Source/misdat.h +++ b/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(direction)] : sprites->list(); } }; diff --git a/Source/missiles.cpp b/Source/missiles.cpp index ccc367006..57c8e7a04 100644 --- a/Source/missiles.cpp +++ b/Source/missiles.cpp @@ -40,6 +40,38 @@ namespace devilution { std::list 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(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(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(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::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 ¶meter) 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 ¶meter) } 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 ¶meter) } } 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 ¶meter) 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 ¶meter) 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 ¶meter) { 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(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 ¶meter) 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 ¶meter) void AddAcid(Missile &missile, AddMissileParameter ¶meter) { 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 ¶meter) 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 ¶meter) 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 ¶meter) 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(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(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::Idle) { missile._miDelFlag = true; } else { - SetMissDir(missile, 1); + missile.setFrameGroup(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::Idle); missile._miAnimFrame = GenerateRnd(11) + 1; } if (missile.duration == missile._miAnimLen - 1) { - SetMissDir(missile, 0); + missile.setFrameGroup(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::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::Idle); + if (leveltype != DTYPE_TOWN && missile.getFrameGroup() != 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::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::Attack && missile.var2 == 0)) { + missile.setFrameGroup(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::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::Idle); - if (leveltype != DTYPE_TOWN && missile._mimfnum != 1 && missile.duration != 0) { + if (leveltype != DTYPE_TOWN && missile.getFrameGroup() != 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(missile._mimfnum)); + missile._miAnimData = mon.getAnimData(graphic).spritesForDirection(missile.getDirection()); } } diff --git a/Source/missiles.h b/Source/missiles.h index b9af0d8a2..1b7e110a2 100644 --- a/Source/missiles.h +++ b/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 + void setFrameGroup(FrameEnum frameGroup) + { + setFrameGroupRaw(static_cast(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(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(dir)); + } + + int getFrameGroupRaw() const + { + return _mimfnum; + } + + template + FrameEnum getFrameGroup() const + { + static_assert(std::is_enum_v, "Frame group must be an enum"); + return static_cast(_mimfnum); + } + + [[nodiscard]] Direction getDirection() const + { + return static_cast(_mimfnum); + } + + [[nodiscard]] Direction16 getDirection16() const + { + return static_cast(_mimfnum); + } }; extern std::list 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(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(dir)); -} - void InitMissiles(); struct AddMissileParameter { diff --git a/Source/monster.cpp b/Source/monster.cpp index a4c050341..ecf6bf42e 100644 --- a/Source/monster.cpp +++ b/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 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(missile._mimfnum); + monster.direction = missile.getDirection(); monster.position.tile = position; M_StartStand(monster, monster.direction); M_StartHit(monster, 0); diff --git a/Source/monsters/validation.cpp b/Source/monsters/validation.cpp new file mode 100644 index 000000000..e7e043497 --- /dev/null +++ b/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 + +#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 diff --git a/Source/monsters/validation.hpp b/Source/monsters/validation.hpp new file mode 100644 index 000000000..30bf9c116 --- /dev/null +++ b/Source/monsters/validation.hpp @@ -0,0 +1,15 @@ +/** + * @file monsters/validation.hpp + * + * Interface of functions for validation of monster data. + */ +#pragma once + +#include + +namespace devilution { + +bool IsEnemyIdValid(size_t enemyId); +bool IsEnemyValid(size_t monsterId, size_t enemyId); + +} // namespace devilution diff --git a/Source/msg.cpp b/Source/msg.cpp index cd9612395..767cebbf1 100644 --- a/Source/msg.cpp +++ b/Source/msg.cpp @@ -3,6 +3,8 @@ * * Implementation of function for sending and receiving network messages. */ +#include "msg.h" + #include #include #include @@ -26,17 +28,22 @@ #include "engine/random.hpp" #include "engine/world_tile.hpp" #include "gamemenu.h" +#include "items/validation.h" #include "levels/crypt.h" #include "levels/town.h" #include "levels/trigs.h" #include "lighting.h" #include "missiles.h" +#include "monsters/validation.hpp" #include "nthread.h" #include "objects.h" #include "options.h" #include "pack.h" #include "pfile.h" +#include "player.h" #include "plrmsg.h" +#include "portals/validation.hpp" +#include "quests/validation.hpp" #include "spells.h" #include "storm/storm_net.hpp" #include "sync.h" @@ -45,6 +52,7 @@ #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/str_cat.hpp" +#include "utils/str_split.hpp" #include "utils/utf8.hpp" #define ValidateField(logValue, condition) \ @@ -263,10 +271,19 @@ uint32_t sgdwRecvOffset; int sgnCurrMegaPlayer; ankerl::unordered_dense::map DeltaLevels; uint8_t sbLastCmd; + /** * @brief buffer used to receive level deltas, size is the worst expected case assuming every object on a level was touched */ -std::byte sgRecvBuf[1U + sizeof(DLevel::item) + sizeof(uint8_t) + (sizeof(WorldTilePosition) + sizeof(_cmd_id)) * MAXOBJECTS + sizeof(DLevel::monster)]; +std::byte sgRecvBuf[1U /* marker byte, always 0 */ + + sizeof(uint8_t) /* level id */ + + sizeof(DLevel::item) /* items spawned during dungeon generation which have been picked up, and items dropped by a player during a game */ + + sizeof(uint8_t) /* count of object interactions which caused a state change since dungeon generation */ + + (sizeof(WorldTilePosition) + sizeof(_cmd_id)) * MAXOBJECTS /* location/action pairs for the object interactions */ + + sizeof(DLevel::monster) /* latest monster state */ + + sizeof(uint16_t) /* spawned monster count */ + + (sizeof(uint16_t) + sizeof(DSpawnedMonster)) * MaxMonsters]; /* spawned monsters */ + _cmd_id sgbRecvCmd; ankerl::unordered_dense::map LocalLevels; DJunk sgJunk; @@ -277,6 +294,17 @@ Item ItemLimbo; /** @brief Last sent player command for the local player. */ TCmdLocParam5 lastSentPlayerCmd; +bool IsPortalDeltaValid(const DPortal &portal) +{ + const WorldTilePosition position { portal.x, portal.y }; + return IsPortalDeltaValid(position, portal.level, portal.ltype, portal.setlvl); +} + +bool IsQuestDeltaValid(quest_id qidx, const MultiQuests &quest) +{ + return IsQuestDeltaValid(qidx, quest.qstate, quest.qlog, quest.qmsg); +} + uint8_t GetLevelForMultiplayer(uint8_t level, bool isSetLevel) { if (isSetLevel) @@ -385,14 +413,14 @@ void PrePacket() uint8_t playerId = std::numeric_limits::max(); for (TMegaPkt &pkt : MegaPktList) { std::byte *data = pkt.data; - size_t spaceLeft = sizeof(pkt.data); - while (spaceLeft != pkt.spaceLeft) { + size_t remainingBytes = sizeof(pkt.data) - pkt.spaceLeft; + while (remainingBytes > 0) { auto cmdId = static_cast<_cmd_id>(*data); if (cmdId == FAKE_CMD_SETID) { auto *cmd = (TFakeCmdPlr *)data; data += sizeof(*cmd); - spaceLeft -= sizeof(*cmd); + remainingBytes -= sizeof(*cmd); playerId = cmd->bPlr; continue; } @@ -400,7 +428,7 @@ void PrePacket() if (cmdId == FAKE_CMD_DROPID) { auto *cmd = (TFakeDropPlr *)data; data += sizeof(*cmd); - spaceLeft -= sizeof(*cmd); + remainingBytes -= sizeof(*cmd); multi_player_left(cmd->bPlr, SDL_SwapLE32(cmd->dwReason)); continue; } @@ -410,19 +438,24 @@ void PrePacket() return; } - size_t size = ParseCmd(playerId, (TCmd *)data); + size_t size = ParseCmd(playerId, (TCmd *)data, remainingBytes); if (size == 0) { Log("Discarding bad network message"); return; } data += size; - spaceLeft -= size; + remainingBytes -= size; } } } void SendPacket(uint8_t pnum, const void *packet, size_t dwSize) { + if (dwSize > sizeof(TMegaPkt::data)) { + Log("Discarding enormous network message"); + return; + } + if (pnum != sgnCurrMegaPlayer) { sgnCurrMegaPlayer = pnum; TFakeCmdPlr cmd; @@ -489,20 +522,26 @@ std::byte *DeltaExportItem(std::byte *dst, const TCmdPItem *src) return dst; } -size_t DeltaImportItem(const std::byte *src, TCmdPItem *dst) +const std::byte *DeltaImportItem(const std::byte *src, const std::byte *end, TCmdPItem *dst) { size_t size = 0; for (int i = 0; i < MAXITEMS; i++, dst++) { + if (&src[size] >= end) + return nullptr; if (src[size] == std::byte { 0xFF }) { memset(dst, 0xFF, sizeof(TCmdPItem)); size++; } else { + if (&src[size] + sizeof(TCmdPItem) > end) + return nullptr; memcpy(dst, &src[size], sizeof(TCmdPItem)); + if (!IsItemDeltaValid(*dst)) + memset(dst, 0xFF, sizeof(TCmdPItem)); size += sizeof(TCmdPItem); } } - return size; + return src + size; } std::byte *DeltaExportObject(std::byte *dst, const ankerl::unordered_dense::map &src) @@ -517,11 +556,21 @@ std::byte *DeltaExportObject(std::byte *dst, const ankerl::unordered_dense::map< return dst; } -const std::byte *DeltaImportObjects(const std::byte *src, ankerl::unordered_dense::map &dst) +const std::byte *DeltaImportObjects(const std::byte *src, const std::byte *end, ankerl::unordered_dense::map &dst) { dst.clear(); + if (src == nullptr || src == end) + return nullptr; + uint8_t numDeltas = static_cast(*src++); + if (numDeltas > MAXOBJECTS) + return nullptr; + + size_t numBytes = (sizeof(WorldTilePosition) + sizeof(_cmd_id)) * numDeltas; + if (src + numBytes > end) + return nullptr; + dst.reserve(numDeltas); for (unsigned i = 0; i < numDeltas; i++) { @@ -547,31 +596,38 @@ std::byte *DeltaExportMonster(std::byte *dst, const DMonsterStr *src) return dst; } -size_t DeltaImportMonster(const std::byte *src, DMonsterStr *dst) +const std::byte *DeltaImportMonster(const std::byte *src, const std::byte *end, DMonsterStr *dst) { + if (src == nullptr) + return nullptr; + size_t size = 0; for (size_t i = 0; i < MaxMonsters; i++, dst++) { + if (&src[size] >= end) + return nullptr; if (src[size] == std::byte { 0xFF }) { memset(dst, 0xFF, sizeof(DMonsterStr)); size++; } else { + if (&src[size] + sizeof(DMonsterStr) > end) + return nullptr; memcpy(dst, &src[size], sizeof(DMonsterStr)); size += sizeof(DMonsterStr); } } - return size; + return src + size; } std::byte *DeltaExportSpawnedMonsters(std::byte *dst, const ankerl::unordered_dense::map &spawnedMonsters) { - auto &size = *reinterpret_cast(dst); - size = static_cast(spawnedMonsters.size()); + uint16_t size = SDL_SwapLE16(static_cast(spawnedMonsters.size())); + memcpy(dst, &size, sizeof(uint16_t)); dst += sizeof(uint16_t); for (const auto &deltaSpawnedMonster : spawnedMonsters) { - auto &monsterId = *reinterpret_cast(dst); - monsterId = static_cast(deltaSpawnedMonster.first); + uint16_t monsterId = SDL_SwapLE16(static_cast(deltaSpawnedMonster.first)); + memcpy(dst, &monsterId, sizeof(uint16_t)); dst += sizeof(uint16_t); memcpy(dst, &deltaSpawnedMonster.second, sizeof(DSpawnedMonster)); @@ -581,14 +637,28 @@ std::byte *DeltaExportSpawnedMonsters(std::byte *dst, const ankerl::unordered_de return dst; } -const std::byte *DeltaImportSpawnedMonsters(const std::byte *src, ankerl::unordered_dense::map &spawnedMonsters) +const std::byte *DeltaImportSpawnedMonsters(const std::byte *src, const std::byte *end, ankerl::unordered_dense::map &spawnedMonsters) { - uint16_t size = *reinterpret_cast(src); + if (src == nullptr || src + sizeof(uint16_t) > end) + return nullptr; + + uint16_t size; + memcpy(&size, src, sizeof(uint16_t)); + size = SDL_SwapLE16(size); src += sizeof(uint16_t); + if (size > MaxMonsters) + return nullptr; + + size_t requiredBytes = (sizeof(uint16_t) + sizeof(DSpawnedMonster)) * size; + if (src + requiredBytes > end) + return nullptr; for (size_t i = 0; i < size; i++) { - uint16_t monsterId = *reinterpret_cast(src); + uint16_t monsterId; + memcpy(&monsterId, src, sizeof(uint16_t)); + monsterId = SDL_SwapLE16(monsterId); src += sizeof(uint16_t); + DSpawnedMonster spawnedMonster; memcpy(&spawnedMonster, src, sizeof(DSpawnedMonster)); src += sizeof(DSpawnedMonster); @@ -618,7 +688,7 @@ std::byte *DeltaExportJunk(std::byte *dst) sgJunk.quests[q].qstate = quest._qactive; sgJunk.quests[q].qvar1 = quest._qvar1; sgJunk.quests[q].qvar2 = quest._qvar2; - sgJunk.quests[q].qmsg = static_cast(quest._qmsg); + sgJunk.quests[q].qmsg = SDL_SwapLE16(static_cast(quest._qmsg)); memcpy(dst, &sgJunk.quests[q], sizeof(MultiQuests)); dst += sizeof(MultiQuests); q++; @@ -627,14 +697,20 @@ std::byte *DeltaExportJunk(std::byte *dst) return dst; } -void DeltaImportJunk(const std::byte *src) +const std::byte *DeltaImportJunk(const std::byte *src, const std::byte *end) { for (int i = 0; i < MAXPORTAL; i++) { + if (src >= end) + return nullptr; if (*src == std::byte { 0xFF }) { memset(&sgJunk.portal[i], 0xFF, sizeof(DPortal)); src++; } else { + if (src + sizeof(DPortal) > end) + return nullptr; memcpy(&sgJunk.portal[i], src, sizeof(DPortal)); + if (!IsPortalDeltaValid(sgJunk.portal[i])) + memset(&sgJunk.portal[i], 0xFF, sizeof(DPortal)); src += sizeof(DPortal); } } @@ -644,10 +720,18 @@ void DeltaImportJunk(const std::byte *src) if (QuestsData[qidx].isSinglePlayerOnly && UseMultiplayerQuests()) { continue; } + if (src + sizeof(MultiQuests) > end) { + return nullptr; + } memcpy(&sgJunk.quests[q], src, sizeof(MultiQuests)); + if (!IsQuestDeltaValid(static_cast(qidx), sgJunk.quests[q])) { + sgJunk.quests[q].qstate = QUEST_INVALID; + } src += sizeof(MultiQuests); q++; } + + return src; } uint32_t CompressData(std::byte *buffer, std::byte *end) @@ -665,43 +749,62 @@ uint32_t CompressData(std::byte *buffer, std::byte *end) #endif } -void DeltaImportData(_cmd_id cmd, uint32_t recvOffset) +void DeltaImportData(_cmd_id cmd, uint32_t recvOffset, int pnum) { + size_t deltaSize = recvOffset; + #ifdef USE_PKWARE - if (sgRecvBuf[0] != std::byte { 0 }) - PkwareDecompress(&sgRecvBuf[1], recvOffset, sizeof(sgRecvBuf) - 1); + if (sgRecvBuf[0] != std::byte { 0 }) { + deltaSize = PkwareDecompress(&sgRecvBuf[1], deltaSize, sizeof(sgRecvBuf) - 1); + if (deltaSize == 0) { + Log("PKWare decompression failure, dropping player {}", pnum); + SNetDropPlayer(pnum, LEAVE_DROP); + return; + } + } #endif const std::byte *src = &sgRecvBuf[1]; + const std::byte *end = src + deltaSize; if (cmd == CMD_DLEVEL_JUNK) { - DeltaImportJunk(src); + src = DeltaImportJunk(src, end); } else if (cmd == CMD_DLEVEL) { uint8_t i = static_cast(src[0]); src += sizeof(uint8_t); DLevel &deltaLevel = GetDeltaLevel(i); - src += DeltaImportItem(src, deltaLevel.item); - src = DeltaImportObjects(src, deltaLevel.object); - src += DeltaImportMonster(src, deltaLevel.monster); - src = DeltaImportSpawnedMonsters(src, deltaLevel.spawnedMonsters); + src = DeltaImportItem(src, end, deltaLevel.item); + src = DeltaImportObjects(src, end, deltaLevel.object); + src = DeltaImportMonster(src, end, deltaLevel.monster); + src = DeltaImportSpawnedMonsters(src, end, deltaLevel.spawnedMonsters); } else { - app_fatal(StrCat("Unknown network message type: ", cmd)); + Log("Received invalid deltas, dropping player {}", pnum); + SNetDropPlayer(pnum, LEAVE_DROP); + return; + } + + if (src == nullptr) { + Log("Received invalid deltas, dropping player {}", pnum); + SNetDropPlayer(pnum, LEAVE_DROP); + return; } sgbDeltaChunks++; } -size_t OnLevelData(uint8_t pnum, const TCmd *pCmd) +size_t OnLevelData(const TCmdPlrInfoHdr &message, size_t maxCmdSize, const Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t wBytes = SDL_SwapLE16(message.wBytes); const uint16_t wOffset = SDL_SwapLE16(message.wOffset); - if (gbDeltaSender != pnum) { + if (!ValidateCmdSize(wBytes + sizeof(message), maxCmdSize, player.getId())) + return maxCmdSize; + + if (gbDeltaSender != player.getId()) { if (message.bCmd != CMD_DLEVEL_END && (message.bCmd != CMD_DLEVEL || wOffset != 0)) { return wBytes + sizeof(message); } - gbDeltaSender = pnum; + gbDeltaSender = player.getId(); sgbRecvCmd = CMD_DLEVEL_END; } @@ -717,7 +820,7 @@ size_t OnLevelData(uint8_t pnum, const TCmd *pCmd) sgdwRecvOffset = 0; sgbRecvCmd = message.bCmd; } else if (sgbRecvCmd != message.bCmd || wOffset == 0) { - DeltaImportData(sgbRecvCmd, sgdwRecvOffset); + DeltaImportData(sgbRecvCmd, sgdwRecvOffset, player.getId()); if (message.bCmd == CMD_DLEVEL_END) { sgbDeltaChunks = MAX_CHUNKS - 1; sgbRecvCmd = CMD_DLEVEL_END; @@ -727,8 +830,14 @@ size_t OnLevelData(uint8_t pnum, const TCmd *pCmd) sgbRecvCmd = message.bCmd; } + if (sgdwRecvOffset + wBytes > sizeof(sgRecvBuf)) { + Log("Received too many deltas, dropping player {}", player.getId()); + SNetDropPlayer(player.getId(), LEAVE_DROP); + return wBytes + sizeof(message); + } + assert(wOffset == sgdwRecvOffset); - memcpy(&sgRecvBuf[wOffset], &message + 1, wBytes); + memcpy(&sgRecvBuf[sgdwRecvOffset], &message + 1, wBytes); sgdwRecvOffset += wBytes; return wBytes + sizeof(message); } @@ -793,7 +902,9 @@ bool DeltaGetItem(const TCmdGItem &message, uint8_t bLevel) return true; } +#ifdef _DEBUG app_fatal("delta:1"); +#endif } if ((message.def.wCI & CF_PREGEN) == 0) @@ -839,9 +950,11 @@ void DeltaPutItem(const TCmdPItem &message, Point position, const Player &player && item.def.wIndx == message.def.wIndx && item.def.wCI == message.def.wCI && item.def.dwSeed == message.def.dwSeed) { - if (item.bCmd == TCmdPItem::DroppedItem) - return; - app_fatal(_("Trying to drop a floor item?")); + if (item.bCmd != TCmdPItem::DroppedItem) { + Log("Suspicious floor item duplication, dropping player {}", player.getId()); + SNetDropPlayer(player.getId(), LEAVE_DROP); + } + return; } } @@ -920,9 +1033,8 @@ void NetSendCmdExtra(const TCmdGItem &item) NetSendHiPri(MyPlayerId, (std::byte *)&cmd, sizeof(cmd)); } -size_t OnWalk(const TCmd *pCmd, Player &player) +size_t OnWalk(const TCmdLoc &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const Point position { message.x, message.y }; if (gbBufferMsgs != 1 && player.isOnActiveLevel() && InDungeonBounds(position)) { @@ -934,10 +1046,8 @@ size_t OnWalk(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnAddStrength(const TCmd *pCmd, Player &player) +size_t OnAddStrength(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) SendPacket(player, &message, sizeof(message)); else if (message.wParam1 <= 256) @@ -946,10 +1056,8 @@ size_t OnAddStrength(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnAddMagic(const TCmd *pCmd, Player &player) +size_t OnAddMagic(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) SendPacket(player, &message, sizeof(message)); else if (message.wParam1 <= 256) @@ -958,10 +1066,8 @@ size_t OnAddMagic(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnAddDexterity(const TCmd *pCmd, Player &player) +size_t OnAddDexterity(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) SendPacket(player, &message, sizeof(message)); else if (message.wParam1 <= 256) @@ -970,10 +1076,8 @@ size_t OnAddDexterity(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnAddVitality(const TCmd *pCmd, Player &player) +size_t OnAddVitality(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) SendPacket(player, &message, sizeof(message)); else if (message.wParam1 <= 256) @@ -982,9 +1086,8 @@ size_t OnAddVitality(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnGotoGetItem(const TCmd *pCmd, Player &player) +size_t OnGotoGetItem(const TCmdLocParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const Point position { message.x, message.y }; if (gbBufferMsgs != 1 && player.isOnActiveLevel() && InDungeonBounds(position) && SDL_SwapLE16(message.wParam1) < MAXITEMS + 1) { @@ -1152,10 +1255,8 @@ int SyncDropItem(const TCmdPItem &message) message.item); } -size_t OnRequestGetItem(const TCmd *pCmd, Player &player) +size_t OnRequestGetItem(const TCmdGItem &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs != 1 && player.isLevelOwnedByLocalClient() && IsGItemValid(message)) { const Point position { message.x, message.y }; const uint32_t dwSeed = SDL_SwapLE32(message.def.dwSeed); @@ -1194,10 +1295,8 @@ size_t OnRequestGetItem(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnGetItem(const TCmd *pCmd, Player &player) +size_t OnGetItem(const TCmdGItem &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) { SendPacket(player, &message, sizeof(message)); } else if (IsGItemValid(message)) { @@ -1229,9 +1328,8 @@ size_t OnGetItem(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnGotoAutoGetItem(const TCmd *pCmd, Player &player) +size_t OnGotoAutoGetItem(const TCmdLocParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const Point position { message.x, message.y }; const uint16_t itemIdx = SDL_SwapLE16(message.wParam1); @@ -1244,10 +1342,8 @@ size_t OnGotoAutoGetItem(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnRequestAutoGetItem(const TCmd *pCmd, Player &player) +size_t OnRequestAutoGetItem(const TCmdGItem &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs != 1 && player.isLevelOwnedByLocalClient() && IsGItemValid(message)) { const Point position { message.x, message.y }; const uint32_t dwSeed = SDL_SwapLE32(message.def.dwSeed); @@ -1270,10 +1366,8 @@ size_t OnRequestAutoGetItem(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnAutoGetItem(const TCmd *pCmd, Player &player) +size_t OnAutoGetItem(const TCmdGItem &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) { SendPacket(player, &message, sizeof(message)); } else if (IsGItemValid(message)) { @@ -1301,10 +1395,8 @@ size_t OnAutoGetItem(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnItemExtra(const TCmd *pCmd, Player &player) +size_t OnItemExtra(const TCmdGItem &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) { SendPacket(player, &message, sizeof(message)); } else if (IsGItemValid(message)) { @@ -1318,10 +1410,8 @@ size_t OnItemExtra(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnPutItem(const TCmd *pCmd, Player &player) +size_t OnPutItem(const TCmdPItem &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) { SendPacket(player, &message, sizeof(message)); } else if (IsPItemValid(message, player)) { @@ -1358,10 +1448,8 @@ size_t OnPutItem(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnSyncPutItem(const TCmd *pCmd, Player &player) +size_t OnSyncPutItem(const TCmdPItem &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) SendPacket(player, &message, sizeof(message)); else if (IsPItemValid(message, player)) { @@ -1388,9 +1476,8 @@ size_t OnSyncPutItem(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnAttackTile(const TCmd *pCmd, Player &player) +size_t OnAttackTile(const TCmdLoc &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const Point position { message.x, message.y }; if (gbBufferMsgs != 1 && player.isOnActiveLevel() && InDungeonBounds(position)) { @@ -1403,9 +1490,8 @@ size_t OnAttackTile(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnStandingAttackTile(const TCmd *pCmd, Player &player) +size_t OnStandingAttackTile(const TCmdLoc &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const Point position { message.x, message.y }; if (gbBufferMsgs != 1 && player.isOnActiveLevel() && InDungeonBounds(position)) { @@ -1418,9 +1504,8 @@ size_t OnStandingAttackTile(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnRangedAttackTile(const TCmd *pCmd, Player &player) +size_t OnRangedAttackTile(const TCmdLoc &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const Point position { message.x, message.y }; if (gbBufferMsgs != 1 && player.isOnActiveLevel() && InDungeonBounds(position)) { @@ -1467,9 +1552,8 @@ bool InitNewSpell(Player &player, uint16_t wParamSpellID, uint16_t wParamSpellTy return true; } -size_t OnSpellWall(const TCmd *pCmd, Player &player) +size_t OnSpellWall(const TCmdLocParam4 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const Point position { message.x, message.y }; if (gbBufferMsgs == 1) @@ -1495,9 +1579,8 @@ size_t OnSpellWall(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnSpellTile(const TCmd *pCmd, Player &player) +size_t OnSpellTile(const TCmdLocParam3 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const Point position { message.x, message.y }; if (gbBufferMsgs == 1) @@ -1519,9 +1602,8 @@ size_t OnSpellTile(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnObjectTileAction(const TCmd &cmd, Player &player, action_id action, bool pathToObject = true) +size_t OnObjectTileAction(const TCmdLoc &message, Player &player, action_id action, bool pathToObject = true) { - const auto &message = reinterpret_cast(cmd); const Point position { message.x, message.y }; const Object *object = FindObjectAtPosition(position); @@ -1536,9 +1618,22 @@ size_t OnObjectTileAction(const TCmd &cmd, Player &player, action_id action, boo return sizeof(message); } -size_t OnAttackMonster(const TCmd *pCmd, Player &player) +size_t OnObjectTileAction(const TCmdLoc &message, Player &player) +{ + switch (message.bCmd) { + case CMD_OPOBJXY: + return OnObjectTileAction(message, player, ACTION_OPERATE); + case CMD_DISARMXY: + return OnObjectTileAction(message, player, ACTION_DISARM); + case CMD_OPOBJT: + return OnObjectTileAction(message, player, ACTION_OPERATETK, false); + default: + return sizeof(message); + } +} + +size_t OnAttackMonster(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t monsterIdx = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs != 1 && player.isOnActiveLevel() && monsterIdx < MaxMonsters) { @@ -1552,9 +1647,8 @@ size_t OnAttackMonster(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnAttackPlayer(const TCmd *pCmd, Player &player) +size_t OnAttackPlayer(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t playerIdx = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs != 1 && player.isOnActiveLevel() && playerIdx < Players.size()) { @@ -1566,9 +1660,8 @@ size_t OnAttackPlayer(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnRangedAttackMonster(const TCmd *pCmd, Player &player) +size_t OnRangedAttackMonster(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t monsterIdx = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs != 1 && player.isOnActiveLevel() && monsterIdx < MaxMonsters) { @@ -1580,9 +1673,8 @@ size_t OnRangedAttackMonster(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnRangedAttackPlayer(const TCmd *pCmd, Player &player) +size_t OnRangedAttackPlayer(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t playerIdx = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs != 1 && player.isOnActiveLevel() && playerIdx < Players.size()) { @@ -1594,10 +1686,8 @@ size_t OnRangedAttackPlayer(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnSpellMonster(const TCmd *pCmd, Player &player) +size_t OnSpellMonster(const TCmdParam4 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) return sizeof(message); if (!player.isOnActiveLevel()) @@ -1617,10 +1707,8 @@ size_t OnSpellMonster(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnSpellPlayer(const TCmd *pCmd, Player &player) +size_t OnSpellPlayer(const TCmdParam4 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) return sizeof(message); if (!player.isOnActiveLevel()) @@ -1640,9 +1728,8 @@ size_t OnSpellPlayer(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnKnockback(const TCmd *pCmd, Player &player) +size_t OnKnockback(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t monsterIdx = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs != 1 && player.isOnActiveLevel() && monsterIdx < MaxMonsters) { @@ -1654,9 +1741,8 @@ size_t OnKnockback(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnResurrect(const TCmd *pCmd, Player &player) +size_t OnResurrect(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t playerIdx = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs == 1) { @@ -1670,9 +1756,8 @@ size_t OnResurrect(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnHealOther(const TCmd *pCmd, const Player &caster) +size_t OnHealOther(const TCmdParam1 &message, const Player &caster) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t playerIdx = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs != 1) { @@ -1684,9 +1769,8 @@ size_t OnHealOther(const TCmd *pCmd, const Player &caster) return sizeof(message); } -size_t OnTalkXY(const TCmd *pCmd, Player &player) +size_t OnTalkXY(const TCmdLocParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const Point position { message.x, message.y }; const uint16_t townerIdx = SDL_SwapLE16(message.wParam1); @@ -1699,9 +1783,8 @@ size_t OnTalkXY(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnNewLevel(const TCmd *pCmd, Player &player) +size_t OnNewLevel(const TCmdParam2 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t eventIdx = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs == 1) { @@ -1723,9 +1806,8 @@ size_t OnNewLevel(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnWarp(const TCmd *pCmd, Player &player) +size_t OnWarp(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t portalIdx = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs == 1) { @@ -1737,9 +1819,8 @@ size_t OnWarp(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnMonstDeath(const TCmd *pCmd, Player &player) +size_t OnMonstDeath(const TCmdLocParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const Point position { message.x, message.y }; const uint16_t monsterIdx = SDL_SwapLE16(message.wParam1); @@ -1757,23 +1838,21 @@ size_t OnMonstDeath(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnRequestSpawnGolem(const TCmd *pCmd, const Player &player) +size_t OnRequestSpawnGolem(const TCmdLocParam1 &message, const Player &player) { - const auto &message = *reinterpret_cast(pCmd); if (gbBufferMsgs == 1) return sizeof(message); const WorldTilePosition position { message.x, message.y }; - if (player.isLevelOwnedByLocalClient() && InDungeonBounds(position)) + if (player.plrlevel > 0 && player.isLevelOwnedByLocalClient() && InDungeonBounds(position)) SpawnGolem(player, position, static_cast(message.wParam1)); return sizeof(message); } -size_t OnMonstDamage(const TCmd *pCmd, Player &player) +size_t OnMonstDamage(const TCmdMonDamage &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t monsterIdx = SDL_SwapLE16(message.wMon); if (gbBufferMsgs != 1) { @@ -1796,9 +1875,8 @@ size_t OnMonstDamage(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnPlayerDeath(const TCmd *pCmd, Player &player) +size_t OnPlayerDeath(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const DeathReason deathReason = static_cast(SDL_SwapLE16(message.wParam1)); if (gbBufferMsgs != 1) { @@ -1813,9 +1891,8 @@ size_t OnPlayerDeath(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnPlayerDamage(const TCmd *pCmd, Player &player) +size_t OnPlayerDamage(const TCmdDamage &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint32_t damage = SDL_SwapLE32(message.dwDam); Player &target = Players[message.bPlr]; @@ -1828,10 +1905,8 @@ size_t OnPlayerDamage(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnOperateObject(const TCmd &pCmd, Player &player) +size_t OnOperateObject(const TCmdLoc &message, Player &player) { - const auto &message = reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) { SendPacket(player, &message, sizeof(message)); } else { @@ -1848,10 +1923,8 @@ size_t OnOperateObject(const TCmd &pCmd, Player &player) return sizeof(message); } -size_t OnBreakObject(const TCmd &pCmd, Player &player) +size_t OnBreakObject(const TCmdLoc &message, Player &player) { - const auto &message = reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) { SendPacket(player, &message, sizeof(message)); } else { @@ -1868,10 +1941,8 @@ size_t OnBreakObject(const TCmd &pCmd, Player &player) return sizeof(message); } -size_t OnChangePlayerItems(const TCmd *pCmd, Player &player) +size_t OnChangePlayerItems(const TCmdChItem &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (message.bLoc >= NUM_INVLOC) return sizeof(message); @@ -1891,10 +1962,8 @@ size_t OnChangePlayerItems(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnDeletePlayerItems(const TCmd *pCmd, Player &player) +size_t OnDeletePlayerItems(const TCmdDelItem &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs != 1) { if (&player != MyPlayer && message.bLoc < NUM_INVLOC) inv_update_rem_item(player, static_cast(message.bLoc)); @@ -1905,10 +1974,8 @@ size_t OnDeletePlayerItems(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnChangeInventoryItems(const TCmd *pCmd, Player &player) +size_t OnChangeInventoryItems(const TCmdChItem &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (message.bLoc >= InventoryGridCells) return sizeof(message); @@ -1923,9 +1990,8 @@ size_t OnChangeInventoryItems(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnDeleteInventoryItems(const TCmd *pCmd, Player &player) +size_t OnDeleteInventoryItems(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t invGridIndex = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs == 1) { @@ -1937,10 +2003,8 @@ size_t OnDeleteInventoryItems(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnChangeBeltItems(const TCmd *pCmd, Player &player) +size_t OnChangeBeltItems(const TCmdChItem &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (message.bLoc >= MaxBeltItems) return sizeof(message); @@ -1955,9 +2019,8 @@ size_t OnChangeBeltItems(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnDeleteBeltItems(const TCmd *pCmd, Player &player) +size_t OnDeleteBeltItems(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t spdBarIndex = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs == 1) { @@ -1969,9 +2032,8 @@ size_t OnDeleteBeltItems(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnPlayerLevel(const TCmd *pCmd, Player &player) +size_t OnPlayerLevel(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t playerLevel = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs != 1) { @@ -1984,10 +2046,8 @@ size_t OnPlayerLevel(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnDropItem(const TCmd *pCmd, Player &player) +size_t OnDropItem(const TCmdPItem &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) { SendPacket(player, &message, sizeof(message)); } else if (IsPItemValid(message, player)) { @@ -1997,10 +2057,8 @@ size_t OnDropItem(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnSpawnItem(const TCmd *pCmd, Player &player) +size_t OnSpawnItem(const TCmdPItem &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) { SendPacket(player, &message, sizeof(message)); } else if (IsPItemValid(message, player)) { @@ -2014,22 +2072,23 @@ size_t OnSpawnItem(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnSendPlayerInfo(const TCmd *pCmd, Player &player) +size_t OnSendPlayerInfo(const TCmdPlrInfoHdr &header, size_t maxCmdSize, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - const uint16_t wBytes = SDL_SwapLE16(message.wBytes); + const uint16_t wBytes = SDL_SwapLE16(header.wBytes); + + if (!ValidateCmdSize(wBytes + sizeof(header), maxCmdSize, player.getId())) + return maxCmdSize; if (gbBufferMsgs == 1) - SendPacket(player, &message, wBytes + sizeof(message)); + SendPacket(player, &header, wBytes + sizeof(header)); else - recv_plrinfo(player, message, message.bCmd == CMD_ACK_PLRINFO); + recv_plrinfo(player, header, header.bCmd == CMD_ACK_PLRINFO); - return wBytes + sizeof(message); + return wBytes + sizeof(header); } -size_t OnPlayerJoinLevel(const TCmd *pCmd, Player &player) +size_t OnPlayerJoinLevel(const TCmdLocParam2 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const Point position { message.x, message.y }; if (gbBufferMsgs == 1) { @@ -2078,9 +2137,8 @@ size_t OnPlayerJoinLevel(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnActivatePortal(const TCmd *pCmd, Player &player) +size_t OnActivatePortal(const TCmdLocParam3 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const Point position { message.x, message.y }; const uint8_t level = static_cast(SDL_SwapLE16(message.wParam1)); const uint16_t dungeonTypeIdx = SDL_SwapLE16(message.wParam2); @@ -2116,10 +2174,10 @@ size_t OnActivatePortal(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnDeactivatePortal(const TCmd *pCmd, Player &player) +size_t OnDeactivatePortal(const TCmd &cmd, Player &player) { if (gbBufferMsgs == 1) { - SendPacket(player, pCmd, sizeof(*pCmd)); + SendPacket(player, &cmd, sizeof(cmd)); } else { if (PortalOnLevel(player)) RemovePortalMissile(player); @@ -2127,13 +2185,13 @@ size_t OnDeactivatePortal(const TCmd *pCmd, Player &player) delta_close_portal(player); } - return sizeof(*pCmd); + return sizeof(cmd); } -size_t OnRestartTown(const TCmd *pCmd, Player &player) +size_t OnRestartTown(const TCmd &cmd, Player &player) { if (gbBufferMsgs == 1) { - SendPacket(player, pCmd, sizeof(*pCmd)); + SendPacket(player, &cmd, sizeof(cmd)); } else { if (&player == MyPlayer) { MyPlayerIsDead = false; @@ -2142,12 +2200,11 @@ size_t OnRestartTown(const TCmd *pCmd, Player &player) RestartTownLvl(player); } - return sizeof(*pCmd); + return sizeof(cmd); } -size_t OnSetStrength(const TCmd *pCmd, Player &player) +size_t OnSetStrength(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t value = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs != 1) { @@ -2160,9 +2217,8 @@ size_t OnSetStrength(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnSetDexterity(const TCmd *pCmd, Player &player) +size_t OnSetDexterity(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t value = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs != 1) { @@ -2175,9 +2231,8 @@ size_t OnSetDexterity(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnSetMagic(const TCmd *pCmd, Player &player) +size_t OnSetMagic(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t value = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs != 1) { @@ -2190,9 +2245,8 @@ size_t OnSetMagic(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnSetVitality(const TCmd *pCmd, Player &player) +size_t OnSetVitality(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); const uint16_t value = SDL_SwapLE16(message.wParam1); if (gbBufferMsgs != 1) { @@ -2205,43 +2259,46 @@ size_t OnSetVitality(const TCmd *pCmd, Player &player) return sizeof(message); } -size_t OnString(const TCmd *pCmd, Player &player) +size_t OnString(const TCmd &cmd, size_t maxCmdSize, Player &player) { - auto *p = (TCmdString *)pCmd; + const auto &message = reinterpret_cast(cmd); + const size_t headerSize = sizeof(message) - sizeof(message.str); + const size_t maxLength = std::min(MAX_SEND_STR_LEN, maxCmdSize - headerSize); + std::string_view str { message.str, maxLength }; + const auto tokens = SplitByChar(str, '\0'); + std::string_view playerMessage = *tokens.begin(); - size_t len = strlen(p->str); if (gbBufferMsgs == 0) - SendPlrMsg(player, p->str); + SendPlrMsg(player, playerMessage); - return len + 2; // length of string + nul terminator + sizeof(p->bCmd) + const size_t nullSize = str.size() != playerMessage.size() ? 1 : 0; + return headerSize + playerMessage.size() + nullSize; } -size_t OnFriendlyMode(const TCmd *pCmd, Player &player) // NOLINT(misc-unused-parameters) +size_t OnFriendlyMode(const TCmd &cmd, Player &player) // NOLINT(misc-unused-parameters) { player.friendlyMode = !player.friendlyMode; RedrawEverything(); - return sizeof(*pCmd); + return sizeof(cmd); } -size_t OnSyncQuest(const TCmd *pCmd, Player &player) +size_t OnSyncQuest(const TCmdQuest &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs == 1) { SendPacket(player, &message, sizeof(message)); } else { if (&player != MyPlayer && message.q < MAXQUESTS && message.qstate <= QUEST_HIVE_DONE) - SetMultiQuest(message.q, message.qstate, message.qlog != 0, message.qvar1, message.qvar2, message.qmsg); + SetMultiQuest(message.q, message.qstate, message.qlog != 0, message.qvar1, message.qvar2, SDL_SwapLE16(message.qmsg)); } return sizeof(message); } -size_t OnCheatExperience(const TCmd *pCmd, Player &player) // NOLINT(misc-unused-parameters) +size_t OnCheatExperience(const TCmd &cmd, Player &player) // NOLINT(misc-unused-parameters) { #ifdef _DEBUG if (gbBufferMsgs == 1) - SendPacket(player, pCmd, sizeof(*pCmd)); + SendPacket(player, &cmd, sizeof(cmd)); else if (!player.isMaxCharacterLevel()) { player._pExperience = player.getNextExperienceThreshold(); if (*GetOptions().Gameplay.experienceBar) { @@ -2250,17 +2307,16 @@ size_t OnCheatExperience(const TCmd *pCmd, Player &player) // NOLINT(misc-unused NextPlrLevel(player); } #endif - return sizeof(*pCmd); + return sizeof(cmd); } -size_t OnChangeSpellLevel(const TCmd *pCmd, Player &player) // NOLINT(misc-unused-parameters) +size_t OnChangeSpellLevel(const TCmdParam2 &message, Player &player) // NOLINT(misc-unused-parameters) { - const auto &message = *reinterpret_cast(pCmd); const SpellID spellID = static_cast(SDL_SwapLE16(message.wParam1)); const uint8_t spellLevel = std::min(static_cast(SDL_SwapLE16(message.wParam2)), MaxSpellLevel); if (gbBufferMsgs == 1) { - SendPacket(player, pCmd, sizeof(*pCmd)); + SendPacket(player, &message, sizeof(message)); } else { player._pMemSpells |= GetSpellBitmask(spellID); player._pSplLvl[static_cast(spellID)] = spellLevel; @@ -2269,38 +2325,36 @@ size_t OnChangeSpellLevel(const TCmd *pCmd, Player &player) // NOLINT(misc-unuse return sizeof(message); } -size_t OnDebug(const TCmd *pCmd) +size_t OnDebug(const TCmd &pCmd) { - return sizeof(*pCmd); + return sizeof(pCmd); } -size_t OnSetShield(const TCmd *pCmd, Player &player) +size_t OnSetShield(const TCmd &cmd, Player &player) { if (gbBufferMsgs != 1) player.pManaShield = true; - return sizeof(*pCmd); + return sizeof(cmd); } -size_t OnRemoveShield(const TCmd *pCmd, Player &player) +size_t OnRemoveShield(const TCmd &cmd, Player &player) { if (gbBufferMsgs != 1) player.pManaShield = false; - return sizeof(*pCmd); + return sizeof(cmd); } -size_t OnSetReflect(const TCmd *pCmd, Player &player) +size_t OnSetReflect(const TCmdParam1 &message, Player &player) { - const auto &message = *reinterpret_cast(pCmd); - if (gbBufferMsgs != 1) player.wReflections = SDL_SwapLE16(message.wParam1); return sizeof(message); } -size_t OnNakrul(const TCmd *pCmd) +size_t OnNakrul(const TCmd &cmd) { if (gbBufferMsgs != 1) { if (currlevel == 24) { @@ -2311,10 +2365,10 @@ size_t OnNakrul(const TCmd *pCmd) Quests[Q_NAKRUL]._qactive = QUEST_DONE; WeakenNaKrul(); } - return sizeof(*pCmd); + return sizeof(cmd); } -size_t OnOpenHive(const TCmd *pCmd, Player &player) +size_t OnOpenHive(const TCmd &cmd, Player &player) { if (gbBufferMsgs != 1) { AddMissile({ 0, 0 }, { 0, 0 }, Direction::South, MissileID::OpenNest, TARGET_MONSTERS, player, 0, 0); @@ -2322,10 +2376,10 @@ size_t OnOpenHive(const TCmd *pCmd, Player &player) InitTownTriggers(); } - return sizeof(*pCmd); + return sizeof(cmd); } -size_t OnOpenGrave(const TCmd *pCmd) +size_t OnOpenGrave(const TCmd &cmd) { if (gbBufferMsgs != 1) { TownOpenGrave(); @@ -2333,14 +2387,15 @@ size_t OnOpenGrave(const TCmd *pCmd) if (leveltype == DTYPE_TOWN) PlaySFX(SfxID::Sarcophagus); } - return sizeof(*pCmd); + return sizeof(cmd); } -size_t OnSpawnMonster(const TCmd *pCmd, const Player &player) +size_t OnSpawnMonster(const TCmdSpawnMonster &message, const Player &player) { - const auto &message = *reinterpret_cast(pCmd); if (gbBufferMsgs == 1) return sizeof(message); + if (player.plrlevel == 0) + return sizeof(message); const WorldTilePosition position { message.x, message.y }; @@ -2367,6 +2422,46 @@ size_t OnSpawnMonster(const TCmd *pCmd, const Player &player) return sizeof(message); } +template +size_t HandleCmd(size_t (*handler)(const TCmdImpl &, size_t, Player &), Player &player, const TCmd *pCmd, size_t maxCmdSize) +{ + if (!ValidateCmdSize(sizeof(TCmdImpl), maxCmdSize, player.getId())) + return maxCmdSize; + + const TCmdImpl *message = reinterpret_cast(pCmd); + return handler(*message, maxCmdSize, player); +} + +template +size_t HandleCmd(size_t (*handler)(const TCmdImpl &, size_t, const Player &), const Player &player, const TCmd *pCmd, size_t maxCmdSize) +{ + if (!ValidateCmdSize(sizeof(TCmdImpl), maxCmdSize, player.getId())) + return maxCmdSize; + + const TCmdImpl *message = reinterpret_cast(pCmd); + return handler(*message, maxCmdSize, player); +} + +template +size_t HandleCmd(size_t (*handler)(const TCmdImpl &, Player &), Player &player, const TCmd *pCmd, size_t maxCmdSize) +{ + if (!ValidateCmdSize(sizeof(TCmdImpl), maxCmdSize, player.getId())) + return maxCmdSize; + + const TCmdImpl *message = reinterpret_cast(pCmd); + return handler(*message, player); +} + +template +size_t HandleCmd(size_t (*handler)(const TCmdImpl &, const Player &), const Player &player, const TCmd *pCmd, size_t maxCmdSize) +{ + if (!ValidateCmdSize(sizeof(TCmdImpl), maxCmdSize, player.getId())) + return maxCmdSize; + + const TCmdImpl *message = reinterpret_cast(pCmd); + return handler(*message, player); +} + } // namespace void PrepareItemForNetwork(const Item &item, TItem &messageItem) @@ -2467,14 +2562,14 @@ void run_delta_info() void DeltaExportData(uint8_t pnum) { for (const auto &[levelNum, deltaLevel] : DeltaLevels) { - const size_t bufferSize = 1U /* marker byte, always 0 */ - + sizeof(uint8_t) /* level id */ - + sizeof(deltaLevel.item) /* items spawned during dungeon generation which have been picked up, and items dropped by a player during a game */ - + sizeof(uint8_t) /* count of object interactions which caused a state change since dungeon generation */ - + (sizeof(WorldTilePosition) + sizeof(DObjectStr)) * deltaLevel.object.size() /* location/action pairs for the object interactions */ - + sizeof(deltaLevel.monster) /* latest monster state */ - + sizeof(uint16_t) /* spanwned monster count */ - + (sizeof(uint16_t) + sizeof(DSpawnedMonster)) * MaxMonsters; /* spanwned monsters */ + const size_t bufferSize = 1U /* marker byte, always 0 */ + + sizeof(uint8_t) /* level id */ + + sizeof(deltaLevel.item) /* items spawned during dungeon generation which have been picked up, and items dropped by a player during a game */ + + sizeof(uint8_t) /* count of object interactions which caused a state change since dungeon generation */ + + (sizeof(WorldTilePosition) + sizeof(DObjectStr)) * deltaLevel.object.size() /* location/action pairs for the object interactions */ + + sizeof(deltaLevel.monster) /* latest monster state */ + + sizeof(uint16_t) /* spawned monster count */ + + (sizeof(uint16_t) + sizeof(DSpawnedMonster)) * deltaLevel.spawnedMonsters.size(); /* spawned monsters */ std::unique_ptr dst { new std::byte[bufferSize] }; std::byte *dstEnd = &dst.get()[1]; @@ -2511,6 +2606,11 @@ void DeltaClearLevel(uint8_t level) LocalLevels.erase(level); } +bool DeltaMonsterIsValid(const DMonsterStr &monster) +{ + return InDungeonBounds(monster.position) && monster.hitPoints >= 0; +} + void delta_kill_monster(const Monster &monster, Point position, const Player &player) { if (!gbIsMultiplayer) @@ -2527,8 +2627,8 @@ void delta_monster_hp(const Monster &monster, const Player &player) return; DMonsterStr *pD = &GetDeltaLevel(player).monster[monster.getId()]; - if (pD->hitPoints > monster.hitPoints) - pD->hitPoints = monster.hitPoints; + if (SDL_SwapLE32(pD->hitPoints) > monster.hitPoints) + pD->hitPoints = SDL_SwapLE32(monster.hitPoints); } void delta_sync_monster(const TSyncMonster &monsterSync, uint8_t level) @@ -2546,7 +2646,7 @@ void delta_sync_monster(const TSyncMonster &monsterSync, uint8_t level) monster.position.y = monsterSync._my; monster._mactive = UINT8_MAX; monster._menemy = monsterSync._menemy; - monster.hitPoints = SDL_SwapLE32(monsterSync._mhitpoints); + monster.hitPoints = monsterSync._mhitpoints; monster.mWhoHit = monsterSync.mWhoHit; } @@ -2576,7 +2676,7 @@ void DeltaSyncJunk() quest._qactive = sgJunk.quests[q].qstate; quest._qvar1 = sgJunk.quests[q].qvar1; quest._qvar2 = sgJunk.quests[q].qvar2; - quest._qmsg = static_cast<_speech_id>(sgJunk.quests[q].qmsg); + quest._qmsg = static_cast<_speech_id>(SDL_SwapLE16(sgJunk.quests[q].qmsg)); } q++; } @@ -2662,25 +2762,26 @@ void DeltaLoadLevel() LoadDeltaSpawnedMonster(deltaSpawnedMonster.second.typeIndex, deltaSpawnedMonster.first, monsterData.seed, monsterData.golemOwnerPlayerId, monsterData.golemSpellLevel); assert(deltaLevel.monster[deltaSpawnedMonster.first].position.x != 0xFF); } + for (size_t i = 0; i < MaxMonsters; i++) { - if (deltaLevel.monster[i].position.x == 0xFF) + DMonsterStr &deltaMonster = deltaLevel.monster[i]; + if (!DeltaMonsterIsValid(deltaMonster)) continue; Monster &monster = Monsters[i]; M_ClearSquares(monster); { - const WorldTilePosition position = deltaLevel.monster[i].position; + const WorldTilePosition position = deltaMonster.position; monster.position.tile = position; monster.position.old = position; monster.position.future = position; if (monster.lightId != NO_LIGHT) ChangeLightXY(monster.lightId, position); } - if (deltaLevel.monster[i].hitPoints != -1) { - monster.hitPoints = deltaLevel.monster[i].hitPoints; - monster.whoHit = deltaLevel.monster[i].mWhoHit; - } - if (deltaLevel.monster[i].hitPoints == 0) { + + monster.hitPoints = SDL_SwapLE32(deltaMonster.hitPoints); + monster.whoHit = deltaMonster.mWhoHit; + if (deltaMonster.hitPoints == 0) { M_ClearSquares(monster); if (monster.ai != MonsterAIID::Diablo) { if (monster.isUnique()) { @@ -2691,19 +2792,31 @@ void DeltaLoadLevel() } monster.isInvalid = true; M_UpdateRelations(monster); + } + } + + // Separate loop ensures that monster hitpoints are + // synced before attempting to validate enemy IDs + for (size_t i = 0; i < MaxMonsters; i++) { + DMonsterStr &deltaMonster = deltaLevel.monster[i]; + if (!DeltaMonsterIsValid(deltaMonster)) + continue; + if (deltaMonster.hitPoints == 0) + continue; + Monster &monster = Monsters[i]; + if (IsEnemyValid(i, deltaMonster._menemy)) + decode_enemy(monster, deltaMonster._menemy); + if (monster.position.tile != Point { 0, 0 } && monster.position.tile != GolemHoldingCell) + monster.occupyTile(monster.position.tile, false); + if (monster.type().type == MT_GOLEM) { + GolumAi(monster); + monster.flags |= (MFLAG_TARGETS_MONSTER | MFLAG_GOLEM); } else { - decode_enemy(monster, deltaLevel.monster[i]._menemy); - if (monster.position.tile != Point { 0, 0 } && monster.position.tile != GolemHoldingCell) - monster.occupyTile(monster.position.tile, false); - if (monster.type().type == MT_GOLEM) { - GolumAi(monster); - monster.flags |= (MFLAG_TARGETS_MONSTER | MFLAG_GOLEM); - } else { - M_StartStand(monster, monster.direction); - } - monster.activeForTicks = deltaLevel.monster[i]._mactive; + M_StartStand(monster, monster.direction); } + monster.activeForTicks = deltaMonster._mactive; } + auto localLevelIt = LocalLevels.find(localLevel); if (localLevelIt != LocalLevels.end()) memcpy(AutomapView, &localLevelIt->second, sizeof(AutomapView)); @@ -2963,7 +3076,7 @@ void NetSendCmdQuest(bool bHiPri, const Quest &quest) cmd.qlog = quest._qlog ? 1 : 0; cmd.qvar1 = quest._qvar1; cmd.qvar2 = quest._qvar2; - cmd.qmsg = quest._qmsg; + cmd.qmsg = SDL_SwapLE16(quest._qmsg); if (bHiPri) NetSendHiPri(MyPlayerId, (std::byte *)&cmd, sizeof(cmd)); @@ -3126,7 +3239,17 @@ void delta_close_portal(const Player &player) memset(&sgJunk.portal[player.getId()], 0xFF, sizeof(sgJunk.portal[player.getId()])); } -size_t ParseCmd(uint8_t pnum, const TCmd *pCmd) +bool ValidateCmdSize(size_t requiredCmdSize, size_t maxCmdSize, size_t playerId) +{ + if (requiredCmdSize <= maxCmdSize) + return true; + + Log("Suspiciously small packet size, dropping player {}", playerId); + SNetDropPlayer(playerId, LEAVE_DROP); + return false; +} + +size_t ParseCmd(uint8_t pnum, const TCmd *pCmd, size_t maxCmdSize) { sbLastCmd = pCmd->bCmd; if (sgwPackPlrOffsetTbl[pnum] != 0 && sbLastCmd != CMD_ACK_PLRINFO && sbLastCmd != CMD_SEND_PLRINFO) @@ -3140,164 +3263,163 @@ size_t ParseCmd(uint8_t pnum, const TCmd *pCmd) switch (pCmd->bCmd) { case CMD_SYNCDATA: - return OnSyncData(pCmd, player); + return HandleCmd(OnSyncData, player, pCmd, maxCmdSize); case CMD_WALKXY: - return OnWalk(pCmd, player); + return HandleCmd(OnWalk, player, pCmd, maxCmdSize); case CMD_ADDSTR: - return OnAddStrength(pCmd, player); + return HandleCmd(OnAddStrength, player, pCmd, maxCmdSize); case CMD_ADDDEX: - return OnAddDexterity(pCmd, player); + return HandleCmd(OnAddDexterity, player, pCmd, maxCmdSize); case CMD_ADDMAG: - return OnAddMagic(pCmd, player); + return HandleCmd(OnAddMagic, player, pCmd, maxCmdSize); case CMD_ADDVIT: - return OnAddVitality(pCmd, player); + return HandleCmd(OnAddVitality, player, pCmd, maxCmdSize); case CMD_GOTOGETITEM: - return OnGotoGetItem(pCmd, player); + return HandleCmd(OnGotoGetItem, player, pCmd, maxCmdSize); case CMD_REQUESTGITEM: - return OnRequestGetItem(pCmd, player); + return HandleCmd(OnRequestGetItem, player, pCmd, maxCmdSize); case CMD_GETITEM: - return OnGetItem(pCmd, player); + return HandleCmd(OnGetItem, player, pCmd, maxCmdSize); case CMD_GOTOAGETITEM: - return OnGotoAutoGetItem(pCmd, player); + return HandleCmd(OnGotoAutoGetItem, player, pCmd, maxCmdSize); case CMD_REQUESTAGITEM: - return OnRequestAutoGetItem(pCmd, player); + return HandleCmd(OnRequestAutoGetItem, player, pCmd, maxCmdSize); case CMD_AGETITEM: - return OnAutoGetItem(pCmd, player); + return HandleCmd(OnAutoGetItem, player, pCmd, maxCmdSize); case CMD_ITEMEXTRA: - return OnItemExtra(pCmd, player); + return HandleCmd(OnItemExtra, player, pCmd, maxCmdSize); case CMD_PUTITEM: - return OnPutItem(pCmd, player); + return HandleCmd(OnPutItem, player, pCmd, maxCmdSize); case CMD_SYNCPUTITEM: - return OnSyncPutItem(pCmd, player); + return HandleCmd(OnSyncPutItem, player, pCmd, maxCmdSize); case CMD_SPAWNITEM: - return OnSpawnItem(pCmd, player); + return HandleCmd(OnSpawnItem, player, pCmd, maxCmdSize); case CMD_ATTACKXY: - return OnAttackTile(pCmd, player); + return HandleCmd(OnAttackTile, player, pCmd, maxCmdSize); case CMD_SATTACKXY: - return OnStandingAttackTile(pCmd, player); + return HandleCmd(OnStandingAttackTile, player, pCmd, maxCmdSize); case CMD_RATTACKXY: - return OnRangedAttackTile(pCmd, player); + return HandleCmd(OnRangedAttackTile, player, pCmd, maxCmdSize); case CMD_SPELLXYD: - return OnSpellWall(pCmd, player); + return HandleCmd(OnSpellWall, player, pCmd, maxCmdSize); case CMD_SPELLXY: - return OnSpellTile(pCmd, player); + return HandleCmd(OnSpellTile, player, pCmd, maxCmdSize); case CMD_OPOBJXY: - return OnObjectTileAction(*pCmd, player, ACTION_OPERATE); case CMD_DISARMXY: - return OnObjectTileAction(*pCmd, player, ACTION_DISARM); case CMD_OPOBJT: - return OnObjectTileAction(*pCmd, player, ACTION_OPERATETK, false); + return HandleCmd(OnObjectTileAction, player, pCmd, maxCmdSize); case CMD_ATTACKID: - return OnAttackMonster(pCmd, player); + return HandleCmd(OnAttackMonster, player, pCmd, maxCmdSize); case CMD_ATTACKPID: - return OnAttackPlayer(pCmd, player); + return HandleCmd(OnAttackPlayer, player, pCmd, maxCmdSize); case CMD_RATTACKID: - return OnRangedAttackMonster(pCmd, player); + return HandleCmd(OnRangedAttackMonster, player, pCmd, maxCmdSize); case CMD_RATTACKPID: - return OnRangedAttackPlayer(pCmd, player); + return HandleCmd(OnRangedAttackPlayer, player, pCmd, maxCmdSize); case CMD_SPELLID: - return OnSpellMonster(pCmd, player); + return HandleCmd(OnSpellMonster, player, pCmd, maxCmdSize); case CMD_SPELLPID: - return OnSpellPlayer(pCmd, player); + return HandleCmd(OnSpellPlayer, player, pCmd, maxCmdSize); case CMD_KNOCKBACK: - return OnKnockback(pCmd, player); + return HandleCmd(OnKnockback, player, pCmd, maxCmdSize); case CMD_RESURRECT: - return OnResurrect(pCmd, player); + return HandleCmd(OnResurrect, player, pCmd, maxCmdSize); case CMD_HEALOTHER: - return OnHealOther(pCmd, player); + return HandleCmd(OnHealOther, player, pCmd, maxCmdSize); case CMD_TALKXY: - return OnTalkXY(pCmd, player); + return HandleCmd(OnTalkXY, player, pCmd, maxCmdSize); case CMD_DEBUG: - return OnDebug(pCmd); + return OnDebug(*pCmd); case CMD_NEWLVL: - return OnNewLevel(pCmd, player); + return HandleCmd(OnNewLevel, player, pCmd, maxCmdSize); case CMD_WARP: - return OnWarp(pCmd, player); + return HandleCmd(OnWarp, player, pCmd, maxCmdSize); case CMD_MONSTDEATH: - return OnMonstDeath(pCmd, player); + return HandleCmd(OnMonstDeath, player, pCmd, maxCmdSize); case CMD_REQUESTSPAWNGOLEM: - return OnRequestSpawnGolem(pCmd, player); + return HandleCmd(OnRequestSpawnGolem, player, pCmd, maxCmdSize); case CMD_MONSTDAMAGE: - return OnMonstDamage(pCmd, player); + return HandleCmd(OnMonstDamage, player, pCmd, maxCmdSize); case CMD_PLRDEAD: - return OnPlayerDeath(pCmd, player); + return HandleCmd(OnPlayerDeath, player, pCmd, maxCmdSize); case CMD_PLRDAMAGE: - return OnPlayerDamage(pCmd, player); + return HandleCmd(OnPlayerDamage, player, pCmd, maxCmdSize); case CMD_OPENDOOR: case CMD_CLOSEDOOR: case CMD_OPERATEOBJ: - return OnOperateObject(*pCmd, player); + return HandleCmd(OnOperateObject, player, pCmd, maxCmdSize); case CMD_BREAKOBJ: - return OnBreakObject(*pCmd, player); + return HandleCmd(OnBreakObject, player, pCmd, maxCmdSize); case CMD_CHANGEPLRITEMS: - return OnChangePlayerItems(pCmd, player); + return HandleCmd(OnChangePlayerItems, player, pCmd, maxCmdSize); case CMD_DELPLRITEMS: - return OnDeletePlayerItems(pCmd, player); + return HandleCmd(OnDeletePlayerItems, player, pCmd, maxCmdSize); case CMD_CHANGEINVITEMS: - return OnChangeInventoryItems(pCmd, player); + return HandleCmd(OnChangeInventoryItems, player, pCmd, maxCmdSize); case CMD_DELINVITEMS: - return OnDeleteInventoryItems(pCmd, player); + return HandleCmd(OnDeleteInventoryItems, player, pCmd, maxCmdSize); case CMD_CHANGEBELTITEMS: - return OnChangeBeltItems(pCmd, player); + return HandleCmd(OnChangeBeltItems, player, pCmd, maxCmdSize); case CMD_DELBELTITEMS: - return OnDeleteBeltItems(pCmd, player); + return HandleCmd(OnDeleteBeltItems, player, pCmd, maxCmdSize); case CMD_PLRLEVEL: - return OnPlayerLevel(pCmd, player); + return HandleCmd(OnPlayerLevel, player, pCmd, maxCmdSize); case CMD_DROPITEM: - return OnDropItem(pCmd, player); + return HandleCmd(OnDropItem, player, pCmd, maxCmdSize); case CMD_ACK_PLRINFO: case CMD_SEND_PLRINFO: - return OnSendPlayerInfo(pCmd, player); + return HandleCmd(OnSendPlayerInfo, player, pCmd, maxCmdSize); case CMD_PLAYER_JOINLEVEL: - return OnPlayerJoinLevel(pCmd, player); + return HandleCmd(OnPlayerJoinLevel, player, pCmd, maxCmdSize); case CMD_ACTIVATEPORTAL: - return OnActivatePortal(pCmd, player); + return HandleCmd(OnActivatePortal, player, pCmd, maxCmdSize); case CMD_DEACTIVATEPORTAL: - return OnDeactivatePortal(pCmd, player); + return OnDeactivatePortal(*pCmd, player); case CMD_RETOWN: - return OnRestartTown(pCmd, player); + return OnRestartTown(*pCmd, player); case CMD_SETSTR: - return OnSetStrength(pCmd, player); + return HandleCmd(OnSetStrength, player, pCmd, maxCmdSize); case CMD_SETMAG: - return OnSetMagic(pCmd, player); + return HandleCmd(OnSetMagic, player, pCmd, maxCmdSize); case CMD_SETDEX: - return OnSetDexterity(pCmd, player); + return HandleCmd(OnSetDexterity, player, pCmd, maxCmdSize); case CMD_SETVIT: - return OnSetVitality(pCmd, player); + return HandleCmd(OnSetVitality, player, pCmd, maxCmdSize); case CMD_STRING: - return OnString(pCmd, player); + return OnString(*pCmd, maxCmdSize, player); case CMD_FRIENDLYMODE: - return OnFriendlyMode(pCmd, player); + return OnFriendlyMode(*pCmd, player); case CMD_SYNCQUEST: - return OnSyncQuest(pCmd, player); + return HandleCmd(OnSyncQuest, player, pCmd, maxCmdSize); case CMD_CHEAT_EXPERIENCE: - return OnCheatExperience(pCmd, player); + return OnCheatExperience(*pCmd, player); case CMD_CHANGE_SPELL_LEVEL: - return OnChangeSpellLevel(pCmd, player); + return HandleCmd(OnChangeSpellLevel, player, pCmd, maxCmdSize); case CMD_SETSHIELD: - return OnSetShield(pCmd, player); + return OnSetShield(*pCmd, player); case CMD_REMSHIELD: - return OnRemoveShield(pCmd, player); + return OnRemoveShield(*pCmd, player); case CMD_SETREFLECT: - return OnSetReflect(pCmd, player); + return HandleCmd(OnSetReflect, player, pCmd, maxCmdSize); case CMD_NAKRUL: - return OnNakrul(pCmd); + return OnNakrul(*pCmd); case CMD_OPENHIVE: - return OnOpenHive(pCmd, player); + return OnOpenHive(*pCmd, player); case CMD_OPENGRAVE: - return OnOpenGrave(pCmd); + return OnOpenGrave(*pCmd); case CMD_SPAWNMONSTER: - return OnSpawnMonster(pCmd, player); + return HandleCmd(OnSpawnMonster, player, pCmd, maxCmdSize); default: break; } if (pCmd->bCmd < CMD_DLEVEL || pCmd->bCmd > CMD_DLEVEL_END) { + Log("Unrecognized network message {}, dropping player {}", static_cast(pCmd->bCmd), pnum); SNetDropPlayer(pnum, LEAVE_DROP); return 0; } - return OnLevelData(pnum, pCmd); + return HandleCmd(OnLevelData, player, pCmd, maxCmdSize); } } // namespace devilution diff --git a/Source/msg.h b/Source/msg.h index 53df874de..c61827e86 100644 --- a/Source/msg.h +++ b/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 diff --git a/Source/multi.cpp b/Source/multi.cpp index 7754fbeb2..7a5255dd0 100644 --- a/Source/multi.cpp +++ b/Source/multi.cpp @@ -3,6 +3,7 @@ * * Implementation of functions for keeping multiplaye games in sync. */ +#include "multi.h" #include #include @@ -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(&data[offset])); + size_t messageSize = ParseCmd(pnum, reinterpret_cast(&data[offset]), size - offset); if (messageSize == 0) { break; } diff --git a/Source/panels/spell_book.cpp b/Source/panels/spell_book.cpp index a640bd651..ed9b8f6fb 100644 --- a/Source/panels/spell_book.cpp +++ b/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) { diff --git a/Source/panels/spell_list.cpp b/Source/panels/spell_list.cpp index 217ca8089..6801edcff 100644 --- a/Source/panels/spell_list.cpp +++ b/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; } diff --git a/Source/portal.cpp b/Source/portal.cpp index 2990f2548..32bf4ea51 100644 --- a/Source/portal.cpp +++ b/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::Idle); if (leveltype != DTYPE_TOWN) missile->_mlid = AddLight(missile->position.tile, 15); diff --git a/Source/portals/validation.cpp b/Source/portals/validation.cpp new file mode 100644 index 000000000..0557fb2aa --- /dev/null +++ b/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 + +#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(ltype); + if (levelType == DTYPE_NONE) + return false; + if (isOnSetLevel) + return levelType == GetSetLevelType(static_cast<_setlevels>(level)); + return levelType == GetLevelType(level); +} + +} // namespace devilution diff --git a/Source/portals/validation.hpp b/Source/portals/validation.hpp new file mode 100644 index 000000000..c0e2c3c30 --- /dev/null +++ b/Source/portals/validation.hpp @@ -0,0 +1,16 @@ +/** + * @file portals/validation.hpp + * + * Interface of functions for validation of portal data. + */ +#pragma once + +#include + +#include "engine/world_tile.hpp" + +namespace devilution { + +bool IsPortalDeltaValid(WorldTilePosition location, uint8_t level, uint8_t levelType, bool isOnSetLevel); + +} // namespace devilution diff --git a/Source/quests/validation.cpp b/Source/quests/validation.cpp new file mode 100644 index 000000000..fff38ac01 --- /dev/null +++ b/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 + +#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(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 diff --git a/Source/quests/validation.hpp b/Source/quests/validation.hpp new file mode 100644 index 000000000..205458add --- /dev/null +++ b/Source/quests/validation.hpp @@ -0,0 +1,17 @@ +/** + * @file quests/validation.hpp + * + * Interface of functions for validation of quest data. + */ +#pragma once + +#include + +#include "objdat.h" +#include "quests.h" + +namespace devilution { + +bool IsQuestDeltaValid(quest_id qidx, quest_state qstate, uint8_t qlog, int16_t qmsg); + +} // namespace devilution diff --git a/Source/sync.cpp b/Source/sync.cpp index 199ed651e..51bcdd156 100644 --- a/Source/sync.cpp +++ b/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(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(pCmd + sizeof(header)); + const auto *monsterSyncs = reinterpret_cast(&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]); } diff --git a/Source/sync.h b/Source/sync.h index 7aacd41d6..09a882cb3 100644 --- a/Source/sync.h +++ b/Source/sync.h @@ -8,10 +8,13 @@ #include #include +#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 diff --git a/Source/textdat.cpp b/Source/textdat.cpp index c5d2fbefd..4275bf854 100644 --- a/Source/textdat.cpp +++ b/Source/textdat.cpp @@ -916,4 +916,7 @@ const Speech Speeches[] = { 1, 5, SfxID::Adria48 }, */ }; + +const size_t SpeechCount = sizeof(Speeches) / sizeof(Speech); + } // namespace devilution diff --git a/Source/textdat.h b/Source/textdat.h index c188b61df..1a8264cf2 100644 --- a/Source/textdat.h +++ b/Source/textdat.h @@ -5,6 +5,7 @@ */ #pragma once +#include #include #include "sound_effect_enums.h" @@ -430,6 +431,7 @@ struct Speech { SfxID sfxnr; }; +extern const size_t SpeechCount; extern const Speech Speeches[]; } // namespace devilution diff --git a/test/missiles_test.cpp b/test/missiles_test.cpp index 09b1c5904..f8b5f283d 100644 --- a/test/missiles_test.cpp +++ b/test/missiles_test.cpp @@ -29,9 +29,9 @@ void TestAnimatedMissileRotatesUniformly(Missile &missile, int startingDir, int { ankerl::unordered_dense::map 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);