Browse Source

Merge 4564d12eb5 into 5a08031caf

pull/8479/merge
Yuri Pourre 4 days ago committed by GitHub
parent
commit
705934d011
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      Source/DiabloUI/multi/selgame.cpp
  2. 7
      Source/dvlnet/base_protocol.h
  3. 3
      Source/dvlnet/packet.h
  4. 2
      Source/dvlnet/tcp_client.cpp
  5. 18
      Source/dvlnet/tcp_server.cpp
  6. 20
      Source/multi.cpp
  7. 5
      Source/multi.h
  8. 44
      test/multi_logging_test.cpp

6
Source/DiabloUI/multi/selgame.cpp

@ -94,12 +94,14 @@ bool IsGameCompatible(const GameData &data)
return (data.versionMajor == PROJECT_VERSION_MAJOR return (data.versionMajor == PROJECT_VERSION_MAJOR
&& data.versionMinor == PROJECT_VERSION_MINOR && data.versionMinor == PROJECT_VERSION_MINOR
&& data.versionPatch == PROJECT_VERSION_PATCH && data.versionPatch == PROJECT_VERSION_PATCH
&& data.programid == GAME_ID); && data.programid == GAME_ID
return false; && data.modHash == sgGameInitInfo.modHash);
} }
static std::string GetErrorMessageIncompatibility(const GameData &data) static std::string GetErrorMessageIncompatibility(const GameData &data)
{ {
if (data.modHash != sgGameInitInfo.modHash)
return std::string(_("The host is using a different set of mods."));
if (data.programid != GAME_ID) { if (data.programid != GAME_ID) {
std::string_view gameMode; std::string_view gameMode;
switch (data.programid) { switch (data.programid) {

7
Source/dvlnet/base_protocol.h

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstddef>
#include <memory> #include <memory>
#include <set> #include <set>
#include <string> #include <string>
@ -318,6 +319,12 @@ void base_protocol<P>::recv()
template <class P> template <class P>
tl::expected<void, PacketError> base_protocol<P>::handle_join_request(packet &inPkt, endpoint_t sender) 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; plr_t i;
for (i = 0; i < Players.size(); ++i) { for (i = 0; i < Players.size(); ++i) {
Peer &peer = peers[i]; Peer &peer = peers[i];

3
Source/dvlnet/packet.h

@ -67,7 +67,8 @@ public:
enum class ErrorCode : uint8_t { enum class ErrorCode : uint8_t {
None, None,
EncryptionFailed, EncryptionFailed,
DecryptionFailed DecryptionFailed,
ModMismatch
}; };
PacketError() PacketError()

2
Source/dvlnet/tcp_client.cpp

@ -214,6 +214,8 @@ void tcp_client::HandleTcpErrorCode()
PacketError::ErrorCode code = static_cast<PacketError::ErrorCode>(pktData[0]); PacketError::ErrorCode code = static_cast<PacketError::ErrorCode>(pktData[0]);
if (code == PacketError::ErrorCode::DecryptionFailed) if (code == PacketError::ErrorCode::DecryptionFailed)
RaiseIoHandlerError(_("Server failed to decrypt your packet. Check if you typed the password correctly.")); 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 else
RaiseIoHandlerError(fmt::format("Unknown error code received from server: {:#04x}", pktData[0])); RaiseIoHandlerError(fmt::format("Unknown error code received from server: {:#04x}", pktData[0]));
} }

18
Source/dvlnet/tcp_server.cpp

@ -1,6 +1,7 @@
#include "dvlnet/tcp_server.h" #include "dvlnet/tcp_server.h"
#include <chrono> #include <chrono>
#include <cstddef>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <utility> #include <utility>
@ -126,11 +127,20 @@ tl::expected<void, PacketError> tcp_server::HandleReceiveNewPlayer(const scc &co
if (newplr == PLR_BROADCAST) if (newplr == PLR_BROADCAST)
return tl::make_unexpected(ServerError()); 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()) { if (Empty()) {
tl::expected<const buffer_t *, PacketError> pktInfo = inPkt.Info(); game_init_info = joinerInfo;
if (!pktInfo.has_value()) } else if (joinerInfo.size() == sizeof(GameData) && game_init_info.size() == sizeof(GameData)) {
return tl::make_unexpected(pktInfo.error()); constexpr size_t ModHashOffset = offsetof(GameData, modHash);
game_init_info = **pktInfo; 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++) { for (plr_t player = 0; player < Players.size(); player++) {

20
Source/multi.cpp

@ -8,6 +8,7 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <span>
#include <string_view> #include <string_view>
#ifdef USE_SDL3 #ifdef USE_SDL3
@ -95,6 +96,7 @@ void GameData::swapLE()
gameSeed[1] = Swap32LE(gameSeed[1]); gameSeed[1] = Swap32LE(gameSeed[1]);
gameSeed[2] = Swap32LE(gameSeed[2]); gameSeed[2] = Swap32LE(gameSeed[2]);
gameSeed[3] = Swap32LE(gameSeed[3]); gameSeed[3] = Swap32LE(gameSeed[3]);
modHash = Swap32LE(modHash);
} }
namespace { namespace {
@ -554,6 +556,22 @@ std::string FormatGameSeed(const uint32_t gameSeed[4])
gameSeed[0], gameSeed[1], gameSeed[2], gameSeed[3]); 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() void InitGameInfo()
{ {
const xoshiro128plusplus gameGenerator = ReserveSeedSequence(); const xoshiro128plusplus gameGenerator = ReserveSeedSequence();
@ -571,6 +589,8 @@ void InitGameInfo()
sgGameInitInfo.bCowQuest = *options.Gameplay.cowQuest ? 1 : 0; sgGameInitInfo.bCowQuest = *options.Gameplay.cowQuest ? 1 : 0;
sgGameInitInfo.bFriendlyFire = *options.Gameplay.friendlyFire ? 1 : 0; sgGameInitInfo.bFriendlyFire = *options.Gameplay.friendlyFire ? 1 : 0;
sgGameInitInfo.fullQuests = (!gbIsMultiplayer || *options.Gameplay.multiplayerFullQuests) ? 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) void NetSendLoPri(uint8_t playerId, const std::byte *data, size_t size)

5
Source/multi.h

@ -6,7 +6,9 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <span>
#include <string> #include <string>
#include <string_view>
#include <vector> #include <vector>
#include "dvlnet/leaveinfo.hpp" #include "dvlnet/leaveinfo.hpp"
@ -39,6 +41,8 @@ struct GameData {
uint8_t fullQuests; uint8_t fullQuests;
/** Used to initialise the seed table for dungeon levels so players in multiplayer games generate the same layout */ /** Used to initialise the seed table for dungeon levels so players in multiplayer games generate the same layout */
uint32_t gameSeed[4]; uint32_t gameSeed[4];
/** FNV-1a hash of active mod list for multiplayer compatibility check */
uint32_t modHash;
void swapLE(); void swapLE();
}; };
@ -68,6 +72,7 @@ extern bool IsLoopback;
DVL_API_FOR_TEST std::string DescribeLeaveReason(leaveinfo_t leaveReason); DVL_API_FOR_TEST std::string DescribeLeaveReason(leaveinfo_t leaveReason);
std::string FormatGameSeed(const uint32_t gameSeed[4]); std::string FormatGameSeed(const uint32_t gameSeed[4]);
uint32_t ComputeModListHash(std::span<const std::string_view> mods);
void InitGameInfo(); void InitGameInfo();
void NetSendLoPri(uint8_t playerId, const std::byte *data, size_t size); void NetSendLoPri(uint8_t playerId, const std::byte *data, size_t size);

44
test/multi_logging_test.cpp

@ -1,9 +1,53 @@
#include <array>
#include <cstdint>
#include <string_view>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include "multi.h" #include "multi.h"
namespace devilution { 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) TEST(MultiplayerLogging, NormalExitReason)
{ {
EXPECT_EQ("normal exit", DescribeLeaveReason(net::leaveinfo_t::LEAVE_EXIT)); EXPECT_EQ("normal exit", DescribeLeaveReason(net::leaveinfo_t::LEAVE_EXIT));

Loading…
Cancel
Save