/** * @file multi.cpp * * Implementation of functions for keeping multiplaye games in sync. */ #include #include #include #include #include #include #include "DiabloUI/diabloui.h" #include "diablo.h" #include "engine/demomode.h" #include "engine/point.hpp" #include "engine/random.hpp" #include "engine/world_tile.hpp" #include "menu.h" #include "nthread.h" #include "options.h" #include "pfile.h" #include "plrmsg.h" #include "qol/chatlog.h" #include "storm/storm_net.hpp" #include "sync.h" #include "tmsg.h" #include "utils/endian.hpp" #include "utils/language.h" #include "utils/str_cat.hpp" namespace devilution { bool gbSomebodyWonGameKludge; uint16_t sgwPackPlrOffsetTbl[MAX_PLRS]; bool sgbPlayerTurnBitTbl[MAX_PLRS]; bool sgbPlayerLeftGameTbl[MAX_PLRS]; bool shareNextHighPriorityMessage; uint8_t gbActivePlayers; bool gbGameDestroyed; bool sgbSendDeltaTbl[MAX_PLRS]; GameData sgGameInitInfo; bool gbSelectProvider; int sglTimeoutStart; int sgdwPlayerLeftReasonTbl[MAX_PLRS]; uint32_t sgdwGameLoops; /** * Specifies the maximum number of players in a game, where 1 * represents a single player game and 4 represents a multi player game. */ bool gbIsMultiplayer; bool sgbTimeout; std::string GameName; std::string GamePassword; bool PublicGame; uint8_t gbDeltaSender; bool sgbNetInited; uint32_t player_state[MAX_PLRS]; Uint32 playerInfoTimers[MAX_PLRS]; bool IsLoopback; /** * Contains the set of supported event types supported by the multiplayer * event handler. */ const event_type EventTypes[3] = { EVENT_TYPE_PLAYER_LEAVE_GAME, EVENT_TYPE_PLAYER_CREATE_GAME, EVENT_TYPE_PLAYER_MESSAGE }; namespace { struct TBuffer { size_t dwNextWriteOffset; std::byte bData[4096]; }; TBuffer highPriorityBuffer; TBuffer lowPriorityBuffer; constexpr uint16_t HeaderCheckVal = #if SDL_BYTEORDER == SDL_LIL_ENDIAN LoadBE16("ip"); #else LoadLE16("ip"); #endif uint32_t sgbSentThisCycle; void BufferInit(TBuffer *pBuf) { pBuf->dwNextWriteOffset = 0; pBuf->bData[0] = std::byte { 0 }; } void CopyPacket(TBuffer *buf, const std::byte *packet, size_t size) { if (buf->dwNextWriteOffset + size + 2 > 0x1000) { return; } std::byte *p = &buf->bData[buf->dwNextWriteOffset]; buf->dwNextWriteOffset += size + 1; *p = static_cast(size); p++; memcpy(p, packet, size); p[size] = std::byte { 0 }; } std::byte *CopyBufferedPackets(std::byte *destination, TBuffer *source, size_t *size) { if (source->dwNextWriteOffset != 0) { std::byte *srcPtr = source->bData; while (true) { auto chunkSize = static_cast(*srcPtr); if (chunkSize == 0) break; if (chunkSize > *size) break; srcPtr++; memcpy(destination, srcPtr, chunkSize); destination += chunkSize; srcPtr += chunkSize; *size -= chunkSize; } memmove(source->bData, srcPtr, (source->bData - srcPtr) + source->dwNextWriteOffset + 1); source->dwNextWriteOffset += source->bData - srcPtr; return destination; } return destination; } void NetReceivePlayerData(TPkt *pkt) { const Player &myPlayer = *MyPlayer; Point target = myPlayer.GetTargetPosition(); // Don't send desired target position when we will change our position soon. // This prevents a desync where the remote client starts a walking to the old target position when the teleport is finished but the the new position isn't received yet. if (myPlayer._pmode == PM_SPELL && IsAnyOf(myPlayer.executedSpell.spellId, SpellID::Teleport, SpellID::Phasing, SpellID::Warp)) target = {}; pkt->hdr.wCheck = HeaderCheckVal; pkt->hdr.px = myPlayer.position.tile.x; pkt->hdr.py = myPlayer.position.tile.y; pkt->hdr.targx = target.x; pkt->hdr.targy = target.y; pkt->hdr.php = SDL_SwapLE32(myPlayer._pHitPoints); pkt->hdr.pmhp = SDL_SwapLE32(myPlayer._pMaxHP); pkt->hdr.mana = SDL_SwapLE32(myPlayer._pMana); pkt->hdr.maxmana = SDL_SwapLE32(myPlayer._pMaxMana); pkt->hdr.bstr = myPlayer._pBaseStr; pkt->hdr.bmag = myPlayer._pBaseMag; pkt->hdr.bdex = myPlayer._pBaseDex; } bool IsNetPlayerValid(const Player &player) { // we no longer check character level here, players with out-of-range clevels are not allowed to join the game and we don't observe change clevel messages that would set it out of range // (there's no code path that would result in _pLevel containing an out of range value in the DevilutionX code) return static_cast(player._pClass) < enum_size::value && player.plrlevel < NUMLEVELS && InDungeonBounds(player.position.tile) && !std::string_view(player._pName).empty(); } void CheckPlayerInfoTimeouts() { for (uint8_t i = 0; i < Players.size(); i++) { Player &player = Players[i]; if (&player == MyPlayer) { continue; } Uint32 &timerStart = playerInfoTimers[i]; bool isPlayerConnected = (player_state[i] & PS_CONNECTED) != 0; bool isPlayerValid = isPlayerConnected && IsNetPlayerValid(player); if (isPlayerConnected && !isPlayerValid && timerStart == 0) { timerStart = SDL_GetTicks(); } if (!isPlayerConnected || isPlayerValid) { timerStart = 0; } if (timerStart == 0) { continue; } // Time the player out after 15 seconds // if we do not receive valid player info if (SDL_GetTicks() - timerStart >= 15000) { SNetDropPlayer(i, LEAVE_DROP); timerStart = 0; } } } void SendPacket(uint8_t playerId, const std::byte *packet, size_t size) { TPkt pkt; NetReceivePlayerData(&pkt); const size_t sizeWithheader = size + sizeof(pkt.hdr); pkt.hdr.wLen = SDL_SwapLE16(static_cast(sizeWithheader)); memcpy(pkt.body, packet, size); if (!SNetSendMessage(playerId, &pkt.hdr, sizeWithheader)) nthread_terminate_game("SNetSendMessage0"); } void MonsterSeeds() { sgdwGameLoops++; const uint32_t seed = (sgdwGameLoops >> 8) | (sgdwGameLoops << 24); for (uint32_t i = 0; i < MaxMonsters; i++) Monsters[i].aiSeed = seed + i; } void HandleTurnUpperBit(uint8_t pnum) { uint8_t i; for (i = 0; i < Players.size(); i++) { if ((player_state[i] & PS_CONNECTED) != 0 && i != pnum) break; } if (MyPlayerId == i) { sgbSendDeltaTbl[pnum] = true; } else if (pnum == MyPlayerId) { gbDeltaSender = i; } } void ParseTurn(uint8_t pnum, uint32_t turn) { if ((turn & 0x80000000) != 0) HandleTurnUpperBit(pnum); uint32_t absTurns = turn & 0x7FFFFFFF; if (sgbSentThisCycle < gdwTurnsInTransit + absTurns) { if (absTurns >= 0x7FFFFFFF) absTurns &= 0xFFFF; sgbSentThisCycle = absTurns + gdwTurnsInTransit; sgdwGameLoops = 4 * absTurns * sgbNetUpdateRate; } } void PlayerLeftMsg(Player &player, bool left) { if (&player == InspectPlayer) InspectPlayer = MyPlayer; if (&player == MyPlayer) return; if (!player.plractive) return; FixPlrWalkTags(player); RemovePortalMissile(player); DeactivatePortal(player); delta_close_portal(player); RemovePlrMissiles(player); if (left) { std::string_view pszFmt = _("Player '{:s}' just left the game"); switch (sgdwPlayerLeftReasonTbl[player.getId()]) { case LEAVE_ENDING: pszFmt = _("Player '{:s}' killed Diablo and left the game!"); gbSomebodyWonGameKludge = true; break; case LEAVE_DROP: pszFmt = _("Player '{:s}' dropped due to timeout"); break; } EventPlrMsg(fmt::format(fmt::runtime(pszFmt), player._pName)); } player.plractive = false; player._pName[0] = '\0'; ResetPlayerGFX(player); gbActivePlayers--; } void ClearPlayerLeftState() { for (uint8_t i = 0; i < Players.size(); i++) { if (sgbPlayerLeftGameTbl[i]) { if (gbBufferMsgs == 1) msg_send_drop_pkt(i, sgdwPlayerLeftReasonTbl[i]); else PlayerLeftMsg(Players[i], true); sgbPlayerLeftGameTbl[i] = false; sgdwPlayerLeftReasonTbl[i] = 0; } } } void CheckDropPlayer() { for (uint8_t i = 0; i < Players.size(); i++) { if ((player_state[i] & PS_ACTIVE) == 0 && (player_state[i] & PS_CONNECTED) != 0) { SNetDropPlayer(i, LEAVE_DROP); } } } void BeginTimeout() { if (!sgbTimeout) { return; } #ifdef _DEBUG if (DebugDisableNetworkTimeout) { return; } #endif uint32_t nTicks = SDL_GetTicks() - sglTimeoutStart; if (nTicks > 20000) { gbRunGame = false; return; } if (nTicks < 10000) { return; } CheckDropPlayer(); } 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])); if (messageSize == 0) { break; } offset += messageSize; } } void ProcessTmsgs() { while (true) { std::unique_ptr msg; uint8_t size = tmsg_get(&msg); if (size == 0) break; HandleAllPackets(MyPlayerId, msg.get(), size); } } void SendPlayerInfo(uint8_t pnum, _cmd_id cmd) { PlayerNetPack packed; Player &myPlayer = *MyPlayer; PackNetPlayer(packed, myPlayer); multi_send_zero_packet(pnum, cmd, reinterpret_cast(&packed), sizeof(PlayerNetPack)); } void SetupLocalPositions() { currlevel = 0; leveltype = DTYPE_TOWN; setlevel = false; const WorldTilePosition spawns[9] = { { 75, 68 }, { 77, 70 }, { 75, 70 }, { 77, 68 }, { 76, 69 }, { 75, 69 }, { 76, 68 }, { 77, 69 }, { 76, 70 } }; Player &myPlayer = *MyPlayer; myPlayer.position.tile = spawns[MyPlayerId]; myPlayer.position.future = myPlayer.position.tile; myPlayer.setLevel(currlevel); myPlayer._pLvlChanging = true; myPlayer.pLvlLoad = 0; myPlayer._pmode = PM_NEWLVL; myPlayer.destAction = ACTION_NONE; } void HandleEvents(_SNETEVENT *pEvt) { switch (pEvt->eventid) { case EVENT_TYPE_PLAYER_CREATE_GAME: { auto *gameData = (GameData *)pEvt->data; if (gameData->size != sizeof(GameData)) app_fatal(StrCat("Invalid size of game data: ", gameData->size)); sgGameInitInfo = *gameData; sgbPlayerTurnBitTbl[pEvt->playerid] = true; break; } case EVENT_TYPE_PLAYER_LEAVE_GAME: { sgbPlayerLeftGameTbl[pEvt->playerid] = true; sgbPlayerTurnBitTbl[pEvt->playerid] = false; int leftReason = 0; if (pEvt->data != nullptr && pEvt->databytes >= sizeof(leftReason)) leftReason = *(int *)pEvt->data; sgdwPlayerLeftReasonTbl[pEvt->playerid] = leftReason; if (leftReason == LEAVE_ENDING) gbSomebodyWonGameKludge = true; sgbSendDeltaTbl[pEvt->playerid] = false; if (gbDeltaSender == pEvt->playerid) gbDeltaSender = MAX_PLRS; } break; case EVENT_TYPE_PLAYER_MESSAGE: EventPlrMsg((char *)pEvt->data); break; } } void RegisterNetEventHandlers() { for (auto eventType : EventTypes) { if (!SNetRegisterEventHandler(eventType, HandleEvents)) { app_fatal(StrCat("SNetRegisterEventHandler:\n", SDL_GetError())); } } } void UnregisterNetEventHandlers() { for (auto eventType : EventTypes) { SNetUnregisterEventHandler(eventType); } } bool InitSingle(GameData *gameData) { Players.resize(1); if (!SNetInitializeProvider(SELCONN_LOOPBACK, gameData)) { return false; } int unused = 0; if (!SNetCreateGame("local", "local", (char *)&sgGameInitInfo, sizeof(sgGameInitInfo), &unused)) { app_fatal(StrCat("SNetCreateGame1:\n", SDL_GetError())); } MyPlayerId = 0; MyPlayer = &Players[MyPlayerId]; InspectPlayer = MyPlayer; gbIsMultiplayer = false; pfile_read_player_from_save(gSaveNumber, *MyPlayer); return true; } bool InitMulti(GameData *gameData) { Players.resize(MAX_PLRS); int playerId; while (true) { if (gbSelectProvider && !UiSelectProvider(gameData)) { return false; } RegisterNetEventHandlers(); if (UiSelectGame(gameData, &playerId)) break; gbSelectProvider = true; } if (static_cast(playerId) >= Players.size()) { return false; } MyPlayerId = playerId; MyPlayer = &Players[MyPlayerId]; InspectPlayer = MyPlayer; gbIsMultiplayer = true; pfile_read_player_from_save(gSaveNumber, *MyPlayer); return true; } } // namespace void InitGameInfo() { xoshiro128plusplus gameGenerator = ReserveSeedSequence(); gameGenerator.save(sgGameInitInfo.gameSeed); sgGameInitInfo.size = sizeof(sgGameInitInfo); sgGameInitInfo.programid = GAME_ID; sgGameInitInfo.versionMajor = PROJECT_VERSION_MAJOR; sgGameInitInfo.versionMinor = PROJECT_VERSION_MINOR; sgGameInitInfo.versionPatch = PROJECT_VERSION_PATCH; sgGameInitInfo.nTickRate = *sgOptions.Gameplay.tickRate; sgGameInitInfo.bRunInTown = *sgOptions.Gameplay.runInTown ? 1 : 0; sgGameInitInfo.bTheoQuest = *sgOptions.Gameplay.theoQuest ? 1 : 0; sgGameInitInfo.bCowQuest = *sgOptions.Gameplay.cowQuest ? 1 : 0; sgGameInitInfo.bFriendlyFire = *sgOptions.Gameplay.friendlyFire ? 1 : 0; sgGameInitInfo.fullQuests = (!gbIsMultiplayer || *sgOptions.Gameplay.multiplayerFullQuests) ? 1 : 0; } void NetSendLoPri(uint8_t playerId, const std::byte *data, size_t size) { if (data != nullptr && size != 0) { CopyPacket(&lowPriorityBuffer, data, size); SendPacket(playerId, data, size); } } void NetSendHiPri(uint8_t playerId, const std::byte *data, size_t size) { if (data != nullptr && size != 0) { CopyPacket(&highPriorityBuffer, data, size); SendPacket(playerId, data, size); } if (shareNextHighPriorityMessage) { shareNextHighPriorityMessage = false; TPkt pkt; NetReceivePlayerData(&pkt); std::byte *destination = pkt.body; size_t remainingSpace = gdwNormalMsgSize - sizeof(TPktHdr); destination = CopyBufferedPackets(destination, &highPriorityBuffer, &remainingSpace); destination = CopyBufferedPackets(destination, &lowPriorityBuffer, &remainingSpace); remainingSpace = sync_all_monsters(destination, remainingSpace); const size_t len = gdwNormalMsgSize - remainingSpace; pkt.hdr.wLen = SDL_SwapLE16(static_cast(len)); if (!SNetSendMessage(SNPLAYER_OTHERS, &pkt.hdr, len)) nthread_terminate_game("SNetSendMessage"); } } void multi_send_msg_packet(uint32_t pmask, const std::byte *data, size_t size) { TPkt pkt; NetReceivePlayerData(&pkt); const size_t len = size + sizeof(pkt.hdr); pkt.hdr.wLen = SDL_SwapLE16(static_cast(len)); memcpy(pkt.body, data, size); uint8_t playerID = 0; for (uint32_t v = 1; playerID < Players.size(); playerID++, v <<= 1) { if ((v & pmask) != 0) { if (!SNetSendMessage(playerID, &pkt.hdr, len)) { nthread_terminate_game("SNetSendMessage"); return; } } } } void multi_msg_countdown() { for (uint8_t i = 0; i < Players.size(); i++) { if ((player_state[i] & PS_TURN_ARRIVED) != 0) { if (gdwMsgLenTbl[i] == sizeof(int32_t)) ParseTurn(i, *(int32_t *)glpMsgTbl[i]); } } } void multi_player_left(uint8_t pnum, int reason) { sgbPlayerLeftGameTbl[pnum] = true; sgdwPlayerLeftReasonTbl[pnum] = reason; ClearPlayerLeftState(); } void multi_net_ping() { sgbTimeout = true; sglTimeoutStart = SDL_GetTicks(); } bool multi_handle_delta() { if (gbGameDestroyed) { gbRunGame = false; return false; } for (uint8_t i = 0; i < Players.size(); i++) { if (sgbSendDeltaTbl[i]) { sgbSendDeltaTbl[i] = false; DeltaExportData(i); } } sgbSentThisCycle = nthread_send_and_recv_turn(sgbSentThisCycle, 1); bool received; if (!nthread_recv_turns(&received)) { BeginTimeout(); return false; } sgbTimeout = false; if (received) { if (!shareNextHighPriorityMessage) { // If there are any high priority messages pending, // share them with other players now shareNextHighPriorityMessage = true; if (highPriorityBuffer.dwNextWriteOffset != 0) NetSendHiPri(MyPlayerId, nullptr, 0); } else { // If there were no high priority messages in at least two consecutive game // ticks, this shares the low priority messages and monster sync data NetSendHiPri(MyPlayerId, nullptr, 0); shareNextHighPriorityMessage = true; } } MonsterSeeds(); return true; } void multi_process_network_packets() { ClearPlayerLeftState(); ProcessTmsgs(); uint8_t playerId = std::numeric_limits::max(); TPktHdr *pkt; size_t dwMsgSize = 0; while (SNetReceiveMessage(&playerId, (void **)&pkt, &dwMsgSize)) { dwRecCount++; ClearPlayerLeftState(); if (dwMsgSize < sizeof(TPktHdr)) continue; if (playerId >= Players.size()) continue; if (pkt->wCheck != HeaderCheckVal) continue; if (SDL_SwapLE16(pkt->wLen) != dwMsgSize) continue; Player &player = Players[playerId]; if (!IsNetPlayerValid(player)) { _cmd_id cmd = *(const _cmd_id *)(pkt + 1); if (gbBufferMsgs == 0 && IsNoneOf(cmd, CMD_SEND_PLRINFO, CMD_ACK_PLRINFO)) { // Distrust all messages until // player info is received continue; } } Point syncPosition = { pkt->px, pkt->py }; player.position.last = syncPosition; if (&player != MyPlayer) { assert(gbBufferMsgs != 2); player._pHitPoints = SDL_SwapLE32(pkt->php); player._pMaxHP = SDL_SwapLE32(pkt->pmhp); player._pMana = SDL_SwapLE32(pkt->mana); player._pMaxMana = SDL_SwapLE32(pkt->maxmana); bool cond = gbBufferMsgs == 1; player._pBaseStr = pkt->bstr; player._pBaseMag = pkt->bmag; player._pBaseDex = pkt->bdex; if (!cond && player.plractive && player._pHitPoints != 0) { if (player.isOnActiveLevel() && !player._pLvlChanging) { if (player.position.tile.WalkingDistance(syncPosition) > 3 && PosOkPlayer(player, syncPosition)) { // got out of sync, clear the tiles around where we last thought the player was located FixPlrWalkTags(player); player.position.old = player.position.tile; // then just in case clear the tiles around the current position (probably unnecessary) FixPlrWalkTags(player); player.position.tile = syncPosition; player.position.future = syncPosition; if (player.isWalking()) player.position.temp = syncPosition; SetPlayerOld(player); player.occupyTile(player.position.tile, false); } if (player.position.future.WalkingDistance(player.position.tile) > 1) { player.position.future = player.position.tile; } Point target = { pkt->targx, pkt->targy }; if (target != Point {}) // does the client send a desired (future) position of remote player? MakePlrPath(player, target, true); } else { player.position.tile = syncPosition; player.position.future = syncPosition; SetPlayerOld(player); } } } HandleAllPackets(playerId, (const std::byte *)(pkt + 1), dwMsgSize - sizeof(TPktHdr)); } CheckPlayerInfoTimeouts(); } void multi_send_zero_packet(uint8_t pnum, _cmd_id bCmd, const std::byte *data, size_t size) { assert(pnum != MyPlayerId); assert(data != nullptr); assert(size <= 0x0ffff); for (size_t offset = 0; offset < size;) { TPkt pkt {}; pkt.hdr.wCheck = HeaderCheckVal; auto &message = *reinterpret_cast(pkt.body); message.bCmd = bCmd; assert(offset <= 0x0ffff); message.wOffset = SDL_SwapLE16(static_cast(offset)); size_t dwBody = gdwLargestMsgSize - sizeof(pkt.hdr) - sizeof(message); dwBody = std::min(dwBody, size - offset); assert(dwBody <= 0x0ffff); message.wBytes = SDL_SwapLE16(static_cast(dwBody)); memcpy(&pkt.body[sizeof(message)], &data[offset], dwBody); const size_t dwMsg = sizeof(pkt.hdr) + sizeof(message) + dwBody; assert(dwMsg <= 0x0ffff); pkt.hdr.wLen = SDL_SwapLE16(static_cast(dwMsg)); if (!SNetSendMessage(pnum, &pkt, dwMsg)) { nthread_terminate_game("SNetSendMessage2"); return; } offset += dwBody; } } void NetClose() { if (!sgbNetInited) { return; } sgbNetInited = false; nthread_cleanup(); tmsg_cleanup(); UnregisterNetEventHandlers(); SNetLeaveGame(3); if (gbIsMultiplayer) SDL_Delay(2000); if (!demo::IsRunning()) { Players.clear(); MyPlayer = nullptr; } } bool NetInit(bool bSinglePlayer) { while (true) { SetRndSeed(0); InitGameInfo(); memset(sgbPlayerTurnBitTbl, 0, sizeof(sgbPlayerTurnBitTbl)); gbGameDestroyed = false; memset(sgbPlayerLeftGameTbl, 0, sizeof(sgbPlayerLeftGameTbl)); memset(sgdwPlayerLeftReasonTbl, 0, sizeof(sgdwPlayerLeftReasonTbl)); memset(sgbSendDeltaTbl, 0, sizeof(sgbSendDeltaTbl)); Players.clear(); MyPlayer = nullptr; memset(sgwPackPlrOffsetTbl, 0, sizeof(sgwPackPlrOffsetTbl)); SNetSetBasePlayer(0); if (bSinglePlayer) { if (!InitSingle(&sgGameInitInfo)) return false; } else { if (!InitMulti(&sgGameInitInfo)) return false; } sgbNetInited = true; sgbTimeout = false; delta_init(); InitPlrMsg(); BufferInit(&highPriorityBuffer); BufferInit(&lowPriorityBuffer); shareNextHighPriorityMessage = true; sync_init(); nthread_start(sgbPlayerTurnBitTbl[MyPlayerId]); tmsg_start(); sgdwGameLoops = 0; sgbSentThisCycle = 0; gbDeltaSender = MyPlayerId; gbSomebodyWonGameKludge = false; nthread_send_and_recv_turn(0, 0); SetupLocalPositions(); SendPlayerInfo(SNPLAYER_OTHERS, CMD_SEND_PLRINFO); Player &myPlayer = *MyPlayer; ResetPlayerGFX(myPlayer); myPlayer.plractive = true; gbActivePlayers = 1; if (!sgbPlayerTurnBitTbl[MyPlayerId] || msg_wait_resync()) break; NetClose(); gbSelectProvider = false; } xoshiro128plusplus gameGenerator(sgGameInitInfo.gameSeed); gnTickDelay = 1000 / sgGameInitInfo.nTickRate; for (int i = 0; i < NUMLEVELS; i++) { DungeonSeeds[i] = gameGenerator.next(); LevelSeeds[i] = std::nullopt; } PublicGame = DvlNet_IsPublicGame(); Player &myPlayer = *MyPlayer; // separator for marking messages from a different game AddMessageToChatLog(_("New Game"), nullptr, UiFlags::ColorRed); AddMessageToChatLog(fmt::format(fmt::runtime(_("Player '{:s}' (level {:d}) just joined the game")), myPlayer._pName, myPlayer.getCharacterLevel())); return true; } void recv_plrinfo(Player &player, const TCmdPlrInfoHdr &header, bool recv) { static PlayerNetPack PackedPlayerBuffer[MAX_PLRS]; if (&player == MyPlayer) { return; } uint8_t pnum = player.getId(); auto &packedPlayer = PackedPlayerBuffer[pnum]; if (sgwPackPlrOffsetTbl[pnum] != SDL_SwapLE16(header.wOffset)) { sgwPackPlrOffsetTbl[pnum] = 0; if (header.wOffset != 0) { return; } } if (!recv && sgwPackPlrOffsetTbl[pnum] == 0) { SendPlayerInfo(pnum, CMD_ACK_PLRINFO); } memcpy(reinterpret_cast(&packedPlayer) + SDL_SwapLE16(header.wOffset), reinterpret_cast(&header) + sizeof(header), SDL_SwapLE16(header.wBytes)); sgwPackPlrOffsetTbl[pnum] += SDL_SwapLE16(header.wBytes); if (sgwPackPlrOffsetTbl[pnum] != sizeof(packedPlayer)) { return; } sgwPackPlrOffsetTbl[pnum] = 0; PlayerLeftMsg(player, false); if (!UnPackNetPlayer(packedPlayer, player)) { player = {}; SNetDropPlayer(pnum, LEAVE_DROP); return; } if (!recv) { return; } ResetPlayerGFX(player); player.plractive = true; gbActivePlayers++; std::string_view szEvent; if (sgbPlayerTurnBitTbl[pnum]) { szEvent = _("Player '{:s}' (level {:d}) just joined the game"); } else { szEvent = _("Player '{:s}' (level {:d}) is already in the game"); } EventPlrMsg(fmt::format(fmt::runtime(szEvent), player._pName, player.getCharacterLevel())); SyncInitPlr(player); if (!player.isOnActiveLevel()) { return; } if (player._pHitPoints >> 6 > 0) { StartStand(player, Direction::South); return; } player._pgfxnum &= ~0xFU; player._pmode = PM_DEATH; NewPlrAnim(player, player_graphic::Death, Direction::South); player.AnimInfo.currentFrame = player.AnimInfo.numberOfFrames - 2; dFlags[player.position.tile.x][player.position.tile.y] |= DungeonFlag::DeadPlayer; } } // namespace devilution