Skip to content

Commit

Permalink
Sync monster spawn in multiplayer
Browse files Browse the repository at this point in the history
  • Loading branch information
obligaron authored and AJenbo committed Jan 11, 2024
1 parent 423150b commit 51ab010
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 18 deletions.
24 changes: 12 additions & 12 deletions Source/lua/modules/dev/monsters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ std::string DebugCmdSpawnMonster(std::string name, std::optional<unsigned> 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;
Expand All @@ -137,28 +138,27 @@ std::string DebugCmdSpawnMonster(std::string name, std::optional<unsigned> count

Player &myPlayer = *MyPlayer;

unsigned spawnedMonster = 0;
size_t monstersToSpawn = std::min<size_t>(MaxMonsters - ActiveMonsterCount, count);
if (monstersToSpawn == 0)
return "Can't spawn any monsters";

auto ret = Crawl(0, MaxCrawlRadius, [&](Displacement displacement) -> std::optional<std::string> {
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
Expand Down
70 changes: 67 additions & 3 deletions Source/monster.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>(monsterId);
ActiveMonsters[index] = oldId;
ActiveMonsterCount += 1;
}
}

} // namespace

size_t AddMonsterType(_monster_id type, placeflag placeflag)
Expand Down Expand Up @@ -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<uint16_t>(typeIndex), static_cast<uint16_t>(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> {
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)
Expand Down
13 changes: 13 additions & 0 deletions Source/monster.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
94 changes: 91 additions & 3 deletions Source/msg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<WorldTilePosition, DObjectStr> object;
std::unordered_map<size_t, DSpawnedMonster> spawnedMonsters;
DMonsterStr monster[MaxMonsters];
};

Expand Down Expand Up @@ -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++) {
Expand All @@ -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<size_t, DSpawnedMonster> &spawnedMonsters)
{
auto &size = *reinterpret_cast<uint16_t *>(dst);
size = static_cast<uint16_t>(spawnedMonsters.size());
dst += sizeof(uint16_t);

for (const auto &deltaSpawnedMonster : spawnedMonsters) {
auto &monsterId = *reinterpret_cast<uint16_t *>(dst);
monsterId = static_cast<uint16_t>(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<size_t, DSpawnedMonster> &spawnedMonsters)
{
uint16_t size = *reinterpret_cast<const uint16_t *>(src);
src += sizeof(uint16_t);

for (size_t i = 0; i < size; i++) {
uint16_t monsterId = *reinterpret_cast<const uint16_t *>(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)
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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<const TCmdSpawnMonster *>(pCmd);
if (gbBufferMsgs == 1)
return sizeof(message);

const Point position { message.x, message.y };

size_t typeIndex = static_cast<size_t>(SDL_SwapLE16(message.typeIndex));
size_t monsterId = static_cast<size_t>(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)
Expand Down Expand Up @@ -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<std::byte[]> dst { new std::byte[bufferSize] };

std::byte *dstEnd = &dst.get()[1];
Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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;
}
Expand Down
15 changes: 15 additions & 0 deletions Source/msg.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 51ab010

Please sign in to comment.