From 1f7b0607a651c3f5ce0bf220c8116ddee975762c Mon Sep 17 00:00:00 2001 From: obligaron Date: Sun, 20 Feb 2022 10:13:39 +0100 Subject: [PATCH] public game browsing: show difficulty, speed, players and possible incompatibility --- Source/DiabloUI/selgame.cpp | 111 ++++++++++++++++++++++++++++------ Source/diablo.h | 6 +- Source/dvlnet/abstract_net.h | 5 +- Source/dvlnet/base_protocol.h | 47 ++++++++++---- Source/dvlnet/cdwrap.h | 4 +- Source/multi.h | 9 +++ Source/storm/storm_net.cpp | 2 +- Source/storm/storm_net.hpp | 4 +- 8 files changed, 149 insertions(+), 39 deletions(-) diff --git a/Source/DiabloUI/selgame.cpp b/Source/DiabloUI/selgame.cpp index 71418e43b..5a9418159 100644 --- a/Source/DiabloUI/selgame.cpp +++ b/Source/DiabloUI/selgame.cpp @@ -13,6 +13,7 @@ #include "options.h" #include "storm/storm_net.hpp" #include "utils/language.h" +#include "utils/utf8.hpp" namespace devilution { @@ -39,7 +40,7 @@ const char *title = ""; std::vector> vecSelGameDlgItems; std::vector> vecSelGameDialog; -std::vector Gamelist; +std::vector Gamelist; uint32_t firstPublicGameInfoRequestSend = 0; int HighlightedItem; @@ -63,6 +64,41 @@ void selgame_Free() selgame_FreeVectors(); } +bool IsGameCompatible(const GameData &data) +{ + return (data.versionMajor == PROJECT_VERSION_MAJOR + && data.versionMinor == PROJECT_VERSION_MINOR + && data.versionPatch == PROJECT_VERSION_PATCH + && data.programid == GAME_ID); + return false; +} + +static std::string GetErrorMessageIncompatibility(const GameData &data) +{ + if (data.programid != GAME_ID) { + std::string gameMode; + switch (data.programid) { + case GameIdDiabloFull: + gameMode = _("Diablo"); + break; + case GameIdDiabloSpawn: + gameMode = _("Diablo Shareware"); + break; + case GameIdHellfireFull: + gameMode = _("Hellfire"); + break; + case GameIdHellfireSpawn: + gameMode = _("Hellfire Shareware"); + break; + default: + return _("The host is running a different game than you."); + } + return fmt::format(_("The host is running a different game mode ({:s}) than you."), gameMode); + } else { + return fmt::format(_(/* TRANSLATORS: Error message when somebody tries to join a game running another version. */ "Your version {:s} does not match the host {:d}.{:d}.{:d}."), PROJECT_VERSION, data.versionMajor, data.versionMinor, data.versionPatch).c_str(); + } +} + } // namespace void selgame_GameSelection_Init() @@ -116,7 +152,7 @@ void selgame_GameSelection_Init() vecSelGameDlgItems.push_back(std::make_unique(_("None"), -1, UiFlags::ElementDisabled | UiFlags::ColorUiSilver)); } else { for (unsigned i = 0; i < Gamelist.size(); i++) { - vecSelGameDlgItems.push_back(std::make_unique(Gamelist[i].c_str(), i + 3, UiFlags::ColorUiGold)); + vecSelGameDlgItems.push_back(std::make_unique(Gamelist[i].name.c_str(), i + 3, UiFlags::ColorUiGold)); } } } @@ -152,7 +188,52 @@ void selgame_GameSelection_Focus(int value) strcpy(selgame_Description, _("Enter an IP or a hostname and join a game already in progress at that address.")); break; default: - strcpy(selgame_Description, _("Join the public game already in progress at this address.")); + const auto &gameInfo = Gamelist[vecSelGameDlgItems[value]->m_value - 3]; + std::string infoString = _("Join the public game already in progress at this address."); + infoString.append("\n\n"); + if (IsGameCompatible(gameInfo.gameData)) { + string_view difficulty; + switch (gameInfo.gameData.nDifficulty) { + case DIFF_NORMAL: + difficulty = _("Normal"); + break; + case DIFF_NIGHTMARE: + difficulty = _("Nightmare"); + break; + case DIFF_HELL: + difficulty = _("Hell"); + break; + } + infoString.append(fmt::format(_(/* TRANSLATORS: {:s} means: Game Difficulty. */ "Difficulty: {:s}"), difficulty)); + infoString.append("\n"); + switch (gameInfo.gameData.nTickRate) { + case 20: + infoString.append(_("Speed: Normal")); + break; + case 30: + infoString.append(_("Speed: Fast")); + break; + case 40: + infoString.append(_("Speed: Faster")); + break; + case 50: + infoString.append(_("Speed: Fastest")); + break; + default: + // This should not occure, so no translations is needed + infoString.append(fmt::format("Speed: {}", gameInfo.gameData.nTickRate)); + break; + } + infoString.append("\n"); + infoString.append(_("Players: ")); + for (auto &playerName : gameInfo.players) { + infoString.append(playerName); + infoString.append(" "); + } + } else { + infoString.append(GetErrorMessageIncompatibility(gameInfo.gameData)); + } + CopyUtf8(selgame_Description, infoString, sizeof(selgame_Description)); break; } strcpy(selgame_Description, WordWrapString(selgame_Description, DESCRIPTION_WIDTH).c_str()); @@ -177,7 +258,7 @@ void selgame_GameSelection_Select(int value) selgame_selectedGame = value; if (value > 2) { - strcpy(selgame_Ip, Gamelist[value - 3].c_str()); + strcpy(selgame_Ip, Gamelist[value - 3].name.c_str()); selgame_Password_Select(value); return; } @@ -454,25 +535,15 @@ void selgame_Password_Init(int /*value*/) UiInitList(nullptr, selgame_Password_Select, selgame_Password_Esc, vecSelGameDialog); } -static bool IsGameCompatible(const GameData &data) +static bool IsGameCompatibleWithErrorMessage(const GameData &data) { - if (data.versionMajor == PROJECT_VERSION_MAJOR - && data.versionMinor == PROJECT_VERSION_MINOR - && data.versionPatch == PROJECT_VERSION_PATCH - && data.programid == GAME_ID) { + if (IsGameCompatible(data)) return IsDifficultyAllowed(data.nDifficulty); - } selgame_Free(); - if (data.programid != GAME_ID) { - UiSelOkDialog(title, _("The host is running a different game than you."), false); - } else { - char msg[128]; - strcpy(msg, fmt::format(_(/* TRANSLATORS: Error message when somebody tries to join a game running another version. */ "Your version {:s} does not match the host {:d}.{:d}.{:d}."), PROJECT_VERSION, data.versionMajor, data.versionMinor, data.versionPatch).c_str()); - - UiSelOkDialog(title, msg, false); - } + std::string errorMessage = GetErrorMessageIncompatibility(data); + UiSelOkDialog(title, errorMessage.c_str(), false); selgame_Init(); @@ -495,7 +566,7 @@ void selgame_Password_Select(int /*value*/) if (selgame_selectedGame > 1) { strcpy(sgOptions.Network.szPreviousHost, selgame_Ip); if (SNetJoinGame(selgame_Ip, gamePassword, gdwPlayerId)) { - if (!IsGameCompatible(*m_game_data)) { + if (!IsGameCompatibleWithErrorMessage(*m_game_data)) { InitGameInfo(); selgame_GameSelection_Select(1); return; @@ -562,7 +633,7 @@ void RefreshGameList() } if (lastUpdate == 0 || currentTime - lastUpdate > 5000) { - std::vector gamelist = DvlNet_GetGamelist(); + std::vector gamelist = DvlNet_GetGamelist(); Gamelist.clear(); for (unsigned i = 0; i < gamelist.size(); i++) { Gamelist.push_back(gamelist[i]); diff --git a/Source/diablo.h b/Source/diablo.h index a1e388631..0b9c5c982 100644 --- a/Source/diablo.h +++ b/Source/diablo.h @@ -17,7 +17,11 @@ namespace devilution { -#define GAME_ID (gbIsHellfire ? (gbIsSpawn ? LoadBE32("HSHR") : LoadBE32("HRTL")) : (gbIsSpawn ? LoadBE32("DSHR") : LoadBE32("DRTL"))) +constexpr uint32_t GameIdDiabloFull = LoadBE32("DRTL"); +constexpr uint32_t GameIdDiabloSpawn = LoadBE32("DSHR"); +constexpr uint32_t GameIdHellfireFull = LoadBE32("HRTL"); +constexpr uint32_t GameIdHellfireSpawn = LoadBE32("HSHR"); +#define GAME_ID (gbIsHellfire ? (gbIsSpawn ? GameIdHellfireSpawn : GameIdHellfireFull) : (gbIsSpawn ? GameIdDiabloSpawn : GameIdDiabloFull)) #define NUMLEVELS 25 diff --git a/Source/dvlnet/abstract_net.h b/Source/dvlnet/abstract_net.h index befe3b8c1..fcd498f2c 100644 --- a/Source/dvlnet/abstract_net.h +++ b/Source/dvlnet/abstract_net.h @@ -5,6 +5,7 @@ #include #include +#include "multi.h" #include "storm/storm_net.hpp" namespace devilution { @@ -57,9 +58,9 @@ public: { } - virtual std::vector get_gamelist() + virtual std::vector get_gamelist() { - return std::vector(); + return std::vector(); } static std::unique_ptr MakeNet(provider_t provider); diff --git a/Source/dvlnet/base_protocol.h b/Source/dvlnet/base_protocol.h index ccf87e761..d84a554e6 100644 --- a/Source/dvlnet/base_protocol.h +++ b/Source/dvlnet/base_protocol.h @@ -27,7 +27,7 @@ public: virtual std::string make_default_gamename(); virtual bool send_info_request(); virtual void clear_gamelist(); - virtual std::vector get_gamelist(); + virtual std::vector get_gamelist(); virtual ~base_protocol() = default; @@ -37,7 +37,7 @@ private: endpoint firstpeer; std::string gamename; - std::map game_list; + std::map, endpoint>> game_list; std::array peers; plr_t get_master(); @@ -86,7 +86,7 @@ bool base_protocol

