Feature: Plugin framework for Social Integration with Steam, Discord, GOG, etc (#11628)

This commit is contained in:
Patric Stout 2024-01-22 20:22:45 +01:00 committed by GitHub
parent 75f21065c9
commit d3b2a576de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1181 additions and 4 deletions

View File

@ -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

View File

@ -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()

View File

@ -6,3 +6,4 @@ add_subdirectory(monocypher)
add_subdirectory(squirrel)
add_subdirectory(nlohmann)
add_subdirectory(opengl)
add_subdirectory(openttd_social_integration_api)

View File

@ -0,0 +1,4 @@
add_files(
openttd_social_integration_api.h
openttd_social_integration_api_v1.h
)

View File

@ -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.

View File

@ -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 */

View File

@ -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 */

View File

@ -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

View File

@ -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) {

View File

@ -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";
}
}
{

View File

@ -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]));
}

View File

@ -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
};

View File

@ -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

View File

@ -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;
}

View File

@ -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"]);
}
{

View File

@ -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<CMD_PAUSE>::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();
}

View File

@ -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<int, StringID> _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 <typename T>
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<SocialIntegrationPlugin::State, StringID> 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<SocialIntegrationPlugin *> plugins;
};
/** Construct nested container widget for managing the list of social plugins. */
std::unique_ptr<NWidgetBase> MakeNWidgetSocialPlugins()
{
return std::make_unique<NWidgetSocialPlugins>();
}
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<NWidgetSocialPlugins>(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(),
};

279
src/signature.cpp Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/** @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<std::array<uint8_t, 32>> _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<uint8_t, 32> digest;
crypto_blake2b_ctx ctx;
crypto_blake2b_init(&ctx, digest.size());
while (!feof(f)) {
std::array<uint8_t, 1024> 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 "<version>$<hash>". 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 "<version>$<signature>". 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<uint8_t, 64> 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<uint8_t *>(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<std::string>();
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
}

15
src/signature.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/** @file signature.h Routines to validate signature files. */
#ifndef SIGNATURE_H
#define SIGNATURE_H
bool ValidateSignatureFile(const std::string &filename);
#endif /* SIGNATURE_H */

246
src/social_integration.cpp Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/** @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<LibraryLoader>(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<LibraryLoader> library = nullptr; ///< Library handle.
SocialIntegrationPlugin external; ///< Information of the plugin to be used by other parts of our codebase.
};
static std::vector<std::unique_ptr<InternalSocialIntegrationPlugin>> _plugins; ///< List of loaded plugins.
static std::set<std::string> _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<InternalSocialIntegrationPlugin>(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<SocialIntegrationPlugin *> SocialIntegration::GetPlugins()
{
std::vector<SocialIntegrationPlugin *> 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 <typename T, typename... Ts>
static void PluginCall(std::unique_ptr<InternalSocialIntegrationPlugin> &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);
}
}

85
src/social_integration.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/** @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<SocialIntegrationPlugin *> 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 */

View File

@ -34,6 +34,8 @@
#include "base_media_base.h"
#include "blitter/factory.hpp"
#include "social_integration.h"
#ifdef WITH_ALLEGRO
# include <allegro.h>
#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.
*

View File

@ -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);

View File

@ -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. */