mirror of
https://github.com/libretro/devilutionX.git
synced 2026-11-07 10:25:57 -10:00
Sync monster spawn in multiplayer
This commit is contained in:
1 parent
423150b457
commit
51ab0106ab
5 files changed
+198
-18
No files matched your search
@@ -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;
|
||||
@@ -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
|
||||
|
||||
+67
-3
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
+91
-3
@@ -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<WorldTilePosition, DObjectStr> object;
|
||||
std::unordered_map<size_t, DSpawnedMonster> 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<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)
|
||||
@@ -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<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)
|
||||
@@ -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];
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user