::wait_firstpeer() // wait for peer for 5 seconds for (auto i = 0; i < 500; ++i) { if (game_list.count(gamename)) { - firstpeer = game_list[gamename]; + firstpeer = std::get<2>(game_list[gamename]); break; } send_info_request(); @@ -229,10 +229,24 @@ template void base_protocol

::recv_decrypted(packet &pkt, endpoint sender) { if (pkt.Source() == PLR_BROADCAST && pkt.Destination() == PLR_MASTER && pkt.Type() == PT_INFO_REPLY) { - std::string pname; - pname.resize(pkt.Info().size()); - std::memcpy(&pname[0], pkt.Info().data(), pkt.Info().size()); - game_list[pname] = sender; + constexpr size_t sizePlayerName = (sizeof(char) * PLR_NAME_LEN); + size_t neededSize = sizeof(GameData) + (sizePlayerName * MAX_PLRS); + if (pkt.Info().size() < neededSize) + return; + const GameData *gameData = (const GameData *)pkt.Info().data(); + std::vector playerNames; + for (size_t i = 0; i < MAX_PLRS; i++) { + std::string playerName; + const char *playerNamePointer = (const char *)(pkt.Info().data() + sizeof(GameData) + (i * sizePlayerName)); + playerName.append(playerNamePointer, strnlen(playerNamePointer, PLR_NAME_LEN)); + if (!playerName.empty()) + playerNames.push_back(playerName); + } + std::string gameName; + size_t gameNameSize = pkt.Info().size() - neededSize; + gameName.resize(gameNameSize); + std::memcpy(&gameName[0], pkt.Info().data() + neededSize, gameNameSize); + game_list[gameName] = std::make_tuple(*gameData, playerNames, sender); return; } recv_ingame(pkt, sender); @@ -247,8 +261,17 @@ void base_protocol

::recv_ingame(packet &pkt, endpoint sender) } else if (pkt.Type() == PT_INFO_REQUEST) { if ((plr_self != PLR_BROADCAST) && (get_master() == plr_self)) { buffer_t buf; - buf.resize(gamename.size()); - std::memcpy(buf.data(), &gamename[0], gamename.size()); + constexpr size_t sizePlayerName = (sizeof(char) * PLR_NAME_LEN); + buf.resize(game_init_info.size() + (sizePlayerName * MAX_PLRS) + gamename.size()); + std::memcpy(buf.data(), &game_init_info[0], game_init_info.size()); + for (size_t i = 0; i < MAX_PLRS; i++) { + if (Players[i].plractive) { + std::memcpy(buf.data() + game_init_info.size() + (i * sizePlayerName), &Players[i]._pName, sizePlayerName); + } else { + std::memset(buf.data() + game_init_info.size() + (i * sizePlayerName), '\0', sizePlayerName); + } + } + std::memcpy(buf.data() + game_init_info.size() + (sizePlayerName * MAX_PLRS), &gamename[0], gamename.size()); auto reply = pktfty->make_packet(PLR_BROADCAST, PLR_MASTER, buf); @@ -280,12 +303,12 @@ void base_protocol

::clear_gamelist() } template -std::vector base_protocol

::get_gamelist() +std::vector base_protocol

::get_gamelist() { recv(); - std::vector ret; + std::vector ret; for (auto &s : game_list) { - ret.push_back(s.first); + ret.push_back({ s.first, std::get<0>(s.second), std::get<1>(s.second) }); } return ret; } diff --git a/Source/dvlnet/cdwrap.h b/Source/dvlnet/cdwrap.h index 73ae234ee..4d48b8ab4 100644 --- a/Source/dvlnet/cdwrap.h +++ b/Source/dvlnet/cdwrap.h @@ -42,7 +42,7 @@ public: virtual std::string make_default_gamename(); virtual bool send_info_request(); virtual void clear_gamelist(); - virtual std::vector get_gamelist(); + virtual std::vector get_gamelist(); virtual void setup_password(std::string pw); virtual void clear_password(); @@ -187,7 +187,7 @@ void cdwrap::clear_gamelist() } template -std::vector cdwrap::get_gamelist() +std::vector cdwrap::get_gamelist() { return dvlnet_wrap->get_gamelist(); } diff --git a/Source/multi.h b/Source/multi.h index 6fd2bad29..39fabb6d3 100644 --- a/Source/multi.h +++ b/Source/multi.h @@ -6,6 +6,8 @@ #pragma once #include +#include +#include #include "msg.h" #include "utils/attributes.h" @@ -31,6 +33,13 @@ struct GameData { uint8_t bFriendlyFire; }; +/* @brief Contains info of running public game (for game list browsing) */ +struct GameInfo { + std::string name; + GameData gameData; + std::vector players; +}; + extern bool gbSomebodyWonGameKludge; extern char szPlayerDescript[128]; extern uint16_t sgwPackPlrOffsetTbl[MAX_PLRS]; diff --git a/Source/storm/storm_net.cpp b/Source/storm/storm_net.cpp index b5037e7e6..fd47dea16 100644 --- a/Source/storm/storm_net.cpp +++ b/Source/storm/storm_net.cpp @@ -244,7 +244,7 @@ void DvlNet_ClearGamelist() return dvlnet_inst->clear_gamelist(); } -std::vector DvlNet_GetGamelist() +std::vector DvlNet_GetGamelist() { return dvlnet_inst->get_gamelist(); } diff --git a/Source/storm/storm_net.hpp b/Source/storm/storm_net.hpp index 892abf051..054574199 100644 --- a/Source/storm/storm_net.hpp +++ b/Source/storm/storm_net.hpp @@ -5,6 +5,8 @@ #include #include +#include "multi.h" + namespace devilution { enum game_info : uint8_t { @@ -167,7 +169,7 @@ void SNetGetProviderCaps(struct _SNETCAPS *); bool DvlNet_SendInfoRequest(); void DvlNet_ClearGamelist(); -std::vector DvlNet_GetGamelist(); +std::vector DvlNet_GetGamelist(); void DvlNet_SetPassword(std::string pw); void DvlNet_ClearPassword(); bool DvlNet_IsPublicGame();