Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Initial blind lobby / game support #4175

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions doc/CmdInterface.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,8 @@ If state of interface buffer is unknown and/or corrupted, interface can send a f
* `chat bcast <message [^\n]>`\
Send system level message to the room from stdin.

* `set host ready <0|1>`\
Sets the host ready state to either not-ready (0) or ready (1).

* `shutdown now`\
Trigger graceful shutdown of the game regardless of state.
9 changes: 8 additions & 1 deletion doc/hosting/AutohostConfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ The `challenge` object defines the game parameters for a multiplayer game.
* `techLevel` sets the starting technology level. `1` for level 1 (wheel), `2` for level 2 (water mill), `3` for level 3 (chip), `4` for level 4 (computer).
* `spectatorHost` when `true` or `1`, the host will spectate the game. When `false` or `0`, the host will play the game.
* `openSpectatorSlots` defines how much spectator slots are opened (one more is opened for the host when spectating).
* `blindMode` configures blind lobby / game mode. Available values are:
* `"none"`: blind modes disabled (the default)
* `"blind_lobby"`: Players' true identities are hidden from everyone except the host - **until the game _starts_**
* `"blind_lobby_simple_lobby"`: Same as `blind_lobby`, but with the addition of "simple lobby" mode (players will be placed in a waiting room where they can't see the list of players until the game starts)
* `"blind_game"`: Players' true identities are hidden from everyone except the host - **until the game _ends_**
* `"blind_game_simple_lobby"`: Same as `blind_game`, but with the addition of "simple lobby" mode (players will be placed in a waiting room where they can't see the list of players until the game starts)
* `allowPositionChange` is deprecated, use the `locked` object instead.

## the `locked` object
Expand Down Expand Up @@ -48,7 +54,7 @@ Each player slot can be customized, starting from 0. The first slot will be defi

## Sample file

```
```json
{
"locked": {
"power": false,
Expand All @@ -71,6 +77,7 @@ Each player slot can be customized, starting from 0. The first slot will be defi
"techLevel": 1,
"spectatorHost": true,
"openSpectatorSlots": 2,
"blindMode": "none",
"allowPositionChange": true
},
"player_0": {
Expand Down
6 changes: 6 additions & 0 deletions doc/hosting/DedicatedHost.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ You can provide more arguments to fine-tune your environment.
* `--autorating=<host>` overrides the autorating url. See `AutoratingServer.md` for details about the autorating server.


#### Advanced usage

* `--enablecmdinterface=<stdin|unixsocket:path>` enables the command interface. See [/doc/CmdInterface.md](/doc/CmdInterface.md)
* `--autohost-not-ready` starts the host (autohost) as not ready, even if it's a spectator host. Should usually be combined with usage of the cmdinterface to trigger host ready via the `set host ready 1` command (or the game will never start!)


### Checking your firewall

If your server starts but nobody can join, check that your firewall is accepting incoming TCP connections on the given port.
Expand Down
2 changes: 1 addition & 1 deletion lib/framework/crc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ EcKey::Key EcKey::toBytes(Privacy privacy) const
{
if (empty())
{
debug(LOG_INFO, "No key");
debug(LOG_WZ, "No key");
return Key();
}
assert(EC_KEY_CAST(vKey) != nullptr);
Expand Down
242 changes: 162 additions & 80 deletions lib/netplay/netplay.cpp

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion lib/netplay/netplay.h
Original file line number Diff line number Diff line change
Expand Up @@ -465,11 +465,13 @@ void NETsetDefaultMPHostFreeChatPreference(bool enabled);
bool NETgetDefaultMPHostFreeChatPreference();
void NETsetEnableTCPNoDelay(bool enabled);
bool NETgetEnableTCPNoDelay();
uint32_t NETgetJoinConnectionNETPINGChallengeSize();
uint32_t NETgetJoinConnectionNETPINGChallengeFromHostSize();
uint32_t NETgetJoinConnectionNETPINGChallengeFromClientSize();

void NETsetGamePassword(const char *password);
void NETBroadcastPlayerInfo(uint32_t index);
void NETBroadcastTwoPlayerInfo(uint32_t index1, uint32_t index2);
void NETSendAllPlayerInfoTo(unsigned to);
bool NETisCorrectVersion(uint32_t game_version_major, uint32_t game_version_minor);
uint32_t NETGetMajorVersion();
uint32_t NETGetMinorVersion();
Expand Down
68 changes: 46 additions & 22 deletions lib/netplay/netreplay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ static const size_t MaxReplayBufferSize = 2 * 1024 * 1024;

typedef std::vector<uint8_t> SerializedNetMessagesBuffer;
static moodycamel::BlockingReaderWriterQueue<SerializedNetMessagesBuffer> serializedBufferWriteQueue(256);
static nlohmann::json queuedSaveSettings;
static SerializedNetMessagesBuffer latestWriteBuffer;
static size_t minBufferSizeToQueue = DefaultReplayBufferSize;
static WZ_THREAD *saveThread = nullptr;
Expand All @@ -83,6 +84,37 @@ static int replaySaveThreadFunc(void *data)
return 0;
}

static bool NETreplaySaveWritePreamble(const nlohmann::json& settings, ReplayOptionsHandler const &optionsHandler)
{
if (!replaySaveHandle)
{
return false;
}

auto data = settings.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace);
PHYSFS_writeUBE32(replaySaveHandle, data.size());
WZ_PHYSFS_writeBytes(replaySaveHandle, data.data(), data.size());

// Save extra map data (if present)
ReplayOptionsHandler::EmbeddedMapData embeddedMapData;
if (!optionsHandler.saveMap(embeddedMapData))
{
// Failed to save map data - just empty it out for now
embeddedMapData.mapBinaryData.clear();
}
PHYSFS_writeUBE32(replaySaveHandle, embeddedMapData.dataVersion);
#if SIZE_MAX > UINT32_MAX
ASSERT_OR_RETURN(false, embeddedMapData.mapBinaryData.size() <= static_cast<size_t>(std::numeric_limits<uint32_t>::max()), "Embedded map data is way too big");
#endif
PHYSFS_writeUBE32(replaySaveHandle, static_cast<uint32_t>(embeddedMapData.mapBinaryData.size()));
if (!embeddedMapData.mapBinaryData.empty())
{
WZ_PHYSFS_writeBytes(replaySaveHandle, embeddedMapData.mapBinaryData.data(), static_cast<uint32_t>(embeddedMapData.mapBinaryData.size()));
}

return true;
}

bool NETreplaySaveStart(std::string const& subdir, ReplayOptionsHandler const &optionsHandler, int maxReplaysSaved, bool appendPlayerToFilename)
{
if (NETisReplay())
Expand Down Expand Up @@ -147,27 +179,6 @@ bool NETreplaySaveStart(std::string const& subdir, ReplayOptionsHandler const &o
optionsHandler.saveOptions(gameOptions);
settings["gameOptions"] = gameOptions;

auto data = settings.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace);
PHYSFS_writeUBE32(replaySaveHandle, data.size());
WZ_PHYSFS_writeBytes(replaySaveHandle, data.data(), data.size());

// Save extra map data (if present)
ReplayOptionsHandler::EmbeddedMapData embeddedMapData;
if (!optionsHandler.saveMap(embeddedMapData))
{
// Failed to save map data - just empty it out for now
embeddedMapData.mapBinaryData.clear();
}
PHYSFS_writeUBE32(replaySaveHandle, embeddedMapData.dataVersion);
#if SIZE_MAX > UINT32_MAX
ASSERT_OR_RETURN(false, embeddedMapData.mapBinaryData.size() <= static_cast<size_t>(std::numeric_limits<uint32_t>::max()), "Embedded map data is way too big");
#endif
PHYSFS_writeUBE32(replaySaveHandle, static_cast<uint32_t>(embeddedMapData.mapBinaryData.size()));
if (!embeddedMapData.mapBinaryData.empty())
{
WZ_PHYSFS_writeBytes(replaySaveHandle, embeddedMapData.mapBinaryData.data(), static_cast<uint32_t>(embeddedMapData.mapBinaryData.size()));
}

// determine best buffer size
size_t desiredBufferSize = optionsHandler.desiredBufferSize();
if (desiredBufferSize == 0)
Expand All @@ -191,19 +202,26 @@ bool NETreplaySaveStart(std::string const& subdir, ReplayOptionsHandler const &o
latestWriteBuffer.reserve(minBufferSizeToQueue);
if (desiredBufferSize != std::numeric_limits<size_t>::max())
{
// Write the preamble immediately (settings, etc)
NETreplaySaveWritePreamble(settings, optionsHandler);

// use a background thread
saveThread = wzThreadCreate(replaySaveThreadFunc, replaySaveHandle, "replaySaveThread");
wzThreadStart(saveThread);
}
else
{
// Do not immediately write settings out - instead, queue them for later writing
queuedSaveSettings = std::move(settings);

// don't use a background thread
saveThread = nullptr;
}

return true;
}

bool NETreplaySaveStop()
bool NETreplaySaveStop(ReplayOptionsHandler const &optionsHandler)
{
if (!replaySaveHandle)
{
Expand Down Expand Up @@ -233,6 +251,12 @@ bool NETreplaySaveStop()
}
else
{
// update the queued settings (ex. might have revealed player identities in a blind game)
optionsHandler.optionsUpdatePlayerInfo(queuedSaveSettings.at("gameOptions"));

// write the preamble
NETreplaySaveWritePreamble(queuedSaveSettings, optionsHandler);

// do the writing now on the main thread
replaySaveThreadFunc(replaySaveHandle);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/netplay/netreplay.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@


bool NETreplaySaveStart(std::string const& subdir, ReplayOptionsHandler const &optionsHandler, int maxReplaysSaved, bool appendPlayerToFilename = false);
bool NETreplaySaveStop();
bool NETreplaySaveStop(ReplayOptionsHandler const &optionsHandler);
void NETreplaySaveNetMessage(NetMessage const *message, uint8_t player);

bool NETreplayLoadStart(std::string const &filename, ReplayOptionsHandler& optionsHandler, uint32_t& output_replayFormatVer);
Expand Down
19 changes: 19 additions & 0 deletions lib/netplay/nettypes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,25 @@ void NETnetMessage(NetMessage const **msg)
}
}

void NETbytesOutputToVector(const std::vector<uint8_t> &data, std::vector<uint8_t>& output)
{
// same logic as using NETbytes() for a write, except written to the `output` vector

// same as queueAuto(uint32_t) - write the length
uint32_t dataSizeU32 = static_cast<uint32_t>(data.size());
uint32_t v = dataSizeU32;
bool moreBytes = true;
for (int n = 0; moreBytes; ++n)
{
uint8_t b;
moreBytes = encode_uint32_t(b, v, n);
output.push_back(b);
}

// write all the data bytes
output.insert(output.end(), data.begin(), data.end());
}

ReplayOptionsHandler::~ReplayOptionsHandler() { }

// TODO Call this function somewhere.
Expand Down
3 changes: 3 additions & 0 deletions lib/netplay/nettypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ static inline void NETauto(T (&ar)[N])

void NETnetMessage(NetMessage const **message); ///< If decoding, must delete the NETMESSAGE.

void NETbytesOutputToVector(const std::vector<uint8_t> &data, std::vector<uint8_t>& output);

#include <nlohmann/json_fwd.hpp>

class ReplayOptionsHandler
Expand All @@ -225,6 +227,7 @@ class ReplayOptionsHandler
public:
virtual bool saveOptions(nlohmann::json& object) const = 0;
virtual bool saveMap(EmbeddedMapData& mapData) const = 0;
virtual bool optionsUpdatePlayerInfo(nlohmann::json& object) const = 0;
virtual bool restoreOptions(const nlohmann::json& object, EmbeddedMapData&& embeddedMapData, uint32_t replay_netcodeMajor, uint32_t replay_netcodeMinor) = 0;
virtual size_t desiredBufferSize() const = 0;
virtual size_t maximumEmbeddedMapBufferSize() const = 0;
Expand Down
4 changes: 4 additions & 0 deletions lib/widget/editbox.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,10 @@ void W_EDITBOX::display(int xOffset, int yOffset)
{
displayCache.wzDisplayedText.setText(displayedText, FontID);
}
if (state & WEDBS_DISABLE)
{
displayedTextColor = WZCOL_TEXT_DARK;
}

int lineSize = displayCache.wzDisplayedText.lineSize();
int aboveBase = displayCache.wzDisplayedText.aboveBase();
Expand Down
7 changes: 7 additions & 0 deletions lib/wzmaplib/src/map_script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-function-type"
#endif
#if defined(__clang__) && defined(__clang_major__) && __clang_major__ >= 19
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wcast-function-type-mismatch"
#endif
#if defined(_MSC_VER)
__pragma(warning( push ))
__pragma(warning( disable : 4191 )) // disable "warning C4191: 'type cast': unsafe conversion from 'JSCFunctionMagic (__cdecl *)' to 'JSCFunction (__cdecl *)'"
Expand All @@ -44,6 +48,9 @@ __pragma(warning( disable : 4191 )) // disable "warning C4191: 'type cast': unsa
#if defined(_MSC_VER)
__pragma(warning( pop ))
#endif
#if defined(__clang__) && defined(__clang_major__) && __clang_major__ >= 19
#pragma clang diagnostic pop
#endif
#if !defined(__clang__) && defined(__GNUC__) && __GNUC__ >= 8
#pragma GCC diagnostic pop
#endif
Expand Down
4 changes: 2 additions & 2 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ if(ENABLE_NLS)
find_package (Intl REQUIRED)
endif()

file(GLOB HEADERS "*.h" "3rdparty/*.h" "titleui/*.h" "hci/*.h" "input/*.h" "screens/*.h")
file(GLOB SRC "*.cpp" "3rdparty/*.cpp" "titleui/*.cpp" "hci/*.cpp" "input/*.cpp" "screens/*.cpp")
file(GLOB HEADERS "*.h" "3rdparty/*.h" "titleui/*.h" "titleui/*/*.h" "hci/*.h" "input/*.h" "screens/*.h")
file(GLOB SRC "*.cpp" "3rdparty/*.cpp" "titleui/*.cpp" "titleui/*/*.cpp" "hci/*.cpp" "input/*.cpp" "screens/*.cpp")

set(_additionalSourceFiles)
if(CMAKE_SYSTEM_NAME MATCHES "Windows")
Expand Down
6 changes: 6 additions & 0 deletions src/clparse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ typedef enum
CLI_ALLOW_VULKAN_IMPLICIT_LAYERS,
CLI_HOST_CHAT_CONFIG,
CLI_HOST_ASYNC_JOIN_APPROVAL,
CLI_AUTOHOST_START_NOT_READY,
#if defined(__EMSCRIPTEN__)
CLI_VIDEOURL,
#endif
Expand Down Expand Up @@ -457,6 +458,7 @@ static const struct poptOption *getOptionsTable()
{ "allow-vulkan-implicit-layers", POPT_ARG_NONE, CLI_ALLOW_VULKAN_IMPLICIT_LAYERS, N_("Allow Vulkan implicit layers (that may be default-disabled due to potential crashes or bugs)"), nullptr },
{ "host-chat-config", POPT_ARG_STRING, CLI_HOST_CHAT_CONFIG, N_("Set the default hosting chat configuration / permissions"), "[allow,quickchat]" },
{ "async-join-approve", POPT_ARG_NONE, CLI_HOST_ASYNC_JOIN_APPROVAL, N_("Enable async join approval (for connecting clients)"), nullptr },
{ "autohost-not-ready", POPT_ARG_NONE, CLI_AUTOHOST_START_NOT_READY, N_("Starts the host (autohost) as not ready, even if it's a spectator host"), nullptr },
#if defined(__EMSCRIPTEN__)
{ "videourl", POPT_ARG_STRING, CLI_VIDEOURL, N_("Base URL for on-demand video downloads"), N_("Base video URL") },
#endif
Expand Down Expand Up @@ -1345,6 +1347,10 @@ bool ParseCommandLine(int argc, const char * const *argv)
NETsetAsyncJoinApprovalRequired(true);
break;

case CLI_AUTOHOST_START_NOT_READY:
setHostLaunchStartNotReady(true);
break;

#if defined(__EMSCRIPTEN__)
case CLI_VIDEOURL:
token = poptGetOptArg(poptCon);
Expand Down
1 change: 1 addition & 0 deletions src/configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,7 @@ bool reloadMPConfig()
game.inactivityMinutes = war_getMPInactivityMinutes();
game.gameTimeLimitMinutes = war_getMPGameTimeLimitMinutes();
game.playerLeaveMode = war_getMPPlayerLeaveMode();
game.blindMode = BLIND_MODE::NONE;

// restore group menus enabled setting (as tutorial may override it)
setGroupButtonEnabled(war_getGroupsMenuEnabled());
Expand Down
5 changes: 5 additions & 0 deletions src/game.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4558,6 +4558,10 @@ static bool loadMainFile(const std::string &fileName)
{
game.playerLeaveMode = static_cast<PLAYER_LEAVE_MODE>(save.value("playerLeaveMode").toInt());
}
if (save.contains("blindMode"))
{
game.blindMode = static_cast<BLIND_MODE>(save.value("blindMode").toInt());
}
if (save.contains("multiplayer"))
{
bMultiPlayer = save.value("multiplayer").toBool();
Expand Down Expand Up @@ -4800,6 +4804,7 @@ static bool writeMainFile(const std::string &fileName, SDWORD saveType)
save.setValue("inactivityMinutes", game.inactivityMinutes);
save.setValue("gameTimeLimitMinutes", game.gameTimeLimitMinutes);
save.setValue("playerLeaveMode", game.playerLeaveMode);
save.setValue("blindMode", game.blindMode);
save.setValue("tweakOptions", getCamTweakOptions());

save.beginArray("scriptSetPlayerDataStrings");
Expand Down
4 changes: 2 additions & 2 deletions src/gamehistorylogger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -333,12 +333,12 @@ void GameStoryLogger::logStartGame()
for (int i = 0; i < game.maxPlayers; i++)
{
FixedPlayerAttributes playerAttrib;
playerAttrib.name = NetPlay.players[i].name;
playerAttrib.name = (strlen(NetPlay.players[i].name) == 0) ? "" : getPlayerName(i);
playerAttrib.position = NetPlay.players[i].position;
playerAttrib.team = NetPlay.players[i].team;
playerAttrib.colour = NetPlay.players[i].colour;
playerAttrib.faction = NetPlay.players[i].faction;
playerAttrib.publicKey = base64Encode(getMultiStats(i).identity.toBytes(EcKey::Public));
playerAttrib.publicKey = base64Encode(getOutputPlayerIdentity(i).toBytes(EcKey::Public));

startingPlayerAttributes.push_back(playerAttrib);
}
Expand Down
4 changes: 2 additions & 2 deletions src/hci/quickchat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2447,8 +2447,8 @@ namespace INTERNAL_ADMIN_ACTION_NOTICE {
return std::string();
}

const char* responsiblePlayerName = NetPlay.players[responsiblePlayerIdx].name;
const char* targetPlayerName = NetPlay.players[targetPlayerIdx].name;
const char* responsiblePlayerName = getPlayerName(responsiblePlayerIdx);
const char* targetPlayerName = getPlayerName(targetPlayerIdx);

const char* responsiblePlayerType = _("Player");
if (responsiblePlayerIdx == NetPlay.hostPlayer)
Expand Down
2 changes: 1 addition & 1 deletion src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2002,7 +2002,7 @@ bool stageThreeShutDown()
{
debug(LOG_WZ, "== stageThreeShutDown ==");

setHostLaunch(HostLaunch::Normal);
resetHostLaunch();

removeSpotters();

Expand Down
3 changes: 2 additions & 1 deletion src/loop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -501,9 +501,10 @@ static void gameStateUpdate()
NetPlay.players[0].allocated, NetPlay.players[1].allocated, NetPlay.players[2].allocated, NetPlay.players[3].allocated, NetPlay.players[4].allocated, NetPlay.players[5].allocated, NetPlay.players[6].allocated, NetPlay.players[7].allocated, NetPlay.players[8].allocated, NetPlay.players[9].allocated,
NetPlay.players[0].position, NetPlay.players[1].position, NetPlay.players[2].position, NetPlay.players[3].position, NetPlay.players[4].position, NetPlay.players[5].position, NetPlay.players[6].position, NetPlay.players[7].position, NetPlay.players[8].position, NetPlay.players[9].position
);
bool overrideHandleClientBlindNames = (game.blindMode >= BLIND_MODE::BLIND_GAME) && (NetPlay.isHost || NETisReplay()) && !ingame.endTime.has_value();
for (unsigned n = 0; n < MAX_PLAYERS; ++n)
{
syncDebug("Player %d = \"%s\"", n, NetPlay.players[n].name);
syncDebug("Player %d = \"%s\"", n, (!overrideHandleClientBlindNames) ? NetPlay.players[n].name : getPlayerGenericName(n));
}

// Add version string to desynch logs. Different version strings will not trigger a desynch dump per se, due to the syncDebug{Get, Set}Crc guard.
Expand Down
3 changes: 2 additions & 1 deletion src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1074,7 +1074,8 @@ static void stopGameLoop()
{
clearInfoMessages(); // clear CONPRINTF messages before each new game/mission

NETreplaySaveStop();
WZGameReplayOptionsHandler replayOptions;
NETreplaySaveStop(replayOptions);
NETshutdownReplay();

if (gameLoopStatus != GAMECODE_NEWLEVEL)
Expand Down
Loading
Loading