From f405ed137c9ccfdb57dd7211f6d071750ef1fc33 Mon Sep 17 00:00:00 2001 From: Karamel Date: Thu, 22 Feb 2024 20:35:01 +0100 Subject: [PATCH 1/3] Add motd to autohost and challenge --- doc/hosting/AutohostConfig.md | 7 ++++++- src/multiint.cpp | 35 +++++++++++++++++++++++++++++++++++ src/multiint.h | 1 + src/multijoin.cpp | 1 + 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/doc/hosting/AutohostConfig.md b/doc/hosting/AutohostConfig.md index 8519bb73292..7100d071be6 100644 --- a/doc/hosting/AutohostConfig.md +++ b/doc/hosting/AutohostConfig.md @@ -45,6 +45,10 @@ Each player slot can be customized, starting from 0. The first slot will be defi * `difficulty` sets the difficulty for an AI. It can be one of `Easy`, `Medium`, `Hard` or `Insane`. * `name` sets a custom name for the AI. +## Message of the Day + +The `motd` is displayed in the chat box when a player joins the game. It is optional and will be truncated if it exceeds 256 characters. + ## Sample file ``` @@ -83,6 +87,7 @@ Each player slot can be customized, starting from 0. The first slot will be defi }, "player_3": { "team": 1 - } + }, + "motd": "Good Luck, Have Fun!" } ``` diff --git a/src/multiint.cpp b/src/multiint.cpp index 238a8b0e818..78dcf166222 100644 --- a/src/multiint.cpp +++ b/src/multiint.cpp @@ -284,6 +284,7 @@ static struct } locked; static bool spectatorHost = false; static uint16_t defaultOpenSpectatorSlots = 0; +static WzString motd = WzString(); struct AIDATA { @@ -6036,6 +6037,23 @@ static void resetPlayerConfiguration(const bool bShouldResetLocal = false) } } +static void loadMapMessageOfTheDay(WzConfig& ini) +{ + if (ini.contains("motd")) + { + motd = ini.value("motd").toWzString(); + if (motd.length() > MAX_CONSOLE_STRING_LENGTH) + { + motd.truncate(MAX_CONSOLE_STRING_LENGTH); + debug(LOG_WARNING, "MOTD was truncated to fit into %d characters.", MAX_CONSOLE_STRING_LENGTH); + } + } + else + { + motd = WzString(""); + } +} + /** * Loads challenge and player configurations from level/autohost/test .json-files. */ @@ -6081,6 +6099,7 @@ static void loadMapChallengeAndPlayerSettings(bool forceLoadPlayers = false) WzConfig ini(ininame, WzConfig::ReadOnly); loadMapChallengeSettings(ini); + loadMapMessageOfTheDay(ini); /* Do not load player settings if we are already hosting an online match */ if (!bIsOnline || forceLoadPlayers) @@ -7665,6 +7684,11 @@ void WzMultiplayerOptionsTitleUI::screenSizeDidChange(unsigned int oldWidth, uns static void printHostHelpMessagesToConsole() { char buf[512] = { '\0' }; + if (!motd.isEmpty()) + { + ssprintf(buf, motd.toUtf8().c_str()); + displayRoomNotifyMessage(buf); + } if (challengeActive) { ssprintf(buf, "%s", _("Hit the ready box to begin your challenge!")); @@ -8614,6 +8638,17 @@ void sendRoomSystemMessageToSingleReceiver(char const *text, uint32_t receiver, message.enqueue(NETnetQueue(receiver)); } +void sendRoomMotdToSingleReceiver(uint32_t receiver) +{ + if (motd.isEmpty()) + { + return; + } + ASSERT_OR_RETURN(, isHumanPlayer(receiver), "Invalid receiver: %" PRIu32 "", receiver); + NetworkTextMessage message(NOTIFY_MESSAGE, motd.toUtf8().c_str()); + message.enqueue(NETnetQueue(receiver)); +} + static void sendRoomChatMessage(char const *text, bool skipLocalDisplay) { NetworkTextMessage message(selectedPlayer, text); diff --git a/src/multiint.h b/src/multiint.h index 605ec4f6be3..4b52bab0c8f 100644 --- a/src/multiint.h +++ b/src/multiint.h @@ -99,6 +99,7 @@ WzString formatGameName(WzString name); void sendRoomSystemMessage(char const *text); void sendRoomNotifyMessage(char const *text); void sendRoomSystemMessageToSingleReceiver(char const *text, uint32_t receiver, bool skipLocalDisplay = false); +void sendRoomMotdToSingleReceiver(uint32_t receiver); void displayRoomSystemMessage(char const *text); void displayRoomNotifyMessage(char const *text); void displayLobbyDisabledNotification(); diff --git a/src/multijoin.cpp b/src/multijoin.cpp index 4025a160a0b..2041c1e89b5 100644 --- a/src/multijoin.cpp +++ b/src/multijoin.cpp @@ -575,6 +575,7 @@ bool MultiPlayerJoin(UDWORD playerIndex) } } } + sendRoomMotdToSingleReceiver(playerIndex); if (lobby_slashcommands_enabled()) { // Inform the new player that this lobby has slash commands enabled. From 175db2522e75563c7f98e19e1dedbf211acb0321 Mon Sep 17 00:00:00 2001 From: Karamel Date: Thu, 22 Feb 2024 22:29:21 +0100 Subject: [PATCH 2/3] Fix format string error --- src/multiint.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multiint.cpp b/src/multiint.cpp index 78dcf166222..cab9a159b01 100644 --- a/src/multiint.cpp +++ b/src/multiint.cpp @@ -7686,7 +7686,7 @@ static void printHostHelpMessagesToConsole() char buf[512] = { '\0' }; if (!motd.isEmpty()) { - ssprintf(buf, motd.toUtf8().c_str()); + ssprintf(buf, "%s", motd.toUtf8().c_str()); displayRoomNotifyMessage(buf); } if (challengeActive) From a6f49cc73957f0b79f5c4023ad44008c1d1c26a8 Mon Sep 17 00:00:00 2001 From: Karamel Date: Sat, 23 Mar 2024 10:49:32 +0100 Subject: [PATCH 3/3] Multilingual motd --- doc/hosting/AutohostConfig.md | 16 +++ lib/framework/i18n.h | 3 + lib/framework/wzi18nstring.cpp | 182 +++++++++++++++++++++++++++++++++ lib/framework/wzi18nstring.h | 61 +++++++++++ lib/netplay/netplay.cpp | 1 + lib/netplay/netplay.h | 1 + src/multiint.cpp | 31 ++++-- src/multiplay.cpp | 50 +++++++++ src/multiplay.h | 15 +++ 9 files changed, 349 insertions(+), 11 deletions(-) create mode 100644 lib/framework/wzi18nstring.cpp create mode 100644 lib/framework/wzi18nstring.h diff --git a/doc/hosting/AutohostConfig.md b/doc/hosting/AutohostConfig.md index 7100d071be6..c1840fcb38b 100644 --- a/doc/hosting/AutohostConfig.md +++ b/doc/hosting/AutohostConfig.md @@ -49,6 +49,22 @@ Each player slot can be customized, starting from 0. The first slot will be defi The `motd` is displayed in the chat box when a player joins the game. It is optional and will be truncated if it exceeds 256 characters. +It can be a single text entry or an object defining multiple messages in various languages. The default language will be `en` (English) and it must be provided. The default language can be changed by providing a `default` entry. + +``` +"motd": "Hello world" +``` +or +``` +"motd": { + "default": "de", + "de": "Hallo Welt", + "en": "Hello world", + "fr": "Bonjour le monde", + "ru": "привет мир" +} +``` + ## Sample file ``` diff --git a/lib/framework/i18n.h b/lib/framework/i18n.h index bf3adab1d30..8256373b0c2 100644 --- a/lib/framework/i18n.h +++ b/lib/framework/i18n.h @@ -53,6 +53,9 @@ // Make xgettext recognize the context #define NP_(Context, String) gettext_noop(String) +#define MAX_LOCALE_CODE_LENGTH (5) +#define DEFAULT_LOCALE "en" + WZ_DECL_PURE const char *getLanguage(); WZ_DECL_PURE const char *getLanguageName(); WZ_DECL_NONNULL(1) bool setLanguage(const char *name); diff --git a/lib/framework/wzi18nstring.cpp b/lib/framework/wzi18nstring.cpp new file mode 100644 index 00000000000..26a19c3f447 --- /dev/null +++ b/lib/framework/wzi18nstring.cpp @@ -0,0 +1,182 @@ +/* + * This file is part of Warzone 2100. + * Copyright (C) 2024 Warzone 2100 Project + * + * Warzone 2100 is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Warzone 2100 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Warzone 2100; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "wzi18nstring.h" + +#include "i18n.h" +#include "wzglobal.h" + +WzI18nString::WzI18nString() +{ + _defaultLocale = DEFAULT_LOCALE; + _strings[_defaultLocale] = WzString(); +} + + +WzI18nString::WzI18nString(WzString string) +{ + _defaultLocale = DEFAULT_LOCALE; + _strings[_defaultLocale] = string; +} + +WzI18nString::WzI18nString(const WzI18nString& other) +{ + _defaultLocale = other._defaultLocale; + for (const auto &locale : other.listLocales()) + { + _strings[std::string(locale)] = WzString(other.getLocaleString(locale)); + } +} + +WzI18nString::WzI18nString(WzConfig &ini, const WzString &key, const int maxChars) +{ + if (!ini.contains(key)) + { + reset(); + return; + } + json_variant value = ini.value(key); + if (value.jsonValue().is_string()) + { + WzString string = value.toWzString(); + if (string.length() > maxChars) + { + string.truncate(maxChars); + debug(LOG_WARNING, "%s: value for %s was truncated to fit into %d characters.", ini.fileName().toUtf8().c_str(), key.toUtf8().c_str(), maxChars); + } + _defaultLocale = DEFAULT_LOCALE; + _strings[_defaultLocale] = string; + return; + } + if (ini.beginGroup(key)) + { + for (auto locale : getLocales()) + { + std::string localeCode = std::string(locale.code); + WzString string = ini.string(WzString::fromUtf8(localeCode)); + if (string.length() > maxChars) + { + string.truncate(maxChars); + debug(LOG_WARNING, "%s: value for %s (%s) was truncated to fit into %d characters.", ini.fileName().toUtf8().c_str(), key.toUtf8().c_str(), localeCode.c_str(), maxChars); + } + if (!string.isEmpty()) + { + _strings[localeCode] = string; + } + } + _defaultLocale = DEFAULT_LOCALE; + WzString altDefault = ini.string(WzString::fromUtf8("default")); + if (!altDefault.isEmpty()) + { + _defaultLocale = altDefault.toUtf8(); + } + if (!this->contains(_defaultLocale)) + { + debug(LOG_ERROR, "%s: no entry for %s in default language %s.", ini.fileName().toUtf8().c_str(), key.toUtf8().c_str(), _defaultLocale.c_str()); + reset(); + } + ini.endGroup(); + } +} + +bool WzI18nString::contains(std::string localeCode) const +{ + return _strings.find(localeCode) != _strings.end(); +} + +bool WzI18nString::isEmpty() const +{ + return this->getDefaultString().isEmpty(); +} + +const std::string& WzI18nString::getDefaultLocaleCode() const +{ + return _defaultLocale; +} + +const WzString& WzI18nString::getDefaultString() const +{ + return _strings.find(_defaultLocale)->second; +} + +const WzString& WzI18nString::getLocaleString() const +{ + return this->getLocaleString(getLanguage()); +} + +const WzString& WzI18nString::getLocaleString(const char* localeCode) const +{ + return this->getLocaleString(std::string(localeCode)); +} + +const WzString& WzI18nString::getLocaleString(std::string localeCode) const +{ + auto it = _strings.find(localeCode); + if (it != _strings.end()) + { + return it->second; + } + return _strings.find(_defaultLocale)->second; +} + +std::vector WzI18nString::listLocales() const +{ + std::vector locales; + for (auto it = _strings.begin(); it != _strings.end(); ++it) + { + locales.push_back(it->first); + } + return locales; +} + +void WzI18nString::reset() +{ + _defaultLocale = DEFAULT_LOCALE; + _strings.clear(); + _strings[_defaultLocale] = WzString(); +} + +void WzI18nString::reset(std::string &defaultLocale) +{ + _strings.clear(); + _defaultLocale = defaultLocale; + _strings[_defaultLocale] = WzString(); +} + +void WzI18nString::reset(std::string &defaultLocale, WzString &text) +{ + _strings.clear(); + _defaultLocale = defaultLocale; + _strings[_defaultLocale] = text; +} + +void WzI18nString::setLocaleString(const char* localeCode, WzString text) +{ + _strings[localeCode] = text; +} + +WzI18nString& WzI18nString::operator=(const WzI18nString& other) +{ + _defaultLocale = other._defaultLocale; + for (const auto &locale : other.listLocales()) + { + _strings[std::string(locale)] = WzString(other.getLocaleString(locale)); + } + return *this; +} diff --git a/lib/framework/wzi18nstring.h b/lib/framework/wzi18nstring.h new file mode 100644 index 00000000000..96b3c878c7a --- /dev/null +++ b/lib/framework/wzi18nstring.h @@ -0,0 +1,61 @@ +/* + * This file is part of Warzone 2100. + * Copyright (C) 2024 Warzone 2100 Project + * + * Warzone 2100 is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Warzone 2100 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Warzone 2100; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _LIB_FRAMEWORK_WZI18NSTRING_H +#define _LIB_FRAMEWORK_WZI18NSTRING_H + +#include +#include +#include +#include "lib/framework/wzstring.h" +#include "lib/framework/wzconfig.h" + +/** + * User provided multilingual string. + */ +class WzI18nString { +public: + WzI18nString(); + WzI18nString(WzString string); + WzI18nString(std::string defaultLocale, std::map strings); + WzI18nString(WzConfig &ini, const WzString &key, const int maxChars); + WzI18nString(const WzI18nString& other); + + bool contains(std::string localeCode) const; + bool isEmpty() const; + const std::string& getDefaultLocaleCode() const; + const WzString& getDefaultString() const; + const WzString& getLocaleString() const; + /** Get the string in the given locale or the default one if not set. */ + const WzString& getLocaleString(const char* localeCode) const; + const WzString& getLocaleString(std::string localeCode) const; + std::vector listLocales() const; +public: + void reset(); + void reset(std::string &defaultlocale); + void reset(std::string &defaultLocale, WzString &text); + void setLocaleString(const char* localeCode, WzString text); +public: + WzI18nString& operator=(const WzI18nString& other); +private: + std::string _defaultLocale; + std::map _strings; +}; + +#endif // _LIB_FRAMEWORK_WZI18NSTRING_H diff --git a/lib/netplay/netplay.cpp b/lib/netplay/netplay.cpp index f0d3d199a12..1721a27f913 100644 --- a/lib/netplay/netplay.cpp +++ b/lib/netplay/netplay.cpp @@ -5804,6 +5804,7 @@ const char *messageTypeToString(unsigned messageType_) case NET_PING: return "NET_PING"; case NET_PLAYER_STATS: return "NET_PLAYER_STATS"; case NET_TEXTMSG: return "NET_TEXTMSG"; + case NET_I18NTEXTMSG: return "NET_I18NTEXTMSG"; case NET_PLAYERRESPONDING: return "NET_PLAYERRESPONDING"; case NET_OPTIONS: return "NET_OPTIONS"; case NET_KICK: return "NET_KICK"; diff --git a/lib/netplay/netplay.h b/lib/netplay/netplay.h index eb9f264c4c5..34bddf3a053 100644 --- a/lib/netplay/netplay.h +++ b/lib/netplay/netplay.h @@ -66,6 +66,7 @@ enum MESSAGE_TYPES NET_PING, ///< ping players. NET_PLAYER_STATS, ///< player stats NET_TEXTMSG, ///< A simple text message between machines. + NET_I18NTEXTMSG, ///< A multilingual text message between machines. NET_PLAYERRESPONDING, ///< computer that sent this is now playing warzone! NET_OPTIONS, ///< welcome a player to a game. NET_KICK, ///< kick a player . diff --git a/src/multiint.cpp b/src/multiint.cpp index cab9a159b01..47ea3eec7a7 100644 --- a/src/multiint.cpp +++ b/src/multiint.cpp @@ -28,7 +28,9 @@ #include "lib/framework/wzapp.h" #include "lib/framework/wzconfig.h" #include "lib/framework/wzpaths.h" +#include "lib/framework/wzi18nstring.h" +#include #include #include "lib/framework/frameresource.h" @@ -284,7 +286,7 @@ static struct } locked; static bool spectatorHost = false; static uint16_t defaultOpenSpectatorSlots = 0; -static WzString motd = WzString(); +static WzI18nString motd = WzI18nString(); struct AIDATA { @@ -6041,16 +6043,11 @@ static void loadMapMessageOfTheDay(WzConfig& ini) { if (ini.contains("motd")) { - motd = ini.value("motd").toWzString(); - if (motd.length() > MAX_CONSOLE_STRING_LENGTH) - { - motd.truncate(MAX_CONSOLE_STRING_LENGTH); - debug(LOG_WARNING, "MOTD was truncated to fit into %d characters.", MAX_CONSOLE_STRING_LENGTH); - } + motd = WzI18nString(ini, "motd", MAX_CONSOLE_STRING_LENGTH); } else { - motd = WzString(""); + motd = WzI18nString(""); } } @@ -7358,6 +7355,17 @@ void WzMultiplayerOptionsTitleUI::frontendMultiMessages(bool running) } break; + case NET_I18NTEXTMSG: + if (ingame.localOptionsReceived) + { + NetworkI18nTextMessage message; + if (message.receive(queue)) + { + displayRoomMessage(buildMessage(message.sender, message.text.getLocaleString(getLanguage()).toUtf8().c_str())); + } + } + break; + case NET_VOTE: if (NetPlay.isHost && ingame.localOptionsReceived) { @@ -7684,9 +7692,10 @@ void WzMultiplayerOptionsTitleUI::screenSizeDidChange(unsigned int oldWidth, uns static void printHostHelpMessagesToConsole() { char buf[512] = { '\0' }; - if (!motd.isEmpty()) + WzString localeMotd = motd.getLocaleString(getLanguage()); + if (!localeMotd.isEmpty()) { - ssprintf(buf, "%s", motd.toUtf8().c_str()); + ssprintf(buf, "%s", localeMotd.toUtf8().c_str()); displayRoomNotifyMessage(buf); } if (challengeActive) @@ -8645,7 +8654,7 @@ void sendRoomMotdToSingleReceiver(uint32_t receiver) return; } ASSERT_OR_RETURN(, isHumanPlayer(receiver), "Invalid receiver: %" PRIu32 "", receiver); - NetworkTextMessage message(NOTIFY_MESSAGE, motd.toUtf8().c_str()); + NetworkI18nTextMessage message(NOTIFY_MESSAGE, &motd); message.enqueue(NETnetQueue(receiver)); } diff --git a/src/multiplay.cpp b/src/multiplay.cpp index e286cdab23a..2908614f65d 100644 --- a/src/multiplay.cpp +++ b/src/multiplay.cpp @@ -1762,6 +1762,56 @@ void sendInGameSystemMessage(const char *text) } } +NetworkI18nTextMessage::NetworkI18nTextMessage(int32_t messageSender, WzI18nString const *messageText) +{ + sender = messageSender; + text = WzI18nString(*messageText); +} + +void NetworkI18nTextMessage::enqueue(NETQUEUE queue) +{ + NETbeginEncode(queue, NET_I18NTEXTMSG); + NETint32_t(&sender); + NETstring(text.getDefaultLocaleCode().data(), MAX_LOCALE_CODE_LENGTH); + std::vector locales = text.listLocales(); + int32_t localeCount = locales.size(); + NETint32_t(&localeCount); + for (const auto &localeCode : locales) + { + NETstring(localeCode.data(), MAX_LOCALE_CODE_LENGTH); + NETstring(text.getLocaleString(localeCode).toUtf8().c_str(), MAX_CONSOLE_STRING_LENGTH); + } + NETend(); +} + +bool NetworkI18nTextMessage::receive(NETQUEUE queue) +{ + NETbeginDecode(queue, NET_I18NTEXTMSG); + NETint32_t(&sender); + char defaultLocale[MAX_LOCALE_CODE_LENGTH]; + NETstring(defaultLocale, MAX_LOCALE_CODE_LENGTH); + std::string defaultLocaleStr(defaultLocale); + text.reset(defaultLocaleStr); + int32_t count = 0; + NETint32_t(&count); + for (int32_t i = 0; i < count; ++i) + { + char locale[MAX_LOCALE_CODE_LENGTH]; + char message[MAX_CONSOLE_STRING_LENGTH]; + NETstring(locale, MAX_LOCALE_CODE_LENGTH); + NETstring(message, MAX_CONSOLE_STRING_LENGTH); + text.setLocaleString(locale, WzString(message)); + } + NETend(); + if (!text.contains(defaultLocale)) + { + debug(LOG_ERROR, "Received corrupted WzI18nTextMessage: no text matching the default locale %s.", defaultLocale); + text.reset(); + return false; + } + return true; +} + void printConsoleNameChange(const char *oldName, const char *newName) { char msg[MAX_CONSOLE_STRING_LENGTH]; diff --git a/src/multiplay.h b/src/multiplay.h index 339763d74bc..4875d0b597e 100644 --- a/src/multiplay.h +++ b/src/multiplay.h @@ -28,6 +28,7 @@ #include "lib/framework/types.h" #include "lib/framework/vector.h" #include "lib/framework/crc.h" +#include "lib/framework/wzi18nstring.h" #include "lib/netplay/nettypes.h" #include "multiplaydefs.h" #include "orderdef.h" @@ -149,6 +150,20 @@ struct NetworkTextMessage bool receive(NETQUEUE queue); }; +struct NetworkI18nTextMessage +{ + /** + * Sender can be a player index, SYSTEM_MESSAGE or NOTIFY_MESSAGE. + **/ + int32_t sender; + WzI18nString text; + + NetworkI18nTextMessage() {} + NetworkI18nTextMessage(int32_t messageSender, WzI18nString const *messageText); + void enqueue(NETQUEUE queue); + bool receive(NETQUEUE queue); +}; + enum STRUCTURE_INFO { STRUCTUREINFO_MANUFACTURE,