diff --git a/README.md b/README.md index 594241a7a2..1943ed6bc3 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,15 @@ Most types of add-on content can be downloaded within OpenTTD via the 'Check Onl Add-on content can also be installed manually, but that's more complicated; the [OpenTTD wiki](https://wiki.openttd.org/) may offer help with that, or the [OpenTTD directory structure guide](./docs/directory_structure.md). +### 1.5.1) Social Integration + +OpenTTD has the ability to load plugins to integrate with Social Platforms like Steam, Discord, etc. + +To enable such integration, the plugin for the specific platform has to be downloaded and stored in the `social_integration` folder. + +See [OpenTTD's website](https://www.openttd.org), under Downloads, for what plugins are available. + + ### 1.6) OpenTTD directories OpenTTD uses its own directory structure to store game data, add-on content etc. @@ -198,7 +207,10 @@ The icu scriptrun implementation in `src/3rdparty/icu` is licensed under the Uni See `src/3rdparty/icu/LICENSE` for the complete license text. The monocypher implementation in `src/3rdparty/monocypher` is licensed under the 2-clause BSD and CC-0 license. -See src/3rdparty/monocypher/LICENSE.md` for the complete license text. +See `src/3rdparty/monocypher/LICENSE.md` for the complete license text. + +The OpenTTD Social Integration API in `src/3rdparty/openttd_social_integration_api` is licensed under the MIT license. +See `src/3rdparty/openttd_social_integration_api/LICENSE` for the complete license text. ## 4.0 Credits diff --git a/cmake/Options.cmake b/cmake/Options.cmake index 371b03d841..3c8692fe4a 100644 --- a/cmake/Options.cmake +++ b/cmake/Options.cmake @@ -67,6 +67,7 @@ function(set_options) option(OPTION_USE_NSIS "Use NSIS to create windows installer; enable only for stable releases" OFF) option(OPTION_TOOLS_ONLY "Build only tools target" OFF) option(OPTION_DOCS_ONLY "Build only docs target" OFF) + option(OPTION_ALLOW_INVALID_SIGNATURE "Allow loading of content with invalid signatures" OFF) if (OPTION_DOCS_ONLY) set(OPTION_TOOLS_ONLY ON PARENT_SCOPE) @@ -92,6 +93,11 @@ function(show_options) else() message(STATUS "Option Survey Key - NOT USED") endif() + + if(OPTION_ALLOW_INVALID_SIGNATURE) + message(STATUS "Option Allow Invalid Signature - USED") + message(WARNING "Ignoring invalid signatures is a security risk! Use with care!") + endif() endfunction() # Add the definitions for the options that are selected. @@ -116,4 +122,8 @@ function(add_definitions_based_on_options) if(OPTION_SURVEY_KEY) add_definitions(-DSURVEY_KEY="${OPTION_SURVEY_KEY}") endif() + + if(OPTION_ALLOW_INVALID_SIGNATURE) + add_definitions(-DALLOW_INVALID_SIGNATURE) + endif() endfunction() diff --git a/src/3rdparty/CMakeLists.txt b/src/3rdparty/CMakeLists.txt index f167ffc3ec..4d17f023a5 100644 --- a/src/3rdparty/CMakeLists.txt +++ b/src/3rdparty/CMakeLists.txt @@ -6,3 +6,4 @@ add_subdirectory(monocypher) add_subdirectory(squirrel) add_subdirectory(nlohmann) add_subdirectory(opengl) +add_subdirectory(openttd_social_integration_api) diff --git a/src/3rdparty/openttd_social_integration_api/CMakeLists.txt b/src/3rdparty/openttd_social_integration_api/CMakeLists.txt new file mode 100644 index 0000000000..86cca4a3d6 --- /dev/null +++ b/src/3rdparty/openttd_social_integration_api/CMakeLists.txt @@ -0,0 +1,4 @@ +add_files( + openttd_social_integration_api.h + openttd_social_integration_api_v1.h +) diff --git a/src/3rdparty/openttd_social_integration_api/LICENSE b/src/3rdparty/openttd_social_integration_api/LICENSE new file mode 100644 index 0000000000..8e40136dbd --- /dev/null +++ b/src/3rdparty/openttd_social_integration_api/LICENSE @@ -0,0 +1,20 @@ + Copyright 2024 OpenTTD project + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/3rdparty/openttd_social_integration_api/openttd_social_integration_api.h b/src/3rdparty/openttd_social_integration_api/openttd_social_integration_api.h new file mode 100644 index 0000000000..3a165a1b96 --- /dev/null +++ b/src/3rdparty/openttd_social_integration_api/openttd_social_integration_api.h @@ -0,0 +1,38 @@ +/* + * Copyright 2024 OpenTTD project + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * Although all the source-files created by OpenTTD are licensed under the + * GPL-v2, this file is an exception. This file is part of the API for + * social integration plugins, and licensed under the MIT license, to allow + * for non-free implementations. + */ + +/** @file openttd_social_integration_api.h Interface definitions for plugins to report/respond to social integration. */ + +#ifndef OPENTTD_SOCIAL_INTEGRATION_API_H +#define OPENTTD_SOCIAL_INTEGRATION_API_H + +#include "openttd_social_integration_api_v1.h" + +#endif /* OPENTTD_SOCIAL_INTEGRATION_API_H */ diff --git a/src/3rdparty/openttd_social_integration_api/openttd_social_integration_api_v1.h b/src/3rdparty/openttd_social_integration_api/openttd_social_integration_api_v1.h new file mode 100644 index 0000000000..fcf9dcc8a7 --- /dev/null +++ b/src/3rdparty/openttd_social_integration_api/openttd_social_integration_api_v1.h @@ -0,0 +1,157 @@ +/* + * Copyright 2024 OpenTTD project + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * Although all the source-files created by OpenTTD are licensed under the + * GPL-v2, this file is an exception. This file is part of the API for + * social integration plugins, and licensed under the MIT license, to allow + * for non-free implementations. + */ + +/** @file v1.h Version 1 definition of the OpenTTD Social Integration Plugin API. */ + +#ifndef OPENTTD_SOCIAL_INTEGRATION_API_V1_H +#define OPENTTD_SOCIAL_INTEGRATION_API_V1_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** Pointers supplied by the plugin for OpenTTD to use. */ +struct OpenTTD_SocialIntegration_v1_PluginInfo { + /** + * The Social Platform this plugin is for. + * + * UTF-8, nul-terminated. The plugin is and remains the owner of the memory. + * + * As there can only be one plugin active for each Social Platform, this + * value is used to determine which plugin to use. + * + * A complete list of names can be found here: + * https://wiki.openttd.org/en/Development/Social%20Integration + * + * Please use names from that list, including capitalization. + * + * If you create a plugin for a new Social Platform, please add it to the + * wiki page. + */ + const char *social_platform; + + const char *name; ///< Full name of the plugin. UTF-8, nul-terminated. The plugin is and remains the owner of the memory. + const char *version; ///< Version of the plugin. UTF-8, nul-terminated. The plugin is and remains the owner of the memory. +}; + +/** Pointers supplied by the plugin for OpenTTD to use. */ +struct OpenTTD_SocialIntegration_v1_PluginApi { + /** + * OpenTTD tells the plugin to shut down. + * + * The plugin should free any resources it allocated, and must not call any of the callback functions after this call. + */ + void (*shutdown)(); + + /** + * OpenTTD calls this function at regular intervals, to handle any callbacks the plugin might have. + * + * It is also safe to call the OpenTTD_SocialIntegrationCallbacks functions here. + * + * @return True if the plugin wants to be called again, false if the plugin wants to be unloaded. + */ + bool (*run_callbacks)(); + + /** + * The player has entered the main menu. + */ + void (*event_enter_main_menu)(); + + /** + * The player has entered the Scenario Editor. + * + * @param map_width The width of the map in tiles. + * @param map_height The height of the map in tiles. + */ + void (*event_enter_scenario_editor)(unsigned int map_width, unsigned int map_height); + + /** + * The player has entered a singleplayer game. + * + * @param map_width The width of the map in tiles. + * @param map_height The height of the map in tiles. + */ + void (*event_enter_singleplayer)(unsigned int map_width, unsigned int map_height); + + /** + * The player has entered a multiplayer game. + * + * @param map_width The width of the map in tiles. + * @param map_height The height of the map in tiles. + */ + void (*event_enter_multiplayer)(unsigned int map_width, unsigned int map_height); + + /** + * The player is joining a multiplayer game. + * + * This is followed by event_enter_multiplayer() if the join was successful. + */ + void (*event_joining_multiplayer)(); +}; + +/** Pointers supplied by OpenTTD, for the plugin to use. */ +struct OpenTTD_SocialIntegration_v1_OpenTTDInfo { + const char *openttd_version; ///< Version of OpenTTD. UTF-8, nul-terminated. OpenTTD is and remains the owner of the memory. +}; + +/** The result of the initialization. */ +enum OpenTTD_SocialIntegration_v1_InitResult : int { + OTTD_SOCIAL_INTEGRATION_V1_INIT_SUCCESS = 1, ///< Plugin initialized successfully. + OTTD_SOCIAL_INTEGRATION_V1_INIT_FAILED = -1, ///< Plugin failed to initialize (generic error). + OTTD_SOCIAL_INTEGRATION_V1_INIT_PLATFORM_NOT_RUNNING = -2, ///< The Social Platform is not running. +}; + +/** + * Type of the Init function the plugin is expected to export from its dynamic library. + * + * The plugin has to export the implementation of this function as "SocialIntegration_vN_Init", where N is the API version this entry point is for. + * A single plugin can have multiple versions implemented. + * + * @param[out] plugin_api Structure the plugin must fill with pointers. Can contain nullptr if the plugin does not support a feature. The plugin is owner of the memory. + * @param[in] openttd_info Structure that OpenTTD filled with pointers. All pointers will remain valid until shutdown(). OpenTTD is owner of the memory. + * @return The status of the initialization. + */ +typedef OpenTTD_SocialIntegration_v1_InitResult (*OpenTTD_SocialIntegration_v1_Init)(OpenTTD_SocialIntegration_v1_PluginApi *plugin_api, const OpenTTD_SocialIntegration_v1_OpenTTDInfo *openttd_info); + +/** + * Type of the GetInfo function the plugin is expected to export from its dynamic library. + * + * The plugin has to export the implementation of this function as "SocialIntegration_vN_GetInfo", where N is the API version this entry point is for. + * A single plugin can have multiple versions implemented. + * + * @param[out] plugin_info Structure the plugin must fill with pointers. The plugin is owner of the memory. + */ +typedef void (*OpenTTD_SocialIntegration_v1_GetInfo)(OpenTTD_SocialIntegration_v1_PluginInfo *plugin_info); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* OPENTTD_SOCIAL_INTEGRATION_API_V1_H */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2a6275cd35..ebb946ae4e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -382,6 +382,8 @@ add_files( signal.cpp signal_func.h signal_type.h + signature.cpp + signature.h signs.cpp signs_base.h signs_cmd.cpp @@ -393,6 +395,8 @@ add_files( slope_type.h smallmap_gui.cpp smallmap_gui.h + social_integration.cpp + social_integration.h sortlist_type.h sound.cpp sound_func.h diff --git a/src/console_cmds.cpp b/src/console_cmds.cpp index 9c72b8fe3e..d47e0cdc5a 100644 --- a/src/console_cmds.cpp +++ b/src/console_cmds.cpp @@ -2141,6 +2141,7 @@ DEF_CONSOLE_CMD(ConListDirs) { SAVE_DIR, "save", true }, { AUTOSAVE_DIR, "autosave", true }, { SCREENSHOT_DIR, "screenshot", true }, + { SOCIAL_INTEGRATION_DIR, "social_integration", true }, }; if (argc != 2) { diff --git a/src/crashlog.cpp b/src/crashlog.cpp index 682d35f7c1..eae4a3bef8 100644 --- a/src/crashlog.cpp +++ b/src/crashlog.cpp @@ -122,6 +122,9 @@ void CrashLog::FillCrashLog() if (!this->TryExecute("libraries", [&info]() { SurveyLibraries(info["libraries"]); return true; })) { info["libraries"] = "crashed while gathering information"; } + if (!this->TryExecute("plugins", [&info]() { SurveyPlugins(info["plugins"]); return true; })) { + info["plugins"] = "crashed while gathering information"; + } } { diff --git a/src/fileio.cpp b/src/fileio.cpp index 05aa8718b7..26792fa596 100644 --- a/src/fileio.cpp +++ b/src/fileio.cpp @@ -52,6 +52,7 @@ static const char * const _subdirs[] = { "game" PATHSEP, "game" PATHSEP "library" PATHSEP, "screenshot" PATHSEP, + "social_integration" PATHSEP, }; static_assert(lengthof(_subdirs) == NUM_SUBDIRS); @@ -1054,7 +1055,7 @@ void DeterminePaths(const char *exe, bool only_local_path) Debug(misc, 1, "{} found as personal directory", _personal_dir); static const Subdirectory default_subdirs[] = { - SAVE_DIR, AUTOSAVE_DIR, SCENARIO_DIR, HEIGHTMAP_DIR, BASESET_DIR, NEWGRF_DIR, AI_DIR, AI_LIBRARY_DIR, GAME_DIR, GAME_LIBRARY_DIR, SCREENSHOT_DIR + SAVE_DIR, AUTOSAVE_DIR, SCENARIO_DIR, HEIGHTMAP_DIR, BASESET_DIR, NEWGRF_DIR, AI_DIR, AI_LIBRARY_DIR, GAME_DIR, GAME_LIBRARY_DIR, SCREENSHOT_DIR, SOCIAL_INTEGRATION_DIR }; for (uint i = 0; i < lengthof(default_subdirs); i++) { @@ -1068,7 +1069,7 @@ void DeterminePaths(const char *exe, bool only_local_path) FillValidSearchPaths(only_local_path); /* Create the directory for each of the types of content */ - const Subdirectory dirs[] = { SCENARIO_DIR, HEIGHTMAP_DIR, BASESET_DIR, NEWGRF_DIR, AI_DIR, AI_LIBRARY_DIR, GAME_DIR, GAME_LIBRARY_DIR }; + const Subdirectory dirs[] = { SCENARIO_DIR, HEIGHTMAP_DIR, BASESET_DIR, NEWGRF_DIR, AI_DIR, AI_LIBRARY_DIR, GAME_DIR, GAME_LIBRARY_DIR, SOCIAL_INTEGRATION_DIR }; for (uint i = 0; i < lengthof(dirs); i++) { FioCreateDirectory(FioGetDirectory(SP_AUTODOWNLOAD_DIR, dirs[i])); } diff --git a/src/fileio_type.h b/src/fileio_type.h index 66d502d88a..c871c1c14b 100644 --- a/src/fileio_type.h +++ b/src/fileio_type.h @@ -121,6 +121,7 @@ enum Subdirectory { GAME_DIR, ///< Subdirectory for all game scripts GAME_LIBRARY_DIR, ///< Subdirectory for all GS libraries SCREENSHOT_DIR, ///< Subdirectory for all screenshots + SOCIAL_INTEGRATION_DIR, ///< Subdirectory for all social integration plugins NUM_SUBDIRS, ///< Number of subdirectories NO_DIRECTORY, ///< A path without any base directory }; diff --git a/src/lang/english.txt b/src/lang/english.txt index 35600f0b19..af9418d5b1 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -943,6 +943,8 @@ STR_GAME_OPTIONS_TAB_GRAPHICS :Graphics STR_GAME_OPTIONS_TAB_GRAPHICS_TT :{BLACK}Choose graphics settings STR_GAME_OPTIONS_TAB_SOUND :Sound STR_GAME_OPTIONS_TAB_SOUND_TT :{BLACK}Choose sound and music settings +STR_GAME_OPTIONS_TAB_SOCIAL :Social +STR_GAME_OPTIONS_TAB_SOCIAL_TT :{BLACK}Choose social integration settings STR_GAME_OPTIONS_VOLUME :Volume STR_GAME_OPTIONS_SFX_VOLUME :Sound effects @@ -1082,6 +1084,20 @@ STR_GAME_OPTIONS_BASE_MUSIC :{BLACK}Base mus STR_GAME_OPTIONS_BASE_MUSIC_TOOLTIP :{BLACK}Select the base music set to use STR_GAME_OPTIONS_BASE_MUSIC_DESCRIPTION_TOOLTIP :{BLACK}Additional information about the base music set +STR_GAME_OPTIONS_SOCIAL_PLUGINS_NONE :{LTBLUE}(no plugins to integrate with social platforms installed) + +STR_GAME_OPTIONS_SOCIAL_PLUGIN_TITLE :{BLACK}{RAW_STRING} ({RAW_STRING}) +STR_GAME_OPTIONS_SOCIAL_PLUGIN_PLATFORM :{BLACK}Platform: +STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE :{BLACK}Plugin state: + +STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE_RUNNING :{GREEN}Running +STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE_FAILED :{RED}Failed to initialize +STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE_PLATFORM_NOT_RUNNING :{ORANGE}{RAW_STRING} not running +STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE_UNLOADED :{RED}Unloaded +STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE_DUPLICATE :{RED}Duplicated plugin +STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE_UNSUPPORTED_API :{RED}Unsupported version +STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE_INVALID_SIGNATURE :{RED}Invalid signature + STR_BASESET_STATUS :{RAW_STRING} {RED}({NUM} missing/corrupted file{P "" s}) STR_ERROR_RESOLUTION_LIST_FAILED :{WHITE}Failed to retrieve a list of supported resolutions diff --git a/src/network/network_client.cpp b/src/network/network_client.cpp index 1a177e87d3..008fe64294 100644 --- a/src/network/network_client.cpp +++ b/src/network/network_client.cpp @@ -31,6 +31,7 @@ #include "network_gamelist.h" #include "../core/backup_type.hpp" #include "../thread.h" +#include "../social_integration.h" #include "table/strings.h" @@ -845,6 +846,8 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_MAP_DONE(Packet SetLocalCompany(_network_join.company); } + SocialIntegration::EventEnterMultiplayer(Map::SizeX(), Map::SizeY()); + return NETWORK_RECV_STATUS_OKAY; } diff --git a/src/network/network_survey.cpp b/src/network/network_survey.cpp index 5e73ffb0b0..59d9b22d60 100644 --- a/src/network/network_survey.cpp +++ b/src/network/network_survey.cpp @@ -61,6 +61,7 @@ std::string NetworkSurveyHandler::CreatePayload(Reason reason, bool for_preview) SurveyFont(info["font"]); SurveyCompiler(info["compiler"]); SurveyLibraries(info["libraries"]); + SurveyPlugins(info["plugins"]); } { diff --git a/src/openttd.cpp b/src/openttd.cpp index 50521ab663..f49f0792b8 100644 --- a/src/openttd.cpp +++ b/src/openttd.cpp @@ -75,6 +75,7 @@ #include "timer/timer_game_economy.h" #include "timer/timer_game_realtime.h" #include "timer/timer_game_tick.h" +#include "social_integration.h" #include "linkgraph/linkgraphschedule.h" @@ -288,6 +289,7 @@ static void ShutdownGame() if (_network_available) NetworkShutDown(); // Shut down the network and close any open connections + SocialIntegration::Shutdown(); DriverFactoryBase::ShutdownDrivers(); UnInitWindowSystem(); @@ -752,6 +754,7 @@ int openttd_main(int argc, char *argv[]) /* The video driver is now selected, now initialise GUI zoom */ AdjustGUIZoom(false); + SocialIntegration::Initialize(); NetworkStartUp(); // initialize network-core if (!HandleBootstrap()) { @@ -997,6 +1000,28 @@ bool SafeLoad(const std::string &filename, SaveLoadOperation fop, DetailedFileTy return false; } +static void UpdateSocialIntegration(GameMode game_mode) +{ + switch (game_mode) { + case GM_BOOTSTRAP: + case GM_MENU: + SocialIntegration::EventEnterMainMenu(); + break; + + case GM_NORMAL: + if (_networking) { + SocialIntegration::EventEnterMultiplayer(Map::SizeX(), Map::SizeY()); + } else { + SocialIntegration::EventEnterSingleplayer(Map::SizeX(), Map::SizeY()); + } + break; + + case GM_EDITOR: + SocialIntegration::EventEnterScenarioEditor(Map::SizeX(), Map::SizeY()); + break; + } +} + void SwitchToMode(SwitchMode new_mode) { /* If we are saving something, the network stays in its current state */ @@ -1044,6 +1069,8 @@ void SwitchToMode(SwitchMode new_mode) case SM_EDITOR: // Switch to scenario editor MakeNewEditorWorld(); GenerateSavegameId(); + + UpdateSocialIntegration(GM_EDITOR); break; case SM_RELOADGAME: // Reload with what-ever started the game @@ -1061,12 +1088,16 @@ void SwitchToMode(SwitchMode new_mode) MakeNewGame(false, new_mode == SM_NEWGAME); GenerateSavegameId(); + + UpdateSocialIntegration(GM_NORMAL); break; case SM_RESTARTGAME: // Restart --> 'Random game' with current settings case SM_NEWGAME: // New Game --> 'Random game' MakeNewGame(false, new_mode == SM_NEWGAME); GenerateSavegameId(); + + UpdateSocialIntegration(GM_NORMAL); break; case SM_LOAD_GAME: { // Load game, Play Scenario @@ -1084,6 +1115,8 @@ void SwitchToMode(SwitchMode new_mode) /* Decrease pause counter (was increased from opening load dialog) */ Command::Post(PM_PAUSED_SAVELOAD, false); } + + UpdateSocialIntegration(GM_NORMAL); break; } @@ -1091,6 +1124,8 @@ void SwitchToMode(SwitchMode new_mode) case SM_START_HEIGHTMAP: // Load a heightmap and start a new game from it MakeNewGame(true, new_mode == SM_START_HEIGHTMAP); GenerateSavegameId(); + + UpdateSocialIntegration(GM_NORMAL); break; case SM_LOAD_HEIGHTMAP: // Load heightmap from scenario editor @@ -1099,6 +1134,8 @@ void SwitchToMode(SwitchMode new_mode) GenerateWorld(GWM_HEIGHTMAP, 1 << _settings_game.game_creation.map_x, 1 << _settings_game.game_creation.map_y); GenerateSavegameId(); MarkWholeScreenDirty(); + + UpdateSocialIntegration(GM_NORMAL); break; case SM_LOAD_SCENARIO: { // Load scenario from scenario editor @@ -1112,12 +1149,16 @@ void SwitchToMode(SwitchMode new_mode) SetDParamStr(0, GetSaveLoadErrorString()); ShowErrorMessage(STR_JUST_RAW_STRING, INVALID_STRING_ID, WL_CRITICAL); } + + UpdateSocialIntegration(GM_NORMAL); break; } case SM_JOIN_GAME: // Join a multiplayer game LoadIntroGame(); NetworkClientJoinGame(); + + SocialIntegration::EventJoiningMultiplayer(); break; case SM_MENU: // Switch to game intro menu @@ -1134,6 +1175,8 @@ void SwitchToMode(SwitchMode new_mode) ShowNetworkAskSurvey(); } } + + UpdateSocialIntegration(GM_MENU); break; case SM_SAVE_GAME: // Save game. @@ -1544,4 +1587,5 @@ void GameLoop() SoundDriver::GetInstance()->MainLoop(); MusicLoop(); + SocialIntegration::RunCallbacks(); } diff --git a/src/settings_gui.cpp b/src/settings_gui.cpp index 15519b62f5..aca64de902 100644 --- a/src/settings_gui.cpp +++ b/src/settings_gui.cpp @@ -46,6 +46,7 @@ #include "network/network_gui.h" #include "network/network_survey.h" #include "video/video_driver.hpp" +#include "social_integration.h" #include "safeguards.h" @@ -170,6 +171,184 @@ static const std::map _volume_labels = { { 127, STR_GAME_OPTIONS_VOLUME_100 }, }; +static const NWidgetPart _nested_social_plugins_widgets[] = { + NWidget(NWID_HORIZONTAL), + NWidget(WWT_FRAME, COLOUR_GREY, WID_GO_SOCIAL_PLUGIN_TITLE), SetDataTip(STR_JUST_STRING2, STR_NULL), + NWidget(NWID_HORIZONTAL), SetPIP(0, WidgetDimensions::unscaled.hsep_normal, 0), + NWidget(WWT_TEXT, COLOUR_GREY), SetMinimalSize(0, 12), SetFill(1, 0), SetDataTip(STR_GAME_OPTIONS_SOCIAL_PLUGIN_PLATFORM, STR_NULL), + NWidget(WWT_TEXT, COLOUR_GREY, WID_GO_SOCIAL_PLUGIN_PLATFORM), SetMinimalSize(100, 12), SetDataTip(STR_JUST_RAW_STRING, STR_NULL), SetAlignment(SA_RIGHT), + EndContainer(), + NWidget(NWID_HORIZONTAL), SetPIP(0, WidgetDimensions::unscaled.hsep_normal, 0), + NWidget(WWT_TEXT, COLOUR_GREY), SetMinimalSize(0, 12), SetFill(1, 0), SetDataTip(STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE, STR_NULL), + NWidget(WWT_TEXT, COLOUR_GREY, WID_GO_SOCIAL_PLUGIN_STATE), SetMinimalSize(100, 12), SetDataTip(STR_JUST_STRING1, STR_NULL), SetAlignment(SA_RIGHT), + EndContainer(), + EndContainer(), + EndContainer(), +}; + +static const NWidgetPart _nested_social_plugins_none_widgets[] = { + NWidget(NWID_HORIZONTAL), + NWidget(WWT_TEXT, COLOUR_GREY), SetMinimalSize(0, 12), SetFill(1, 0), SetDataTip(STR_GAME_OPTIONS_SOCIAL_PLUGINS_NONE, STR_NULL), + EndContainer(), +}; + +class NWidgetSocialPlugins : public NWidgetVertical { +public: + NWidgetSocialPlugins() + { + this->plugins = SocialIntegration::GetPlugins(); + + if (this->plugins.empty()) { + auto widget = MakeNWidgets(std::begin(_nested_social_plugins_none_widgets), std::end(_nested_social_plugins_none_widgets), nullptr); + this->Add(std::move(widget)); + } else { + for (size_t i = 0; i < this->plugins.size(); i++) { + auto widget = MakeNWidgets(std::begin(_nested_social_plugins_widgets), std::end(_nested_social_plugins_widgets), nullptr); + this->Add(std::move(widget)); + } + } + + this->SetPIP(0, WidgetDimensions::unscaled.vsep_wide, 0); + } + + void FillWidgetLookup(WidgetLookup &widget_lookup) override + { + widget_lookup[WID_GO_SOCIAL_PLUGINS] = this; + NWidgetVertical::FillWidgetLookup(widget_lookup); + } + + void SetupSmallestSize(Window *w) override + { + this->current_index = -1; + NWidgetVertical::SetupSmallestSize(w); + } + + /** + * Find of all the plugins the one where the member is the widest (in pixels). + * + * @param member The member to check with. + * @return The plugin that has the widest value (in pixels) for the given member. + */ + template + std::string &GetWidestPlugin(T SocialIntegrationPlugin::*member) const + { + std::string *longest = &(this->plugins[0]->*member); + int longest_length = 0; + + for (auto *plugin : this->plugins) { + int length = GetStringBoundingBox(plugin->*member).width; + if (length > longest_length) { + longest_length = length; + longest = &(plugin->*member); + } + } + + return *longest; + } + + void SetStringParameters(int widget) const + { + switch (widget) { + case WID_GO_SOCIAL_PLUGIN_TITLE: + /* For SetupSmallestSize, use the longest string we have. */ + if (this->current_index < 0) { + SetDParamStr(0, GetWidestPlugin(&SocialIntegrationPlugin::name)); + SetDParamStr(1, GetWidestPlugin(&SocialIntegrationPlugin::version)); + break; + } + + if (this->plugins[this->current_index]->name.empty()) { + SetDParam(0, STR_JUST_RAW_STRING); + SetDParamStr(1, this->plugins[this->current_index]->basepath); + } else { + SetDParam(0, STR_GAME_OPTIONS_SOCIAL_PLUGIN_TITLE); + SetDParamStr(1, this->plugins[this->current_index]->name); + SetDParamStr(2, this->plugins[this->current_index]->version); + } + break; + + case WID_GO_SOCIAL_PLUGIN_PLATFORM: + /* For SetupSmallestSize, use the longest string we have. */ + if (this->current_index < 0) { + SetDParamStr(0, GetWidestPlugin(&SocialIntegrationPlugin::social_platform)); + break; + } + + SetDParamStr(0, this->plugins[this->current_index]->social_platform); + break; + + case WID_GO_SOCIAL_PLUGIN_STATE: { + static const std::pair state_to_string[] = { + { SocialIntegrationPlugin::RUNNING, STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE_RUNNING }, + { SocialIntegrationPlugin::FAILED, STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE_FAILED }, + { SocialIntegrationPlugin::PLATFORM_NOT_RUNNING, STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE_PLATFORM_NOT_RUNNING }, + { SocialIntegrationPlugin::UNLOADED, STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE_UNLOADED }, + { SocialIntegrationPlugin::DUPLICATE, STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE_DUPLICATE }, + { SocialIntegrationPlugin::UNSUPPORTED_API, STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE_UNSUPPORTED_API }, + { SocialIntegrationPlugin::INVALID_SIGNATURE, STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE_INVALID_SIGNATURE }, + }; + + /* For SetupSmallestSize, use the longest string we have. */ + if (this->current_index < 0) { + auto longest_plugin = GetWidestPlugin(&SocialIntegrationPlugin::social_platform); + + /* Set the longest plugin when looking for the longest status. */ + SetDParamStr(0, longest_plugin); + + StringID longest = STR_NULL; + int longest_length = 0; + for (auto state : state_to_string) { + int length = GetStringBoundingBox(state.second).width; + if (length > longest_length) { + longest_length = length; + longest = state.second; + } + } + + SetDParam(0, longest); + SetDParamStr(1, longest_plugin); + break; + } + + auto plugin = this->plugins[this->current_index]; + + /* Default string, in case no state matches. */ + SetDParam(0, STR_GAME_OPTIONS_SOCIAL_PLUGIN_STATE_FAILED); + SetDParamStr(1, plugin->social_platform); + + /* Find the string for the state. */ + for (auto state : state_to_string) { + if (plugin->state == state.first) { + SetDParam(0, state.second); + break; + } + } + } + break; + } + } + + void Draw(const Window *w) override + { + this->current_index = 0; + + for (auto &wid : this->children) { + wid->Draw(w); + this->current_index++; + } + } + +private: + int current_index = -1; + std::vector plugins; +}; + +/** Construct nested container widget for managing the list of social plugins. */ +std::unique_ptr MakeNWidgetSocialPlugins() +{ + return std::make_unique(); +} + struct GameOptionsWindow : Window { GameSettings *opt; bool reload; @@ -348,6 +527,16 @@ struct GameOptionsWindow : Window { } break; } + + case WID_GO_SOCIAL_PLUGIN_TITLE: + case WID_GO_SOCIAL_PLUGIN_PLATFORM: + case WID_GO_SOCIAL_PLUGIN_STATE: { + const NWidgetSocialPlugins *plugin = this->GetWidget(WID_GO_SOCIAL_PLUGINS); + assert(plugin != nullptr); + + plugin->SetStringParameters(widget); + break; + } } } @@ -390,7 +579,7 @@ struct GameOptionsWindow : Window { void SetTab(WidgetID widget) { - this->SetWidgetsLoweredState(false, WID_GO_TAB_GENERAL, WID_GO_TAB_GRAPHICS, WID_GO_TAB_SOUND); + this->SetWidgetsLoweredState(false, WID_GO_TAB_GENERAL, WID_GO_TAB_GRAPHICS, WID_GO_TAB_SOUND, WID_GO_TAB_SOCIAL); this->LowerWidget(widget); GameOptionsWindow::active_tab = widget; @@ -399,6 +588,7 @@ struct GameOptionsWindow : Window { case WID_GO_TAB_GENERAL: pane = 0; break; case WID_GO_TAB_GRAPHICS: pane = 1; break; case WID_GO_TAB_SOUND: pane = 2; break; + case WID_GO_TAB_SOCIAL: pane = 3; break; default: NOT_REACHED(); } @@ -493,6 +683,7 @@ struct GameOptionsWindow : Window { case WID_GO_TAB_GENERAL: case WID_GO_TAB_GRAPHICS: case WID_GO_TAB_SOUND: + case WID_GO_TAB_SOCIAL: this->SetTab(widget); break; @@ -814,6 +1005,7 @@ static constexpr NWidgetPart _nested_game_options_widgets[] = { NWidget(WWT_TEXTBTN, COLOUR_YELLOW, WID_GO_TAB_GENERAL), SetMinimalTextLines(2, 0), SetDataTip(STR_GAME_OPTIONS_TAB_GENERAL, STR_GAME_OPTIONS_TAB_GENERAL_TT), SetFill(1, 0), NWidget(WWT_TEXTBTN, COLOUR_YELLOW, WID_GO_TAB_GRAPHICS), SetMinimalTextLines(2, 0), SetDataTip(STR_GAME_OPTIONS_TAB_GRAPHICS, STR_GAME_OPTIONS_TAB_GRAPHICS_TT), SetFill(1, 0), NWidget(WWT_TEXTBTN, COLOUR_YELLOW, WID_GO_TAB_SOUND), SetMinimalTextLines(2, 0), SetDataTip(STR_GAME_OPTIONS_TAB_SOUND, STR_GAME_OPTIONS_TAB_SOUND_TT), SetFill(1, 0), + NWidget(WWT_TEXTBTN, COLOUR_YELLOW, WID_GO_TAB_SOCIAL), SetMinimalTextLines(2, 0), SetDataTip(STR_GAME_OPTIONS_TAB_SOCIAL, STR_GAME_OPTIONS_TAB_SOCIAL_TT), SetFill(1, 0), EndContainer(), EndContainer(), NWidget(WWT_PANEL, COLOUR_GREY), @@ -969,6 +1161,11 @@ static constexpr NWidgetPart _nested_game_options_widgets[] = { EndContainer(), EndContainer(), EndContainer(), + + /* Social tab */ + NWidget(NWID_VERTICAL), SetPadding(WidgetDimensions::unscaled.sparse), SetPIP(0, WidgetDimensions::unscaled.vsep_wide, 0), + NWidgetFunction(MakeNWidgetSocialPlugins), + EndContainer(), EndContainer(), EndContainer(), }; diff --git a/src/signature.cpp b/src/signature.cpp new file mode 100644 index 0000000000..a329410e35 --- /dev/null +++ b/src/signature.cpp @@ -0,0 +1,279 @@ +/* + * This file is part of OpenTTD. + * OpenTTD 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, version 2. + * OpenTTD 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 OpenTTD. If not, see . + */ + +/** @file signature.cpp Implementation of signature validation routines. */ + +#include "stdafx.h" + +#include "signature.h" + +#include "debug.h" +#include "fileio_func.h" +#include "string_func.h" + +#include "3rdparty/monocypher/monocypher.h" +#include "3rdparty/monocypher/monocypher-ed25519.h" +#include "3rdparty/nlohmann/json.hpp" + +#include "safeguards.h" + +/** The public keys used for signature validation. */ +static const std::initializer_list> _public_keys_v1 = { + /* 2024-01-20 - Public key for Social Integration Plugins. */ + { 0xed, 0x5d, 0x57, 0x47, 0x21, 0x99, 0x8b, 0x02, 0xdf, 0x6e, 0x3d, 0x69, 0xe1, 0x87, 0xca, 0xd0, 0x0e, 0x88, 0xc3, 0xe2, 0xb2, 0xa6, 0x7b, 0xc0, 0x42, 0xc8, 0xd6, 0x4b, 0x65, 0xe6, 0x48, 0xf7 }, +}; + +/** + * Calculate the 32-byte blake2b hash of a file. + * + * @param filename The filename to calculate the hash of. + * @return The 32-byte blake2b hash of the file, hex-encoded. + */ +static std::string CalculateHashV1(const std::string &filename) +{ + FILE *f = FioFOpenFile(filename, "rb", NO_DIRECTORY); + if (f == nullptr) { + return ""; + } + + std::array digest; + crypto_blake2b_ctx ctx; + crypto_blake2b_init(&ctx, digest.size()); + + while (!feof(f)) { + std::array buf; + size_t len = fread(buf.data(), 1, buf.size(), f); + + crypto_blake2b_update(&ctx, buf.data(), len); + } + fclose(f); + + crypto_blake2b_final(&ctx, digest.data()); + return FormatArrayAsHex(digest); +} + +/** + * Validate whether the checksum of a file is the same. + * + * @param filename The filename to validate the checksum of. + * @param checksum The expected checksum. + * @return True iff the checksum of the file is the same as the expected checksum. + */ +static bool ValidateChecksum(const std::string &filename, const std::string &checksum) +{ + /* Checksums are "$". Split out the version. */ + auto pos = checksum.find('$'); + assert(pos != std::string::npos); // Already validated by ValidateSchema(). + const std::string version = checksum.substr(0, pos); + const std::string hash = checksum.substr(pos + 1); + + /* Calculate the checksum over the file. */ + std::string calculated_hash; + if (version == "1") { + calculated_hash = CalculateHashV1(filename); + } else { + Debug(misc, 0, "Failed to validate signature: unknown checksum version: {}", filename); + return false; + } + + /* Validate the checksum is the same. */ + if (calculated_hash.empty()) { + Debug(misc, 0, "Failed to validate signature: couldn't calculate checksum for: {}", filename); + return false; + } + if (calculated_hash != hash) { + Debug(misc, 0, "Failed to validate signature: checksum mismatch for: {}", filename); + return false; + } + + return true; +} + +/** + * Validate whether the signature is valid for this set of files. + * + * @param signature The signature to validate. + * @param files The files to validate the signature against. + * @param filename The filename of the signatures file (for error-reporting). + * @return True iff the signature is valid for this set of files. + */ +static bool ValidateSignature(const std::string &signature, const nlohmann::json &files, const std::string &filename) +{ + /* Signatures are "$". Split out the version. */ + auto pos = signature.find('$'); + assert(pos != std::string::npos); // Already validated by ValidateSchema(). + const std::string version = signature.substr(0, pos); + const std::string sig_value = signature.substr(pos + 1); + + /* Create the message we are going to validate. */ + std::string message = files.dump(-1); + + /* Validate the signature. */ + if (version == "1") { + std::array sig; + if (sig_value.size() != 128 || !ConvertHexToBytes(sig_value, sig)) { + Debug(misc, 0, "Failed to validate signature: invalid signature: {}", filename); + return false; + } + + for (auto &pk_value : _public_keys_v1) { + /* Check if the message is valid with this public key. */ + auto res = crypto_ed25519_check(sig.data(), pk_value.data(), reinterpret_cast(message.data()), message.size()); + if (res == 0) { + return true; + } + } + + Debug(misc, 0, "Failed to validate signature: signature validation failed: {}", filename); + return false; + } else { + Debug(misc, 0, "Failed to validate signature: unknown signature version: {}", filename); + return false; + } + + return true; +} + +/** + * Validate the signatures file complies with the JSON schema. + * + * @param signatures The signatures JSON to validate. + * @param filename The filename of the signatures file (for error-reporting). + * @return True iff the signatures file complies with the JSON schema. + */ +static bool ValidateSchema(const nlohmann::json &signatures, const std::string &filename) +{ + if (signatures["files"].is_null()) { + Debug(misc, 0, "Failed to validate signature: no files found: {}", filename); + return false; + } + + if (signatures["signature"].is_null()) { + Debug(misc, 0, "Failed to validate signature: no signature found: {}", filename); + return false; + } + + for (auto &signature : signatures["files"]) { + if (signature["filename"].is_null() || signature["checksum"].is_null()) { + Debug(misc, 0, "Failed to validate signature: invalid entry in files: {}", filename); + return false; + } + + const std::string sig_filename = signature["filename"]; + const std::string sig_checksum = signature["checksum"]; + + if (sig_filename.empty() || sig_checksum.empty()) { + Debug(misc, 0, "Failed to validate signature: invalid entry in files: {}", filename); + return false; + } + + auto pos = sig_checksum.find('$'); + if (pos == std::string::npos) { + Debug(misc, 0, "Failed to validate signature: invalid checksum format: {}", filename); + return false; + } + } + + const std::string signature = signatures["signature"]; + auto pos = signature.find('$'); + if (pos == std::string::npos) { + Debug(misc, 0, "Failed to validate signature: invalid signature format: {}", filename); + return false; + } + + return true; +} + +/** + * Validate that the signatures mentioned in the signature file are matching + * the files in question. + * + * @return True iff the files in the signature file passed validation. + */ +static bool _ValidateSignatureFile(const std::string &filename) +{ + size_t filesize; + FILE *f = FioFOpenFile(filename, "rb", NO_DIRECTORY, &filesize); + if (f == nullptr) { + Debug(misc, 0, "Failed to validate signature: file not found: {}", filename); + return false; + } + + std::string text(filesize, '\0'); + size_t len = fread(text.data(), filesize, 1, f); + if (len != 1) { + Debug(misc, 0, "Failed to validate signature: failed to read file: {}", filename); + return false; + } + + nlohmann::json signatures; + try { + signatures = nlohmann::json::parse(text); + } catch (nlohmann::json::exception &) { + Debug(misc, 0, "Failed to validate signature: not a valid JSON file: {}", filename); + return false; + } + + /* + * The JSON file should look like: + * + * { + * "files": [ + * { + * "checksum": "version$hash" + * "filename": "filename", + * }, + * ... + * ], + * "signature": "version$signature" + * } + * + * The signature is a signed message of the content of "files", dumped as + * JSON without spaces / newlines, keys in the order as indicated above. + */ + + if (!ValidateSchema(signatures, filename)) { + return false; + } + + if (!ValidateSignature(signatures["signature"], signatures["files"], filename)) { + return false; + } + + std::string dirname = std::filesystem::path(filename).parent_path().string(); + + for (auto &signature : signatures["files"]) { + const std::string sig_filename = dirname + PATHSEPCHAR + signature["filename"].get(); + const std::string sig_checksum = signature["checksum"]; + + if (!ValidateChecksum(sig_filename, sig_checksum)) { + return false; + } + } + + return true; +} + +/** + * Validate that the signatures mentioned in the signature file are matching + * the files in question. + * + * @note if ALLOW_INVALID_SIGNATURE is defined, this function will always + * return true (but will still report any errors in the console). + * + * @return True iff the files in the signature file passed validation. + */ +bool ValidateSignatureFile(const std::string &filename) +{ + auto res = _ValidateSignatureFile(filename);; +#if defined(ALLOW_INVALID_SIGNATURE) + (void)res; // Ignore the result. + return true; +#else + return res; +#endif +} diff --git a/src/signature.h b/src/signature.h new file mode 100644 index 0000000000..769aeee591 --- /dev/null +++ b/src/signature.h @@ -0,0 +1,15 @@ +/* + * This file is part of OpenTTD. + * OpenTTD 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, version 2. + * OpenTTD 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 OpenTTD. If not, see . + */ + +/** @file signature.h Routines to validate signature files. */ + +#ifndef SIGNATURE_H +#define SIGNATURE_H + +bool ValidateSignatureFile(const std::string &filename); + +#endif /* SIGNATURE_H */ diff --git a/src/social_integration.cpp b/src/social_integration.cpp new file mode 100644 index 0000000000..18e8e44eb7 --- /dev/null +++ b/src/social_integration.cpp @@ -0,0 +1,246 @@ +/* + * This file is part of OpenTTD. + * OpenTTD 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, version 2. + * OpenTTD 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 OpenTTD. If not, see . + */ + +/** @file social_integration.cpp Base implementation of social integration support. */ + +#include "stdafx.h" + +#include "social_integration.h" +#include "3rdparty/openttd_social_integration_api/openttd_social_integration_api.h" + +#include "debug.h" +#include "fileio_func.h" +#include "library_loader.h" +#include "rev.h" +#include "string_func.h" +#include "signature.h" + +#include "safeguards.h" + +/** + * Container to track information per plugin. + */ +class InternalSocialIntegrationPlugin { +public: + InternalSocialIntegrationPlugin(const std::string &filename, const std::string &basepath) : library(nullptr), external(basepath) + { + openttd_info.openttd_version = _openttd_revision; + + if (!ValidateSignatureFile(fmt::format("{}.sig", filename))) { + external.state = SocialIntegrationPlugin::INVALID_SIGNATURE; + return; + } + + this->library = std::make_unique(filename); + } + + OpenTTD_SocialIntegration_v1_PluginInfo plugin_info = {}; ///< Information supplied by plugin. + OpenTTD_SocialIntegration_v1_PluginApi plugin_api = {}; ///< API supplied by plugin. + OpenTTD_SocialIntegration_v1_OpenTTDInfo openttd_info = {}; ///< Information supplied by OpenTTD. + + std::unique_ptr library = nullptr; ///< Library handle. + + SocialIntegrationPlugin external; ///< Information of the plugin to be used by other parts of our codebase. +}; + +static std::vector> _plugins; ///< List of loaded plugins. +static std::set _loaded_social_platform; ///< List of Social Platform plugins already loaded. Used to prevent loading a plugin for the same Social Platform twice. + +/** Helper for scanning for files with SocialIntegration as extension */ +class SocialIntegrationFileScanner : FileScanner { +public: + void Scan() + { +#ifdef _WIN32 + std::string extension = "-social.dll"; +#elif defined(__APPLE__) + std::string extension = "-social.dylib"; +#else + std::string extension = "-social.so"; +#endif + + this->FileScanner::Scan(extension.c_str(), SOCIAL_INTEGRATION_DIR, false); + } + + bool AddFile(const std::string &filename, size_t basepath_length, const std::string &) override + { + std::string basepath = filename.substr(basepath_length); + Debug(misc, 1, "[Social Integration: {}] Loading ...", basepath); + + auto &plugin = _plugins.emplace_back(std::make_unique(filename, basepath)); + + /* Validation failed, so no library was loaded. */ + if (plugin->library == nullptr) { + return false; + } + + if (plugin->library->HasError()) { + plugin->external.state = SocialIntegrationPlugin::FAILED; + + Debug(misc, 0, "[Social Integration: {}] Failed to load library: {}", basepath, plugin->library->GetLastError()); + return false; + } + + OpenTTD_SocialIntegration_v1_GetInfo getinfo_func = plugin->library->GetFunction("SocialIntegration_v1_GetInfo"); + if (plugin->library->HasError()) { + plugin->external.state = SocialIntegrationPlugin::UNSUPPORTED_API; + + Debug(misc, 0, "[Social Integration: {}] Failed to find symbol SocialPlugin_v1_GetInfo: {}", basepath, plugin->library->GetLastError()); + return false; + } + + OpenTTD_SocialIntegration_v1_Init init_func = plugin->library->GetFunction("SocialIntegration_v1_Init"); + if (plugin->library->HasError()) { + plugin->external.state = SocialIntegrationPlugin::UNSUPPORTED_API; + + Debug(misc, 0, "[Social Integration: {}] Failed to find symbol SocialPlugin_v1_Init: {}", basepath, plugin->library->GetLastError()); + return false; + } + + getinfo_func(&plugin->plugin_info); + /* Setup the information for the outside world to see. */ + plugin->external.social_platform = plugin->plugin_info.social_platform; + plugin->external.name = plugin->plugin_info.name; + plugin->external.version = plugin->plugin_info.version; + + /* Lowercase the string for comparison. */ + std::string lc_social_platform = plugin->plugin_info.social_platform; + strtolower(lc_social_platform); + + /* Prevent more than one plugin for a certain Social Platform to be loaded, as that never ends well. */ + if (_loaded_social_platform.find(lc_social_platform) != _loaded_social_platform.end()) { + plugin->external.state = SocialIntegrationPlugin::DUPLICATE; + + Debug(misc, 0, "[Social Integration: {}] Another plugin for {} is already loaded", basepath, plugin->plugin_info.social_platform); + return false; + } + _loaded_social_platform.insert(lc_social_platform); + + auto state = init_func(&plugin->plugin_api, &plugin->openttd_info); + switch (state) { + case OTTD_SOCIAL_INTEGRATION_V1_INIT_SUCCESS: + plugin->external.state = SocialIntegrationPlugin::RUNNING; + + Debug(misc, 1, "[Social Integration: {}] Loaded for {}: {} ({})", basepath, plugin->plugin_info.social_platform, plugin->plugin_info.name, plugin->plugin_info.version); + return true; + + case OTTD_SOCIAL_INTEGRATION_V1_INIT_FAILED: + plugin->external.state = SocialIntegrationPlugin::FAILED; + + Debug(misc, 0, "[Social Integration: {}] Failed to initialize", basepath); + return false; + + case OTTD_SOCIAL_INTEGRATION_V1_INIT_PLATFORM_NOT_RUNNING: + plugin->external.state = SocialIntegrationPlugin::PLATFORM_NOT_RUNNING; + + Debug(misc, 1, "[Social Integration: {}] Failed to initialize: {} is not running", basepath, plugin->plugin_info.social_platform); + return false; + + default: + NOT_REACHED(); + } + } +}; + +std::vector SocialIntegration::GetPlugins() +{ + std::vector plugins; + + for (auto &plugin : _plugins) { + plugins.push_back(&plugin->external); + } + + return plugins; +} + +void SocialIntegration::Initialize() +{ + SocialIntegrationFileScanner fs; + fs.Scan(); +} + +/** + * Wrapper to call a function pointer of a plugin if it isn't a nullptr. + * + * @param plugin Plugin to call the function pointer on. + * @param func Function pointer to call. + */ +template +static void PluginCall(std::unique_ptr &plugin, T func, Ts... args) +{ + if (plugin->external.state != SocialIntegrationPlugin::RUNNING) { + return; + } + + if (func != nullptr) { + func(args...); + } +} + +void SocialIntegration::Shutdown() +{ + for (auto &plugin : _plugins) { + PluginCall(plugin, plugin->plugin_api.shutdown); + } + + _plugins.clear(); + _loaded_social_platform.clear(); +} + +void SocialIntegration::RunCallbacks() +{ + for (auto &plugin : _plugins) { + if (plugin->external.state != SocialIntegrationPlugin::RUNNING) { + continue; + } + + if (plugin->plugin_api.run_callbacks != nullptr) { + if (!plugin->plugin_api.run_callbacks()) { + Debug(misc, 1, "[Social Plugin: {}] Requested to be unloaded", plugin->external.basepath); + + _loaded_social_platform.erase(plugin->plugin_info.social_platform); + plugin->external.state = SocialIntegrationPlugin::UNLOADED; + PluginCall(plugin, plugin->plugin_api.shutdown); + } + } + } +} + +void SocialIntegration::EventEnterMainMenu() +{ + for (auto &plugin : _plugins) { + PluginCall(plugin, plugin->plugin_api.event_enter_main_menu); + } +} + +void SocialIntegration::EventEnterScenarioEditor(uint map_width, uint map_height) +{ + for (auto &plugin : _plugins) { + PluginCall(plugin, plugin->plugin_api.event_enter_scenario_editor, map_width, map_height); + } +} + +void SocialIntegration::EventEnterSingleplayer(uint map_width, uint map_height) +{ + for (auto &plugin : _plugins) { + PluginCall(plugin, plugin->plugin_api.event_enter_singleplayer, map_width, map_height); + } +} + +void SocialIntegration::EventEnterMultiplayer(uint map_width, uint map_height) +{ + for (auto &plugin : _plugins) { + PluginCall(plugin, plugin->plugin_api.event_enter_multiplayer, map_width, map_height); + } +} + +void SocialIntegration::EventJoiningMultiplayer() +{ + for (auto &plugin : _plugins) { + PluginCall(plugin, plugin->plugin_api.event_joining_multiplayer); + } +} diff --git a/src/social_integration.h b/src/social_integration.h new file mode 100644 index 0000000000..b3c9b092c2 --- /dev/null +++ b/src/social_integration.h @@ -0,0 +1,85 @@ +/* + * This file is part of OpenTTD. + * OpenTTD 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, version 2. + * OpenTTD 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 OpenTTD. If not, see . + */ + +/** @file social_integration.h Interface definitions for game to report/respond to social integration. */ + +#ifndef SOCIAL_INTEGRATION_H +#define SOCIAL_INTEGRATION_H + +class SocialIntegrationPlugin { +public: + enum State { + RUNNING, ///< The plugin is successfully loaded and running. + + FAILED, ///< The plugin failed to initialize. + PLATFORM_NOT_RUNNING, ///< The plugin failed to initialize because the Social Platform is not running. + UNLOADED, ///< The plugin is unloaded upon request. + DUPLICATE, ///< Another plugin of the same Social Platform is already loaded. + UNSUPPORTED_API, ///< The plugin does not support the current API version. + INVALID_SIGNATURE, ///< The signature of the plugin is invalid. + }; + + std::string basepath; ///< Base path of the plugin. + + std::string social_platform = "unknown"; ///< Social platform this plugin is for. + std::string name = ""; ///< Name of the plugin. + std::string version = ""; ///< Version of the plugin. + + State state = FAILED; ///< Result of the plugin's init function. + + SocialIntegrationPlugin(const std::string &basepath) : basepath(basepath) {} +}; + +class SocialIntegration { +public: + /** + * Get the list of loaded social integration plugins. + */ + static std::vector GetPlugins(); + + /** + * Initialize the social integration system, loading any social integration plugins that are available. + */ + static void Initialize(); + + /** + * Shutdown the social integration system, and all social integration plugins that are loaded. + */ + static void Shutdown(); + + /** + * Allow any social integration library to handle their own events. + */ + static void RunCallbacks(); + + /** + * Event: user entered the main menu. + */ + static void EventEnterMainMenu(); + + /** + * Event: user entered the Scenario Editor. + */ + static void EventEnterScenarioEditor(uint map_width, uint map_height); + + /** + * Event: user entered a singleplayer game. + */ + static void EventEnterSingleplayer(uint map_width, uint map_height); + + /** + * Event: user entered a multiplayer game. + */ + static void EventEnterMultiplayer(uint map_width, uint map_height); + + /** + * Event: user is joining a multiplayer game. + */ + static void EventJoiningMultiplayer(); +}; + +#endif /* SOCIAL_INTEGRATION_H */ diff --git a/src/survey.cpp b/src/survey.cpp index 6f71c869bb..89a57fc717 100644 --- a/src/survey.cpp +++ b/src/survey.cpp @@ -34,6 +34,8 @@ #include "base_media_base.h" #include "blitter/factory.hpp" +#include "social_integration.h" + #ifdef WITH_ALLEGRO # include #endif /* WITH_ALLEGRO */ @@ -81,6 +83,17 @@ NLOHMANN_JSON_SERIALIZE_ENUM(GRFStatus, { {GRFStatus::GCS_ACTIVATED, "activated"}, }) +NLOHMANN_JSON_SERIALIZE_ENUM(SocialIntegrationPlugin::State, { + {SocialIntegrationPlugin::State::RUNNING, "running"}, + {SocialIntegrationPlugin::State::FAILED, "failed"}, + {SocialIntegrationPlugin::State::PLATFORM_NOT_RUNNING, "platform_not_running"}, + {SocialIntegrationPlugin::State::UNLOADED, "unloaded"}, + {SocialIntegrationPlugin::State::DUPLICATE, "duplicate"}, + {SocialIntegrationPlugin::State::UNSUPPORTED_API, "unsupported_api"}, + {SocialIntegrationPlugin::State::INVALID_SIGNATURE, "invalid_signature"}, +}) + + /** Lookup table to convert a VehicleType to a string. */ static const std::string _vehicle_type_to_string[] = { "train", @@ -435,6 +448,26 @@ void SurveyLibraries(nlohmann::json &survey) #endif } +/** + * Convert plugin information to JSON. + * + * @param survey The JSON object. + */ +void SurveyPlugins(nlohmann::json &survey) +{ + auto _plugins = SocialIntegration::GetPlugins(); + + for (auto &plugin : _plugins) { + auto &platform = survey[plugin->social_platform]; + platform.push_back({ + {"name", plugin->name}, + {"version", plugin->version}, + {"basepath", plugin->basepath}, + {"state", plugin->state}, + }); + } +} + /** * Change the bytes of memory into a textual version rounded up to the biggest unit. * diff --git a/src/survey.h b/src/survey.h index aae16d4bf4..0e74641a27 100644 --- a/src/survey.h +++ b/src/survey.h @@ -21,6 +21,7 @@ void SurveyFont(nlohmann::json &survey); void SurveyGameScript(nlohmann::json &survey); void SurveyGrfs(nlohmann::json &survey); void SurveyLibraries(nlohmann::json &survey); +void SurveyPlugins(nlohmann::json &survey); void SurveyOpenTTD(nlohmann::json &survey); void SurveySettings(nlohmann::json &survey, bool skip_if_default); void SurveyTimers(nlohmann::json &survey); diff --git a/src/widgets/settings_widget.h b/src/widgets/settings_widget.h index 4c40f1e0a0..fccba67a2b 100644 --- a/src/widgets/settings_widget.h +++ b/src/widgets/settings_widget.h @@ -15,6 +15,7 @@ enum GameOptionsWidgets : WidgetID { WID_GO_TAB_GENERAL, ///< General tab. WID_GO_TAB_GRAPHICS, ///< Graphics tab. WID_GO_TAB_SOUND, ///< Sound tab. + WID_GO_TAB_SOCIAL, ///< Social tab. WID_GO_TAB_SELECTION, ///< Background of the tab selection. WID_GO_CURRENCY_DROPDOWN, ///< Currency dropdown. WID_GO_DISTANCE_DROPDOWN, ///< Measuring unit dropdown. @@ -53,6 +54,10 @@ enum GameOptionsWidgets : WidgetID { WID_GO_SURVEY_PARTICIPATE_BUTTON, ///< Toggle for participating in the automated survey. WID_GO_SURVEY_LINK_BUTTON, ///< Button to open browser to go to the survey website. WID_GO_SURVEY_PREVIEW_BUTTON, ///< Button to open a preview window with the survey results + WID_GO_SOCIAL_PLUGINS, ///< Main widget handling the social plugins. + WID_GO_SOCIAL_PLUGIN_TITLE, ///< Title of the frame of the social plugin. + WID_GO_SOCIAL_PLUGIN_PLATFORM, ///< Platform of the social plugin. + WID_GO_SOCIAL_PLUGIN_STATE, ///< State of the social plugin. }; /** Widgets of the #GameSettingsWindow class. */