Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Source/DiabloUI/multi/selgame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
#include <SDL3/SDL_rect.h>
#include <SDL3/SDL_timer.h>
#else
#include <SDL.h>

Check warning on line 17 in Source/DiabloUI/multi/selgame.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/DiabloUI/multi/selgame.cpp:17:1 [misc-include-cleaner]

included header SDL.h is not used directly
#endif

#include <fmt/core.h>
Expand Down Expand Up @@ -54,8 +54,8 @@
int nTickRate;
int heroLevel;

static GameData *m_game_data;

Check warning on line 57 in Source/DiabloUI/multi/selgame.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/DiabloUI/multi/selgame.cpp:57:18 [misc-use-anonymous-namespace]

variable 'm_game_data' declared 'static', move to anonymous namespace instead
extern int provider;

Check warning on line 58 in Source/DiabloUI/multi/selgame.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/DiabloUI/multi/selgame.cpp:58:12 [readability-redundant-declaration]

redundant 'provider' declaration

#define DESCRIPTION_WIDTH 205

Expand All @@ -69,20 +69,20 @@
uint32_t firstPublicGameInfoRequestSend = 0;
size_t HighlightedItem;

void selgame_FreeVectors()

Check warning on line 72 in Source/DiabloUI/multi/selgame.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/DiabloUI/multi/selgame.cpp:72:6 [readability-identifier-naming]

invalid case style for function 'selgame_FreeVectors'
{
vecSelGameDlgItems.clear();

vecSelGameDialog.clear();
}

void selgame_Init()

Check warning on line 79 in Source/DiabloUI/multi/selgame.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/DiabloUI/multi/selgame.cpp:79:6 [readability-identifier-naming]

invalid case style for function 'selgame_Init'
{
LoadBackgroundArt("ui_art\\selgame");
LoadScrollBar();
}

void selgame_Free()

Check warning on line 85 in Source/DiabloUI/multi/selgame.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/DiabloUI/multi/selgame.cpp:85:6 [readability-identifier-naming]

invalid case style for function 'selgame_Free'
{
ArtBackground = std::nullopt;
UnloadScrollBar();
Expand All @@ -94,12 +94,14 @@
return (data.versionMajor == PROJECT_VERSION_MAJOR
&& data.versionMinor == PROJECT_VERSION_MINOR
&& data.versionPatch == PROJECT_VERSION_PATCH
&& data.programid == GAME_ID);
return false;
&& data.programid == GAME_ID
&& data.modHash == sgGameInitInfo.modHash);
}

static std::string GetErrorMessageIncompatibility(const GameData &data)

Check warning on line 101 in Source/DiabloUI/multi/selgame.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/DiabloUI/multi/selgame.cpp:101:20 [readability-static-definition-in-anonymous-namespace]

