diff --git a/Source/lua/modules/dev/monsters.cpp b/Source/lua/modules/dev/monsters.cpp index 1fc188db4..42b0ccd84 100644 --- a/Source/lua/modules/dev/monsters.cpp +++ b/Source/lua/modules/dev/monsters.cpp @@ -114,6 +114,7 @@ std::string DebugCmdSpawnMonster(std::string name, std::optional count } if (mtype == -1) return "Monster not found"; + if (!MyPlayer->isLevelOwnedByLocalClient()) return "You are not the level owner."; size_t id = MaxLvlMTypes - 1; bool found = false; @@ -137,28 +138,27 @@ std::string DebugCmdSpawnMonster(std::string name, std::optional count Player &myPlayer = *MyPlayer; - unsigned spawnedMonster = 0; + size_t monstersToSpawn = std::min(MaxMonsters - ActiveMonsterCount, count); + if (monstersToSpawn == 0) + return "Can't spawn any monsters"; - auto ret = Crawl(0, MaxCrawlRadius, [&](Displacement displacement) -> std::optional { + size_t spawnedMonster = 0; + Crawl(0, MaxCrawlRadius, [&](Displacement displacement) { Point pos = myPlayer.position.tile + displacement; if (dPlayer[pos.x][pos.y] != 0 || dMonster[pos.x][pos.y] != 0) - return {}; + return false; if (!IsTileWalkable(pos)) - return {}; + return false; - if (AddMonster(pos, myPlayer._pdir, id, true) == nullptr) - return StrCat("Spawned ", spawnedMonster, " monsters. (Unable to spawn more)"); + SpawnMonster(pos, myPlayer._pdir, id); spawnedMonster += 1; - if (spawnedMonster >= count) - return StrCat("Spawned ", spawnedMonster, " monsters."); - - return {}; + return spawnedMonster == monstersToSpawn; }); - if (!ret.has_value()) + if (monstersToSpawn != count) return StrCat("Spawned ", spawnedMonster, " monsters. (Unable to spawn more)"); - return *ret; + return StrCat("Spawned ", spawnedMonster, " monsters."); } } // namespace diff --git a/Source/monster.cpp b/Source/monster.cpp index 378268bc7..b17d8e226 100644 --- a/Source/monster.cpp +++ b/Source/monster.cpp @@ -3135,6 +3135,21 @@ MonsterSpritesData LoadMonsterSpritesData(const MonsterData &monsterData) return result; } +void EnsureMonsterIndexIsActive(size_t monsterId) +{ + assert(monsterId < MaxMonsters); + for (size_t index = 0; index < MaxMonsters; index++) { + if (ActiveMonsters[index] != monsterId) + continue; + if (index < ActiveMonsterCount) + return; // monster is already active + int oldId = ActiveMonsters[ActiveMonsterCount]; + ActiveMonsters[ActiveMonsterCount] = static_cast(monsterId); + ActiveMonsters[index] = oldId; + ActiveMonsterCount += 1; + } +} + } // namespace size_t AddMonsterType(_monster_id type, placeflag placeflag) @@ -3645,9 +3660,58 @@ Monster *AddMonster(Point position, Direction dir, size_t typeIndex, bool inMap) void SpawnMonster(Point position, Direction dir, size_t typeIndex, bool startSpecialStand /*= false*/) { - Monster *monster = AddMonster(position, dir, typeIndex, true); - if (startSpecialStand && monster != nullptr) - StartSpecialStand(*monster, dir); + if (ActiveMonsterCount >= MaxMonsters) + return; + + // The command is only executed for the level owner, to prevent desyncs in multiplayer. + if (!MyPlayer->isLevelOwnedByLocalClient()) + return; + + size_t monsterIndex = ActiveMonsters[ActiveMonsterCount]; + ActiveMonsterCount += 1; + uint32_t seed = GetLCGEngineState(); + // Update local state immediately to increase ActiveMonsterCount instantly (this allows multiple monsters to be spawned in one game tick) + InitializeSpawnedMonster(position, dir, typeIndex, monsterIndex, seed); + NetSendCmdSpawnMonster(position, dir, static_cast(typeIndex), static_cast(monsterIndex), seed); +} + +void LoadDeltaSpawnedMonster(size_t typeIndex, size_t monsterId, uint32_t seed) +{ + SetRndSeed(seed); + EnsureMonsterIndexIsActive(monsterId); + WorldTilePosition position = GolemHoldingCell; + Monster &monster = Monsters[monsterId]; + M_ClearSquares(monster); + InitMonster(monster, Direction::South, typeIndex, position); +} + +void InitializeSpawnedMonster(Point position, Direction dir, size_t typeIndex, size_t monsterId, uint32_t seed) +{ + SetRndSeed(seed); + EnsureMonsterIndexIsActive(monsterId); + Monster &monster = Monsters[monsterId]; + M_ClearSquares(monster); + + // When we receive a network message, the position we got for the new monster may already be occupied. + // That's why we check for the next free tile for the monster. + auto freePosition = Crawl(0, MaxCrawlRadius, [&](Displacement displacement) -> std::optional { + Point posToCheck = position + displacement; + if (IsTileAvailable(posToCheck)) + return posToCheck; + return {}; + }); + + assert(freePosition); + assert(!MyPlayer->isLevelOwnedByLocalClient() || (freePosition && position == *freePosition)); + position = freePosition.value_or(position); + + monster.occupyTile(position, false); + InitMonster(monster, dir, typeIndex, position); + + if (IsSkel(monster.type().type)) + StartSpecialStand(monster, dir); + else + M_StartStand(monster, dir); } void AddDoppelganger(Monster &monster) diff --git a/Source/monster.h b/Source/monster.h index bbb88c951..f857a48ab 100644 --- a/Source/monster.h +++ b/Source/monster.h @@ -495,7 +495,20 @@ void InitGolems(); void InitMonsters(); void SetMapMonsters(const uint16_t *dunData, Point startPosition); Monster *AddMonster(Point position, Direction dir, size_t mtype, bool inMap); +/** + * @brief Spawns a new monsters (dynamically/not on level load). + * The command is only executed for the level owner, to prevent desyncs in multiplayer. + * The level owner sends a CMD_SPAWNMONSTER-message to the other players. + */ void SpawnMonster(Point position, Direction dir, size_t typeIndex, bool startSpecialStand = false); +/** + * @brief Loads data for a dynamically spawned monster when entering a level in multiplayer. + */ +void LoadDeltaSpawnedMonster(size_t typeIndex, size_t monsterId, uint32_t seed); +/** + * @brief Initialize a spanwed monster (from a network message or from SpawnMonster-function). + */ +void InitializeSpawnedMonster(Point position, Direction dir, size_t typeIndex, size_t monsterId, uint32_t seed); void AddDoppelganger(Monster &monster); void ApplyMonsterDamage(DamageType damageType, Monster &monster, int damage); bool M_Talker(const Monster &monster); diff --git a/Source/msg.cpp b/Source/msg.cpp index 26ad26761..c8dc860c2 100644 --- a/Source/msg.cpp +++ b/Source/msg.cpp @@ -177,6 +177,7 @@ std::string_view CmdIdString(_cmd_id cmd) case CMD_NAKRUL: return "CMD_NAKRUL"; case CMD_OPENHIVE: return "CMD_OPENHIVE"; case CMD_OPENGRAVE: return "CMD_OPENGRAVE"; + case CMD_SPAWNMONSTER: return "CMD_SPAWNMONSTER"; case FAKE_CMD_SETID: return "FAKE_CMD_SETID"; case FAKE_CMD_DROPID: return "FAKE_CMD_DROPID"; case CMD_INVALID: return "CMD_INVALID"; @@ -210,9 +211,15 @@ struct DObjectStr { _cmd_id bCmd; }; +struct DSpawnedMonster { + size_t typeIndex; + uint32_t seed; +}; + struct DLevel { TCmdPItem item[MAXITEMS]; std::unordered_map object; + std::unordered_map spawnedMonsters; DMonsterStr monster[MaxMonsters]; }; @@ -539,7 +546,7 @@ std::byte *DeltaExportMonster(std::byte *dst, const DMonsterStr *src) return dst; } -void DeltaImportMonster(const std::byte *src, DMonsterStr *dst) +size_t DeltaImportMonster(const std::byte *src, DMonsterStr *dst) { size_t size = 0; for (size_t i = 0; i < MaxMonsters; i++, dst++) { @@ -551,6 +558,43 @@ void DeltaImportMonster(const std::byte *src, DMonsterStr *dst) size += sizeof(DMonsterStr); } } + + return size; +} + +std::byte *DeltaExportSpawnedMonsters(std::byte *dst, const std::unordered_map &spawnedMonsters) +{ + auto &size = *reinterpret_cast(dst); + size = static_cast(spawnedMonsters.size()); + dst += sizeof(uint16_t); + + for (const auto &deltaSpawnedMonster : spawnedMonsters) { + auto &monsterId = *reinterpret_cast(dst); + monsterId = static_cast(deltaSpawnedMonster.first); + dst += sizeof(uint16_t); + + memcpy(dst, &deltaSpawnedMonster.second, sizeof(DSpawnedMonster)); + dst += sizeof(DSpawnedMonster); + } + + return dst; +} + +const std::byte *DeltaImportSpawnedMonsters(const std::byte *src, std::unordered_map &spawnedMonsters) +{ + uint16_t size = *reinterpret_cast(src); + src += sizeof(uint16_t); + + for (size_t i = 0; i < size; i++) { + uint16_t monsterId = *reinterpret_cast(src); + src += sizeof(uint16_t); + DSpawnedMonster spawnedMonster; + memcpy(&spawnedMonster, src, sizeof(DSpawnedMonster)); + src += sizeof(DSpawnedMonster); + spawnedMonsters.emplace(monsterId, spawnedMonster); + } + + return src; } std::byte *DeltaExportJunk(std::byte *dst) @@ -636,7 +680,8 @@ void DeltaImportData(_cmd_id cmd, uint32_t recvOffset) DLevel &deltaLevel = GetDeltaLevel(i); src += DeltaImportItem(src, deltaLevel.item); src = DeltaImportObjects(src, deltaLevel.object); - DeltaImportMonster(src, deltaLevel.monster); + src += DeltaImportMonster(src, deltaLevel.monster); + src = DeltaImportSpawnedMonsters(src, deltaLevel.spawnedMonsters); } else { app_fatal(StrCat("Unkown network message type: ", cmd)); } @@ -2328,6 +2373,26 @@ size_t OnOpenGrave(const TCmd *pCmd) return sizeof(*pCmd); } +size_t OnSpawnMonster(const TCmd *pCmd, const Player &player) +{ + const auto &message = *reinterpret_cast(pCmd); + if (gbBufferMsgs == 1) + return sizeof(message); + + const Point position { message.x, message.y }; + + size_t typeIndex = static_cast(SDL_SwapLE16(message.typeIndex)); + size_t monsterId = static_cast(SDL_SwapLE16(message.monsterId)); + + DLevel &deltaLevel = GetDeltaLevel(player); + + deltaLevel.spawnedMonsters[monsterId] = { typeIndex, message.seed }; + + if (player.isOnActiveLevel() && &player != MyPlayer) + InitializeSpawnedMonster(position, message.dir, typeIndex, monsterId, message.seed); + return sizeof(message); +} + } // namespace void PrepareItemForNetwork(const Item &item, TItem &messageItem) @@ -2434,7 +2499,9 @@ void DeltaExportData(uint8_t pnum) + 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(deltaLevel.monster) /* latest monster state */ + + sizeof(uint16_t) /* spanwned monster count */ + + (sizeof(uint16_t) + sizeof(DSpawnedMonster)) * MaxMonsters; /* spanwned monsters */ std::unique_ptr dst { new std::byte[bufferSize] }; std::byte *dstEnd = &dst.get()[1]; @@ -2443,6 +2510,7 @@ void DeltaExportData(uint8_t pnum) dstEnd = DeltaExportItem(dstEnd, deltaLevel.item); dstEnd = DeltaExportObject(dstEnd, deltaLevel.object); dstEnd = DeltaExportMonster(dstEnd, deltaLevel.monster); + dstEnd = DeltaExportSpawnedMonsters(dstEnd, deltaLevel.spawnedMonsters); uint32_t size = CompressData(dst.get(), dstEnd); multi_send_zero_packet(pnum, CMD_DLEVEL, dst.get(), size); } @@ -2616,6 +2684,10 @@ void DeltaLoadLevel() uint8_t localLevel = GetLevelForMultiplayer(*MyPlayer); DLevel &deltaLevel = GetDeltaLevel(localLevel); if (leveltype != DTYPE_TOWN) { + for (auto &deltaSpawnedMonster : deltaLevel.spawnedMonsters) { + LoadDeltaSpawnedMonster(deltaSpawnedMonster.second.typeIndex, deltaSpawnedMonster.first, deltaSpawnedMonster.second.seed); + assert(deltaLevel.monster[deltaSpawnedMonster.first].position.x != 0xFF); + } for (size_t i = 0; i < MaxMonsters; i++) { if (deltaLevel.monster[i].position.x == 0xFF) continue; @@ -2756,6 +2828,20 @@ void NetSendCmdGolem(uint8_t mx, uint8_t my, Direction dir, uint8_t menemy, int NetSendLoPri(MyPlayerId, (std::byte *)&cmd, sizeof(cmd)); } +void NetSendCmdSpawnMonster(Point position, Direction dir, uint16_t typeIndex, uint16_t monsterId, uint32_t seed) +{ + TCmdSpawnMonster cmd; + + cmd.bCmd = CMD_SPAWNMONSTER; + cmd.x = position.x; + cmd.y = position.y; + cmd.dir = dir; + cmd.typeIndex = SDL_SwapLE16(typeIndex); + cmd.monsterId = SDL_SwapLE16(monsterId); + cmd.seed = SDL_SwapLE32(seed); + NetSendHiPri(MyPlayerId, (std::byte *)&cmd, sizeof(cmd)); +} + void NetSendCmdLoc(uint8_t playerId, bool bHiPri, _cmd_id bCmd, Point position) { if (playerId == MyPlayerId && WasPlayerCmdAlreadyRequested(bCmd, position)) @@ -3264,6 +3350,8 @@ size_t ParseCmd(uint8_t pnum, const TCmd *pCmd) return OnOpenHive(pCmd, player); case CMD_OPENGRAVE: return OnOpenGrave(pCmd); + case CMD_SPAWNMONSTER: + return OnSpawnMonster(pCmd, player); default: break; } diff --git a/Source/msg.h b/Source/msg.h index c743049ac..c6229ad7c 100644 --- a/Source/msg.h +++ b/Source/msg.h @@ -416,6 +416,10 @@ enum _cmd_id : uint8_t { CMD_NAKRUL, CMD_OPENHIVE, CMD_OPENGRAVE, + // Spawn a monster at target location. + // + // body (TCmdSpawnMonster) + CMD_SPAWNMONSTER, // Fake command; set current player for succeeding mega pkt buffer messages. // // body (TFakeCmdPlr) @@ -514,6 +518,16 @@ struct TCmdGolem { uint8_t _currlevel; }; +struct TCmdSpawnMonster { + _cmd_id bCmd; + uint8_t x; + uint8_t y; + Direction dir; + uint16_t typeIndex; + uint16_t monsterId; + uint32_t seed; +}; + struct TCmdQuest { _cmd_id bCmd; int8_t q; @@ -738,6 +752,7 @@ void DeltaLoadLevel(); void ClearLastSentPlayerCmd(); void NetSendCmd(bool bHiPri, _cmd_id bCmd); void NetSendCmdGolem(uint8_t mx, uint8_t my, Direction dir, uint8_t menemy, int hp, uint8_t cl); +void NetSendCmdSpawnMonster(Point position, Direction dir, uint16_t typeIndex, uint16_t monsterId, uint32_t seed); void NetSendCmdLoc(uint8_t playerId, bool bHiPri, _cmd_id bCmd, Point position); void NetSendCmdLocParam1(bool bHiPri, _cmd_id bCmd, Point position, uint16_t wParam1); void NetSendCmdLocParam2(bool bHiPri, _cmd_id bCmd, Point position, uint16_t wParam1, uint16_t wParam2);