'GetErrorMessageIncompatibility' is a static definition in anonymous namespace; static is redundant here
{
if (data.modHash != sgGameInitInfo.modHash)
return std::string(_("The host is using a different set of mods."));
if (data.programid != GAME_ID) {
std::string_view gameMode;
switch (data.programid) {
Expand All @@ -119,12 +121,12 @@
return std::string(_("The host is running a different game than you."));
}
return fmt::format(fmt::runtime(_("The host is running a different game mode ({:s}) than you.")), gameMode);
} else {

Check warning on line 124 in Source/DiabloUI/multi/selgame.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/DiabloUI/multi/selgame.cpp:124:4 [readability-else-after-return]

do not use 'else' after 'return'
return fmt::format(fmt::runtime(_(/* 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);
}
}

void UiInitGameSelectionList(std::string_view search)

Check warning on line 129 in Source/DiabloUI/multi/selgame.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/DiabloUI/multi/selgame.cpp:129:6 [misc-no-recursion]

function 'UiInitGameSelectionList' is within a recursive call chain
{
selgame_enteringGame = false;
selgame_selectedGame = 0;
Expand All @@ -150,7 +152,7 @@

const Point uiPosition = GetUIRectangle().position;

const SDL_Rect rectScrollbar = { (Sint16)(uiPosition.x + 590), (Sint16)(uiPosition.y + 244), 25, 178 };

Check warning on line 155 in Source/DiabloUI/multi/selgame.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/DiabloUI/multi/selgame.cpp:155:8 [misc-include-cleaner]

no header providing "SDL_Rect" is directly included
vecSelGameDialog.push_back(std::make_unique<UiScrollbar>((*ArtScrollBarBackground)[0], (*ArtScrollBarThumb)[0], *ArtScrollBarArrow, rectScrollbar));

const SDL_Rect rect1 = { (Sint16)(uiPosition.x + 24), (Sint16)(uiPosition.y + 161), 590, 35 };
Expand Down
7 changes: 7 additions & 0 deletions Source/dvlnet/base_protocol.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <cstddef>
#include <memory>
#include <set>
#include <string>
Expand Down Expand Up @@ -318,6 +319,12 @@ void base_protocol<P>::recv()
template <class P>
tl::expected<void, PacketError> base_protocol<P>::handle_join_request(packet &inPkt, endpoint_t sender)
{
tl::expected<const buffer_t *, PacketError> pktInfo = inPkt.Info();
if (pktInfo.has_value() && (*pktInfo)->size() == sizeof(GameData) && game_init_info.size() == sizeof(GameData)) {
constexpr size_t ModHashOffset = offsetof(GameData, modHash);
if (LoadLE32((*pktInfo)->data() + ModHashOffset) != LoadLE32(game_init_info.data() + ModHashOffset))
return {};
}
plr_t i;
for (i = 0; i < Players.size(); ++i) {
Peer &peer = peers[i];
Expand Down
3 changes: 2 additions & 1 deletion Source/dvlnet/packet.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ class PacketError {
enum class ErrorCode : uint8_t {
None,
EncryptionFailed,
DecryptionFailed
DecryptionFailed,
ModMismatch
};

PacketError()
Expand Down
2 changes: 2 additions & 0 deletions Source/dvlnet/tcp_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ void tcp_client::HandleTcpErrorCode()
PacketError::ErrorCode code = static_cast<PacketError::ErrorCode>(pktData[0]);
if (code == PacketError::ErrorCode::DecryptionFailed)
RaiseIoHandlerError(_("Server failed to decrypt your packet. Check if you typed the password correctly."));
else if (code == PacketError::ErrorCode::ModMismatch)
RaiseIoHandlerError(_("The host is using a different set of mods."));
else
RaiseIoHandlerError(fmt::format("Unknown error code received from server: {:#04x}", pktData[0]));
}
Expand Down
18 changes: 14 additions & 4 deletions Source/dvlnet/tcp_server.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "dvlnet/tcp_server.h"

#include <chrono>
#include <cstddef>
#include <functional>
#include <memory>
#include <utility>
Expand Down Expand Up @@ -126,11 +127,20 @@ tl::expected<void, PacketError> tcp_server::HandleReceiveNewPlayer(const scc &co
if (newplr == PLR_BROADCAST)
return tl::make_unexpected(ServerError());

tl::expected<const buffer_t *, PacketError> pktInfo = inPkt.Info();
if (!pktInfo.has_value())
return tl::make_unexpected(pktInfo.error());
const buffer_t &joinerInfo = **pktInfo;

if (Empty()) {
tl::expected<const buffer_t *, PacketError> pktInfo = inPkt.Info();
if (!pktInfo.has_value())
return tl::make_unexpected(pktInfo.error());
game_init_info = **pktInfo;
game_init_info = joinerInfo;
} else if (joinerInfo.size() == sizeof(GameData) && game_init_info.size() == sizeof(GameData)) {
constexpr size_t ModHashOffset = offsetof(GameData, modHash);
if (LoadLE32(joinerInfo.data() + ModHashOffset) != LoadLE32(game_init_info.data() + ModHashOffset)) {
StartSend(con, PacketError::ErrorCode::ModMismatch);
DropConnection(con);
return {};
}
}

for (plr_t player = 0; player < Players.size(); player++) {
Expand Down
20 changes: 20 additions & 0 deletions Source/multi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <span>
#include <string_view>

#ifdef USE_SDL3
Expand Down Expand Up @@ -95,6 +96,7 @@ void GameData::swapLE()
gameSeed[1] = Swap32LE(gameSeed[1]);
gameSeed[2] = Swap32LE(gameSeed[2]);
gameSeed[3] = Swap32LE(gameSeed[3]);
modHash = Swap32LE(modHash);
}

namespace {
Expand Down Expand Up @@ -553,6 +555,22 @@ std::string FormatGameSeed(const uint32_t gameSeed[4])
gameSeed[0], gameSeed[1], gameSeed[2], gameSeed[3]);
}

uint32_t ComputeModListHash(std::span<const std::string_view> mods)
{
constexpr uint32_t FnvPrime = 16777619U;
constexpr uint32_t FnvOffsetBasis = 2166136261U;
uint32_t result = 0;
for (const std::string_view mod : mods) {
uint32_t hash = FnvOffsetBasis;
for (const char c : mod) {
hash ^= static_cast<uint8_t>(c);
hash *= FnvPrime;
}
result ^= hash;
}
return result;
}

void InitGameInfo()
{
const xoshiro128plusplus gameGenerator = ReserveSeedSequence();
Expand All @@ -570,6 +588,8 @@ void InitGameInfo()
sgGameInitInfo.bCowQuest = *options.Gameplay.cowQuest ? 1 : 0;
sgGameInitInfo.bFriendlyFire = *options.Gameplay.friendlyFire ? 1 : 0;
sgGameInitInfo.fullQuests = (!gbIsMultiplayer || *options.Gameplay.multiplayerFullQuests) ? 1 : 0;
const std::vector<std::string_view> activeMods = GetOptions().Mods.GetActiveModList();
sgGameInitInfo.modHash = ComputeModListHash(activeMods);
}

void NetSendLoPri(uint8_t playerId, const std::byte *data, size_t size)
Expand Down
5 changes: 5 additions & 0 deletions Source/multi.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
#pragma once

#include <cstdint>
#include <span>
#include <string>
#include <string_view>
#include <vector>

#include "dvlnet/leaveinfo.hpp"
Expand Down Expand Up @@ -39,6 +41,8 @@ struct GameData {
uint8_t fullQuests;
/** Used to initialise the seed table for dungeon levels so players in multiplayer games generate the same layout */
uint32_t gameSeed[4];
/** FNV-1a hash of active mod list for multiplayer compatibility check */
uint32_t modHash;

void swapLE();
};
Expand Down Expand Up @@ -68,6 +72,7 @@ extern bool IsLoopback;

DVL_API_FOR_TEST std::string DescribeLeaveReason(leaveinfo_t leaveReason);
std::string FormatGameSeed(const uint32_t gameSeed[4]);
uint32_t ComputeModListHash(std::span<const std::string_view> mods);

void InitGameInfo();
void NetSendLoPri(uint8_t playerId, const std::byte *data, size_t size);
Expand Down
44 changes: 44 additions & 0 deletions test/multi_logging_test.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,53 @@
#include <array>
#include <cstdint>
#include <string_view>

#include <gtest/gtest.h>

#include "multi.h"

namespace devilution {

TEST(ComputeModListHash, EmptyListProducesZero)
{
// An empty mod list produces zero (XOR identity with no contributors).
EXPECT_EQ(ComputeModListHash({}), 0U);
}

TEST(ComputeModListHash, Deterministic)
{
const std::array<std::string_view, 2> mods = { "mod-a", "mod-b" };
EXPECT_EQ(ComputeModListHash(mods), ComputeModListHash(mods));
}

TEST(ComputeModListHash, DifferentModsProduceDifferentHashes)
{
const std::array<std::string_view, 1> modsA = { "mod-a" };
const std::array<std::string_view, 1> modsB = { "mod-b" };
EXPECT_NE(ComputeModListHash(modsA), ComputeModListHash(modsB));
}

TEST(ComputeModListHash, OrderDoesNotMatter)
{
const std::array<std::string_view, 2> ab = { "mod-a", "mod-b" };
const std::array<std::string_view, 2> ba = { "mod-b", "mod-a" };
EXPECT_EQ(ComputeModListHash(ab), ComputeModListHash(ba));
}

TEST(ComputeModListHash, DifferentModNamesDifferentHashes)
{
// ["ab", "c"] must not collide with ["a", "bc"].
const std::array<std::string_view, 2> splitFirst = { "ab", "c" };
const std::array<std::string_view, 2> splitSecond = { "a", "bc" };
EXPECT_NE(ComputeModListHash(splitFirst), ComputeModListHash(splitSecond));
}

TEST(ComputeModListHash, NoModsDifferFromSomeMods)
{
const std::array<std::string_view, 1> oneMod = { "any-mod" };
EXPECT_NE(ComputeModListHash({}), ComputeModListHash(oneMod));
}

TEST(MultiplayerLogging, NormalExitReason)
{
EXPECT_EQ("normal exit", DescribeLeaveReason(net::leaveinfo_t::LEAVE_EXIT));
Expand Down
Loading