Index: ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.js (revision 14436)
+++ ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.js (revision 14437)
@@ -1,321 +1,321 @@
var userReportEnabledText; // contains the original version with "$status" placeholder
var currentSubmenuType; // contains submenu type
const MARGIN = 4; // menu border size
const background = "hellenes1"; // Background type. Currently: 'hellenes1', 'persians1'.
var g_ShowSplashScreens;
function init(initData, hotloadData)
{
initMusic();
// Play main menu music
global.music.setState(global.music.states.MENU);
userReportEnabledText = getGUIObjectByName("userReportEnabledText").caption;
// initialize currentSubmenuType with placeholder to avoid null when switching
currentSubmenuType = "submenuSinglePlayer";
EnableUserReport(Engine.IsUserReportEnabled());
// Only show splash screen(s) once at startup, but not again after hotloading
g_ShowSplashScreens = hotloadData ? hotloadData.showSplashScreens : initData && initData.isStartup;
}
function getHotloadData()
{
return { "showSplashScreens": g_ShowSplashScreens };
}
var t0 = new Date;
function scrollBackgrounds(background)
{
if (background == "hellenes1")
{
var layer1 = getGUIObjectByName("backgroundHele1-1");
var layer2 = getGUIObjectByName("backgroundHele1-2");
var layer3 = getGUIObjectByName("backgroundHele1-3");
layer1.hidden = false;
layer2.hidden = false;
layer3.hidden = false;
var screen = layer1.parent.getComputedSize();
var h = screen.bottom - screen.top; // height of screen
var w = h*16/9; // width of background image
// Offset the layers by oscillating amounts
var t = (t0 - new Date) / 700;
var speed = 1/20;
var off1 = 0.02 * w * (1+Math.cos(t*speed));
var off2 = 0.12 * w * (1+Math.cos(t*speed)) - h*6/9;
var off3 = 0.16 * w * (1+Math.cos(t*speed));
var left = screen.right - w * (1 + Math.ceil(screen.right / w));
layer1.size = new GUISize(left + off1, screen.top, screen.right + off1, screen.bottom);
layer2.size = new GUISize(screen.right/2 - h + off2, screen.top, screen.right/2 + h + off2, screen.bottom);
layer3.size = new GUISize(screen.right - h + off3, screen.top, screen.right + off3, screen.bottom);
}
if (background == "persians1")
{
var layer1 = getGUIObjectByName("backgroundPers1-1");
var layer2 = getGUIObjectByName("backgroundPers1-2");
var layer3 = getGUIObjectByName("backgroundPers1-3");
var layer4 = getGUIObjectByName("backgroundPers1-4");
layer1.hidden = false;
layer2.hidden = false;
layer3.hidden = false;
layer4.hidden = false;
var screen = layer1.parent.getComputedSize();
var h = screen.bottom - screen.top; // height of screen
var screenWidth = screen.right - screen.left;
var w = h*16/9;
var t = (t0 - new Date) / 1000;
var speed = 1/20;
var off1 = 0.01 * w * (Math.cos(t*speed));
var off2 = 0.03 * w * (Math.cos(t*speed));
var off3 = 0.07 * w * (1+Math.cos(t*speed)) + 0.5 * screenWidth - h*1.1;
var off4 = 0.16 * w * (1+Math.cos(t*speed)) - h*6/9;
var left = screen.right - w * (1 + Math.ceil(screen.right / w)) - 0.5 * screenWidth + h;
layer1.size = new GUISize(left + off1, screen.top, screen.right + off1 + h, screen.bottom);
layer2.size = new GUISize(left + off2, screen.top, screen.right + off2 + h, screen.bottom);
layer3.size = new GUISize(screen.left + off3, screen.top, screen.left + 2 * h + off3, screen.bottom);
layer4.size = new GUISize(screen.left + off4, screen.top, screen.left + 2 * h + off4, screen.bottom);
}
}
function submitUserReportMessage()
{
var input = getGUIObjectByName("userReportMessageInput");
var msg = input.caption;
if (msg.length)
Engine.SubmitUserReport("message", 1, msg);
input.caption = "";
}
function formatUserReportStatus(status)
{
var d = status.split(/:/, 3);
if (d[0] == "disabled")
return "disabled";
if (d[0] == "connecting")
return "connecting to server";
if (d[0] == "sending")
{
var done = d[1];
return "uploading (" + Math.floor(100*done) + "%)";
}
if (d[0] == "completed")
{
var httpCode = d[1];
if (httpCode == 200)
return "upload succeeded";
else
return "upload failed (" + httpCode + ")";
}
if (d[0] == "failed")
{
var errCode = d[1];
var errMessage = d[2];
return "upload failed (" + errMessage + ")";
}
return "unknown";
}
var lastTickTime = new Date;
function onTick()
{
var now = new Date;
var tickLength = new Date - lastTickTime;
lastTickTime = now;
// Animate backgrounds
scrollBackgrounds(background);
// Animate submenu
updateMenuPosition(tickLength);
if (Engine.IsUserReportEnabled())
{
getGUIObjectByName("userReportEnabledText").caption =
userReportEnabledText.replace(/\$status/,
formatUserReportStatus(Engine.GetUserReportStatus()));
}
// Show splash screens here, so we don't interfere with main menu hotloading
if (g_ShowSplashScreens)
{
g_ShowSplashScreens = false;
- if (Engine.IsSplashScreenEnabled())
+ if (Engine.ConfigDB_GetValue("user", "splashscreenenable") !== "false")
Engine.PushGuiPage("page_splashscreen.xml", { "page": "splashscreen" } );
// Warn about removing fixed render path
if (Engine.Renderer_GetRenderPath() == "fixed")
messageBox(
600,
300,
"[font=\"serif-bold-16\"][color=\"200 20 20\"]Warning:[/color] You appear to be using non-shader (fixed function) graphics. This option will be removed in a future 0 A.D. release, to allow for more advanced graphics features. We advise upgrading your graphics card to a more recent, shader-compatible model.\n\nPlease press \"Read More\" for more information or \"Ok\" to continue.",
"WARNING!",
0,
["Ok", "Read More"],
[null, function() { Engine.OpenURL("http://www.wildfiregames.com/forum/index.php?showtopic=16734"); }]
);
}
}
function EnableUserReport(Enabled)
{
getGUIObjectByName("userReportDisabled").hidden = Enabled;
getGUIObjectByName("userReportEnabled").hidden = !Enabled;
Engine.SetUserReportEnabled(Enabled);
}
/*
* MENU FUNCTIONS
*/
// Temporarily adding this here
//const BUTTON_SOUND = "audio/interface/ui/ui_button_longclick.ogg";
//function playButtonSound()
//{
// var buttonSound = new Sound(BUTTON_SOUND);
// buttonSound.play();
//}
// Slide menu
function updateMenuPosition(dt)
{
var submenu = getGUIObjectByName("submenu");
if (submenu.hidden == false)
{
// Number of pixels per millisecond to move
const SPEED = 1.2;
var maxOffset = getGUIObjectByName("mainMenu").size.right - submenu.size.left;
if (maxOffset > 0)
{
var offset = Math.min(SPEED * dt, maxOffset);
var size = submenu.size;
size.left += offset;
size.right += offset;
submenu.size = size;
}
}
}
// Opens the menu by revealing the screen which contains the menu
function openMenu(newSubmenu, position, buttonHeight, numButtons)
{
// switch to new submenu type
currentSubmenuType = newSubmenu;
getGUIObjectByName(currentSubmenuType).hidden = false;
// set position of new submenu
var submenu = getGUIObjectByName("submenu");
var top = position - MARGIN;
var bottom = position + ((buttonHeight + MARGIN) * numButtons);
submenu.size = submenu.size.left + " " + top + " " + submenu.size.right + " " + bottom;
// Blend in right border of main menu into the left border of the submenu
blendSubmenuIntoMain(top, bottom);
// Reveal submenu
getGUIObjectByName("submenu").hidden = false;
}
// Closes the menu and resets position
function closeMenu()
{
// playButtonSound();
// remove old submenu type
getGUIObjectByName(currentSubmenuType).hidden = true;
// hide submenu and reset position
var submenu = getGUIObjectByName("submenu");
submenu.hidden = true;
submenu.size = getGUIObjectByName("mainMenu").size;
// reset main menu panel right border
getGUIObjectByName("MainMenuPanelRightBorderTop").size = "100%-2 0 100% 100%";
}
// Sizes right border on main menu panel to match the submenu
function blendSubmenuIntoMain(topPosition, bottomPosition)
{
var topSprite = getGUIObjectByName("MainMenuPanelRightBorderTop");
topSprite.size = "100%-2 0 100% " + (topPosition + MARGIN);
var bottomSprite = getGUIObjectByName("MainMenuPanelRightBorderBottom");
bottomSprite.size = "100%-2 " + (bottomPosition) + " 100% 100%";
}
/*
* FUNCTIONS BELOW DO NOT WORK YET
*/
//// Switch to a given options tab window.
//function openOptionsTab(tabName)
//{
// // Hide the other tabs.
// for (var i = 1; i <= 3; i++)
// {
// switch (i)
// {
// case 1:
// var tmpName = "pgOptionsAudio";
// break;
// case 2:
// var tmpName = "pgOptionsVideo";
// break;
// case 3:
// var tmpName = "pgOptionsGame";
// break;
// default:
// break;
// }
//
// if (tmpName != tabName)
// {
// getGUIObjectByName (tmpName + "Window").hidden = true;
// getGUIObjectByName (tmpName + "Button").enabled = true;
// }
// }
//
// // Make given tab visible.
// getGUIObjectByName (tabName + "Window").hidden = false;
// getGUIObjectByName (tabName + "Button").enabled = false;
//}
//
//// Move the credits up the screen.
//function updateCredits()
//{
// // If there are still credit lines to remove, remove them.
// if (getNumItems("pgCredits") > 0)
// removeItem ("pgCredits", 0);
// else
// {
// // When we've run out of credit,
//
// // Stop the increment timer if it's still active.
// cancelInterval();
//
// // Close the credits screen and return.
// closeMainMenuSubWindow ("pgCredits");
// guiUnHide ("pg");
// }
//}
Index: ps/trunk/binaries/data/mods/public/gui/splashscreen/splashscreen.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/splashscreen/splashscreen.xml (revision 14436)
+++ ps/trunk/binaries/data/mods/public/gui/splashscreen/splashscreen.xml (revision 14437)
@@ -1,39 +1,40 @@
Index: ps/trunk/source/gui/scripting/ScriptFunctions.cpp
===================================================================
--- ps/trunk/source/gui/scripting/ScriptFunctions.cpp (revision 14436)
+++ ps/trunk/source/gui/scripting/ScriptFunctions.cpp (revision 14437)
@@ -1,752 +1,733 @@
/* Copyright (C) 2013 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "scriptinterface/ScriptInterface.h"
#include "graphics/Camera.h"
#include "graphics/GameView.h"
#include "graphics/MapReader.h"
#include "gui/GUIManager.h"
#include "graphics/scripting/JSInterface_GameView.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include "lib/sysdep/sysdep.h"
#include "lobby/scripting/JSInterface_Lobby.h"
#include "maths/FixedVector3D.h"
#include "network/NetClient.h"
#include "network/NetServer.h"
#include "network/NetTurnManager.h"
#include "ps/CLogger.h"
#include "ps/CConsole.h"
#include "ps/Errors.h"
#include "ps/Game.h"
#include "ps/GUID.h"
#include "ps/World.h"
#include "ps/Hotkey.h"
#include "ps/Overlay.h"
#include "ps/ProfileViewer.h"
#include "ps/Pyrogenesis.h"
#include "ps/SavedGame.h"
#include "ps/scripting/JSInterface_ConfigDB.h"
#include "ps/scripting/JSInterface_Console.h"
#include "ps/UserReport.h"
#include "ps/GameSetup/Atlas.h"
#include "ps/GameSetup/Config.h"
#include "ps/ConfigDB.h"
#include "renderer/scripting/JSInterface_Renderer.h"
#include "tools/atlas/GameInterface/GameLoop.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpAIManager.h"
#include "simulation2/components/ICmpCommandQueue.h"
#include "simulation2/components/ICmpGuiInterface.h"
#include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/components/ICmpTemplateManager.h"
#include "simulation2/components/ICmpSelectable.h"
#include "simulation2/helpers/Selection.h"
#include "js/jsapi.h"
/*
* This file defines a set of functions that are available to GUI scripts, to allow
* interaction with the rest of the engine.
* Functions are exposed to scripts within the global object 'Engine', so
* scripts should call "Engine.FunctionName(...)" etc.
*/
extern void restart_mainloop_in_atlas(); // from main.cpp
namespace {
CScriptVal GetActiveGui(void* UNUSED(cbdata))
{
return OBJECT_TO_JSVAL(g_GUI->GetScriptObject());
}
void PushGuiPage(void* UNUSED(cbdata), std::wstring name, CScriptVal initData)
{
g_GUI->PushPage(name, initData);
}
void SwitchGuiPage(void* UNUSED(cbdata), std::wstring name, CScriptVal initData)
{
g_GUI->SwitchPage(name, initData);
}
void PopGuiPage(void* UNUSED(cbdata))
{
g_GUI->PopPage();
}
CScriptVal GuiInterfaceCall(void* cbdata, std::wstring name, CScriptVal data)
{
CGUIManager* guiManager = static_cast (cbdata);
if (!g_Game)
return JSVAL_VOID;
CSimulation2* sim = g_Game->GetSimulation2();
ENSURE(sim);
CmpPtr cmpGuiInterface(*sim, SYSTEM_ENTITY);
if (!cmpGuiInterface)
return JSVAL_VOID;
int player = -1;
if (g_Game)
player = g_Game->GetPlayerID();
CScriptValRooted arg (sim->GetScriptInterface().GetContext(), sim->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), data.get()));
CScriptVal ret (cmpGuiInterface->ScriptCall(player, name, arg.get()));
return guiManager->GetScriptInterface().CloneValueFromOtherContext(sim->GetScriptInterface(), ret.get());
}
void PostNetworkCommand(void* cbdata, CScriptVal cmd)
{
CGUIManager* guiManager = static_cast (cbdata);
if (!g_Game)
return;
CSimulation2* sim = g_Game->GetSimulation2();
ENSURE(sim);
CmpPtr cmpCommandQueue(*sim, SYSTEM_ENTITY);
if (!cmpCommandQueue)
return;
jsval cmd2 = sim->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), cmd.get());
cmpCommandQueue->PostNetworkCommand(cmd2);
}
std::vector PickEntitiesAtPoint(void* UNUSED(cbdata), int x, int y)
{
return EntitySelection::PickEntitiesAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, g_Game->GetPlayerID(), false);
}
std::vector PickFriendlyEntitiesInRect(void* UNUSED(cbdata), int x0, int y0, int x1, int y1, int player)
{
return EntitySelection::PickEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x0, y0, x1, y1, player, false);
}
std::vector PickFriendlyEntitiesOnScreen(void* cbdata, int player)
{
return PickFriendlyEntitiesInRect(cbdata, 0, 0, g_xres, g_yres, player);
}
std::vector PickSimilarFriendlyEntities(void* UNUSED(cbdata), std::string templateName, bool includeOffScreen, bool matchRank, bool allowFoundations)
{
return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetPlayerID(), includeOffScreen, matchRank, false, allowFoundations);
}
CFixedVector3D GetTerrainAtScreenPoint(void* UNUSED(cbdata), int x, int y)
{
CVector3D pos = g_Game->GetView()->GetCamera()->GetWorldCoordinates(x, y, true);
return CFixedVector3D(fixed::FromFloat(pos.X), fixed::FromFloat(pos.Y), fixed::FromFloat(pos.Z));
}
std::wstring SetCursor(void* UNUSED(cbdata), std::wstring name)
{
std::wstring old = g_CursorName;
g_CursorName = name;
return old;
}
int GetPlayerID(void* UNUSED(cbdata))
{
if (g_Game)
return g_Game->GetPlayerID();
return -1;
}
void SetPlayerID(void* UNUSED(cbdata), int id)
{
if (g_Game)
g_Game->SetPlayerID(id);
}
void StartNetworkGame(void* UNUSED(cbdata))
{
ENSURE(g_NetServer);
g_NetServer->StartGame();
}
void StartGame(void* cbdata, CScriptVal attribs, int playerID)
{
CGUIManager* guiManager = static_cast (cbdata);
ENSURE(!g_NetServer);
ENSURE(!g_NetClient);
ENSURE(!g_Game);
g_Game = new CGame();
// Convert from GUI script context to sim script context
CSimulation2* sim = g_Game->GetSimulation2();
CScriptValRooted gameAttribs (sim->GetScriptInterface().GetContext(),
sim->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), attribs.get()));
g_Game->SetPlayerID(playerID);
g_Game->StartGame(gameAttribs, "");
}
CScriptVal StartSavedGame(void* cbdata, std::wstring name)
{
CGUIManager* guiManager = static_cast (cbdata);
ENSURE(!g_NetServer);
ENSURE(!g_NetClient);
ENSURE(!g_Game);
// Load the saved game data from disk
CScriptValRooted metadata;
std::string savedState;
Status err = SavedGames::Load(name, guiManager->GetScriptInterface(), metadata, savedState);
if (err < 0)
return CScriptVal();
g_Game = new CGame();
// Convert from GUI script context to sim script context
CSimulation2* sim = g_Game->GetSimulation2();
CScriptValRooted gameMetadata (sim->GetScriptInterface().GetContext(),
sim->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), metadata.get()));
CScriptValRooted gameInitAttributes;
sim->GetScriptInterface().GetProperty(gameMetadata.get(), "initAttributes", gameInitAttributes);
int playerID;
sim->GetScriptInterface().GetProperty(gameMetadata.get(), "player", playerID);
// Start the game
g_Game->SetPlayerID(playerID);
g_Game->StartGame(gameInitAttributes, savedState);
return metadata.get();
}
void SaveGame(void* cbdata, std::wstring filename, std::wstring description)
{
CGUIManager* guiManager = static_cast (cbdata);
if (SavedGames::Save(filename, description, *g_Game->GetSimulation2(), guiManager, g_Game->GetPlayerID()) < 0)
LOGERROR(L"Failed to save game");
}
void SaveGamePrefix(void* cbdata, std::wstring prefix, std::wstring description)
{
CGUIManager* guiManager = static_cast (cbdata);
if (SavedGames::SavePrefix(prefix, description, *g_Game->GetSimulation2(), guiManager, g_Game->GetPlayerID()) < 0)
LOGERROR(L"Failed to save game");
}
void SetNetworkGameAttributes(void* cbdata, CScriptVal attribs)
{
CGUIManager* guiManager = static_cast (cbdata);
ENSURE(g_NetServer);
g_NetServer->UpdateGameAttributes(attribs, guiManager->GetScriptInterface());
}
void StartNetworkHost(void* cbdata, std::wstring playerName)
{
CGUIManager* guiManager = static_cast (cbdata);
ENSURE(!g_NetClient);
ENSURE(!g_NetServer);
ENSURE(!g_Game);
g_NetServer = new CNetServer();
if (!g_NetServer->SetupConnection())
{
guiManager->GetScriptInterface().ReportError("Failed to start server");
SAFE_DELETE(g_NetServer);
return;
}
g_Game = new CGame();
g_NetClient = new CNetClient(g_Game);
g_NetClient->SetUserName(playerName);
if (!g_NetClient->SetupConnection("127.0.0.1"))
{
guiManager->GetScriptInterface().ReportError("Failed to connect to server");
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
}
void StartNetworkJoin(void* cbdata, std::wstring playerName, std::string serverAddress)
{
CGUIManager* guiManager = static_cast (cbdata);
ENSURE(!g_NetClient);
ENSURE(!g_NetServer);
ENSURE(!g_Game);
g_Game = new CGame();
g_NetClient = new CNetClient(g_Game);
g_NetClient->SetUserName(playerName);
if (!g_NetClient->SetupConnection(serverAddress))
{
guiManager->GetScriptInterface().ReportError("Failed to connect to server");
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
}
void DisconnectNetworkGame(void* UNUSED(cbdata))
{
// TODO: we ought to do async reliable disconnections
SAFE_DELETE(g_NetServer);
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
CScriptVal PollNetworkClient(void* cbdata)
{
CGUIManager* guiManager = static_cast (cbdata);
if (!g_NetClient)
return CScriptVal();
CScriptValRooted poll = g_NetClient->GuiPoll();
// Convert from net client context to GUI script context
return guiManager->GetScriptInterface().CloneValueFromOtherContext(g_NetClient->GetScriptInterface(), poll.get());
}
void AssignNetworkPlayer(void* UNUSED(cbdata), int playerID, std::string guid)
{
ENSURE(g_NetServer);
g_NetServer->AssignPlayer(playerID, guid);
}
void SendNetworkChat(void* UNUSED(cbdata), std::wstring message)
{
ENSURE(g_NetClient);
g_NetClient->SendChatMessage(message);
}
std::vector GetAIs(void* cbdata)
{
CGUIManager* guiManager = static_cast (cbdata);
return ICmpAIManager::GetAIs(guiManager->GetScriptInterface());
}
std::vector GetSavedGames(void* cbdata)
{
CGUIManager* guiManager = static_cast (cbdata);
return SavedGames::GetSavedGames(guiManager->GetScriptInterface());
}
bool DeleteSavedGame(void* UNUSED(cbdata), std::wstring name)
{
return SavedGames::DeleteSavedGame(name);
}
void OpenURL(void* UNUSED(cbdata), std::string url)
{
sys_open_url(url);
}
std::wstring GetMatchID(void* UNUSED(cbdata))
{
return ps_generate_guid().FromUTF8();
}
void RestartInAtlas(void* UNUSED(cbdata))
{
restart_mainloop_in_atlas();
}
bool AtlasIsAvailable(void* UNUSED(cbdata))
{
return ATLAS_IsAvailable();
}
bool IsAtlasRunning(void* UNUSED(cbdata))
{
return (g_AtlasGameLoop && g_AtlasGameLoop->running);
}
CScriptVal LoadMapSettings(void* cbdata, VfsPath pathname)
{
CGUIManager* guiManager = static_cast (cbdata);
CMapSummaryReader reader;
if (reader.LoadMap(pathname) != PSRETURN_OK)
return CScriptVal();
return reader.GetMapSettings(guiManager->GetScriptInterface()).get();
}
CScriptVal GetMapSettings(void* cbdata)
{
CGUIManager* guiManager = static_cast (cbdata);
if (!g_Game)
return CScriptVal();
return guiManager->GetScriptInterface().CloneValueFromOtherContext(
g_Game->GetSimulation2()->GetScriptInterface(),
g_Game->GetSimulation2()->GetMapSettings().get());
}
/**
* Get the current X coordinate of the camera.
*/
float CameraGetX(void* UNUSED(cbdata))
{
if (g_Game && g_Game->GetView())
return g_Game->GetView()->GetCameraX();
return -1;
}
/**
* Get the current Z coordinate of the camera.
*/
float CameraGetZ(void* UNUSED(cbdata))
{
if (g_Game && g_Game->GetView())
return g_Game->GetView()->GetCameraZ();
return -1;
}
/**
* Start / stop camera following mode
* @param entityid unit id to follow. If zero, stop following mode
*/
void CameraFollow(void* UNUSED(cbdata), entity_id_t entityid)
{
if (g_Game && g_Game->GetView())
g_Game->GetView()->CameraFollow(entityid, false);
}
/**
* Start / stop first-person camera following mode
* @param entityid unit id to follow. If zero, stop following mode
*/
void CameraFollowFPS(void* UNUSED(cbdata), entity_id_t entityid)
{
if (g_Game && g_Game->GetView())
g_Game->GetView()->CameraFollow(entityid, true);
}
/// Move camera to a 2D location
void CameraMoveTo(void* UNUSED(cbdata), entity_pos_t x, entity_pos_t z)
{
// called from JS; must not fail
if(!(g_Game && g_Game->GetWorld() && g_Game->GetView() && g_Game->GetWorld()->GetTerrain()))
return;
CTerrain* terrain = g_Game->GetWorld()->GetTerrain();
CVector3D target;
target.X = x.ToFloat();
target.Z = z.ToFloat();
target.Y = terrain->GetExactGroundLevel(target.X, target.Z);
g_Game->GetView()->MoveCameraTarget(target);
}
entity_id_t GetFollowedEntity(void* UNUSED(cbdata))
{
if (g_Game && g_Game->GetView())
return g_Game->GetView()->GetFollowedEntity();
return INVALID_ENTITY;
}
bool HotkeyIsPressed_(void* UNUSED(cbdata), std::string hotkeyName)
{
return HotkeyIsPressed(hotkeyName);
}
void DisplayErrorDialog(void* UNUSED(cbdata), std::wstring msg)
{
debug_DisplayError(msg.c_str(), DE_NO_DEBUG_INFO, NULL, NULL, NULL, 0, NULL, NULL);
}
CScriptVal GetProfilerState(void* cbdata)
{
CGUIManager* guiManager = static_cast (cbdata);
return g_ProfileViewer.SaveToJS(guiManager->GetScriptInterface());
}
bool IsUserReportEnabled(void* UNUSED(cbdata))
{
return g_UserReporter.IsReportingEnabled();
}
-bool IsSplashScreenEnabled(void* UNUSED(cbdata))
-{
- bool splashScreenEnable = true;
- CFG_GET_VAL("splashscreenenable", Bool, splashScreenEnable);
- return splashScreenEnable;
-}
-
-void SetSplashScreenEnabled(void* UNUSED(cbdata), bool enabled)
-{
- CStr val = (enabled ? "true" : "false");
- g_ConfigDB.CreateValue(CFG_USER, "splashscreenenable")->m_String = val;
- g_ConfigDB.WriteFile(CFG_USER);
-}
-
-
void SetUserReportEnabled(void* UNUSED(cbdata), bool enabled)
{
g_UserReporter.SetReportingEnabled(enabled);
}
std::string GetUserReportStatus(void* UNUSED(cbdata))
{
return g_UserReporter.GetStatus();
}
void SubmitUserReport(void* UNUSED(cbdata), std::string type, int version, std::wstring data)
{
g_UserReporter.SubmitReport(type.c_str(), version, utf8_from_wstring(data));
}
void SetSimRate(void* UNUSED(cbdata), float rate)
{
g_Game->SetSimRate(rate);
}
float GetSimRate(void* UNUSED(cbdata))
{
return g_Game->GetSimRate();
}
void SetTurnLength(void* UNUSED(cbdata), int length)
{
if (g_NetServer)
g_NetServer->SetTurnLength(length);
else
LOGERROR(L"Only network host can change turn length");
}
// Focus the game camera on a given position.
void SetCameraTarget(void* UNUSED(cbdata), float x, float y, float z)
{
g_Game->GetView()->ResetCameraTarget(CVector3D(x, y, z));
}
// Deliberately cause the game to crash.
// Currently implemented via access violation (read of address 0).
// Useful for testing the crashlog/stack trace code.
int Crash(void* UNUSED(cbdata))
{
debug_printf(L"Crashing at user's request.\n");
return *(volatile int*)0;
}
void DebugWarn(void* UNUSED(cbdata))
{
debug_warn(L"Warning at user's request.");
}
// Force a JS garbage collection cycle to take place immediately.
// Writes an indication of how long this took to the console.
void ForceGC(void* cbdata)
{
CGUIManager* guiManager = static_cast (cbdata);
double time = timer_Time();
JS_GC(guiManager->GetScriptInterface().GetContext());
time = timer_Time() - time;
g_Console->InsertMessage(L"Garbage collection completed in: %f", time);
}
void DumpSimState(void* UNUSED(cbdata))
{
OsPath path = psLogDir()/"sim_dump.txt";
std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
g_Game->GetSimulation2()->DumpDebugState(file);
}
void DumpTerrainMipmap(void* UNUSED(cbdata))
{
VfsPath filename(L"screenshots/terrainmipmap.png");
g_Game->GetWorld()->GetTerrain()->GetHeightMipmap().DumpToDisk(filename);
OsPath realPath;
g_VFS->GetRealPath(filename, realPath);
LOGMESSAGERENDER(L"Terrain mipmap written to '%ls'", realPath.string().c_str());
}
void EnableTimeWarpRecording(void* UNUSED(cbdata), unsigned int numTurns)
{
g_Game->GetTurnManager()->EnableTimeWarpRecording(numTurns);
}
void RewindTimeWarp(void* UNUSED(cbdata))
{
g_Game->GetTurnManager()->RewindTimeWarp();
}
void QuickSave(void* UNUSED(cbdata))
{
g_Game->GetTurnManager()->QuickSave();
}
void QuickLoad(void* UNUSED(cbdata))
{
g_Game->GetTurnManager()->QuickLoad();
}
void SetBoundingBoxDebugOverlay(void* UNUSED(cbdata), bool enabled)
{
ICmpSelectable::ms_EnableDebugOverlays = enabled;
}
} // namespace
void GuiScriptingInit(ScriptInterface& scriptInterface)
{
JSI_GameView::RegisterScriptFunctions(scriptInterface);
JSI_Renderer::RegisterScriptFunctions(scriptInterface);
JSI_Console::RegisterScriptFunctions(scriptInterface);
JSI_ConfigDB::RegisterScriptFunctions(scriptInterface);
// GUI manager functions:
scriptInterface.RegisterFunction("GetActiveGui");
scriptInterface.RegisterFunction("PushGuiPage");
scriptInterface.RegisterFunction("SwitchGuiPage");
scriptInterface.RegisterFunction("PopGuiPage");
// Simulation<->GUI interface functions:
scriptInterface.RegisterFunction("GuiInterfaceCall");
scriptInterface.RegisterFunction("PostNetworkCommand");
// Entity picking
scriptInterface.RegisterFunction, int, int, &PickEntitiesAtPoint>("PickEntitiesAtPoint");
scriptInterface.RegisterFunction, int, int, int, int, int, &PickFriendlyEntitiesInRect>("PickFriendlyEntitiesInRect");
scriptInterface.RegisterFunction, int, &PickFriendlyEntitiesOnScreen>("PickFriendlyEntitiesOnScreen");
scriptInterface.RegisterFunction, std::string, bool, bool, bool, &PickSimilarFriendlyEntities>("PickSimilarFriendlyEntities");
scriptInterface.RegisterFunction("GetTerrainAtScreenPoint");
// Network / game setup functions
scriptInterface.RegisterFunction("StartNetworkGame");
scriptInterface.RegisterFunction("StartGame");
scriptInterface.RegisterFunction("StartNetworkHost");
scriptInterface.RegisterFunction("StartNetworkJoin");
scriptInterface.RegisterFunction("DisconnectNetworkGame");
scriptInterface.RegisterFunction("PollNetworkClient");
scriptInterface.RegisterFunction("SetNetworkGameAttributes");
scriptInterface.RegisterFunction("AssignNetworkPlayer");
scriptInterface.RegisterFunction("SendNetworkChat");
scriptInterface.RegisterFunction, &GetAIs>("GetAIs");
// Saved games
scriptInterface.RegisterFunction("StartSavedGame");
scriptInterface.RegisterFunction, &GetSavedGames>("GetSavedGames");
scriptInterface.RegisterFunction("DeleteSavedGame");
scriptInterface.RegisterFunction("SaveGame");
scriptInterface.RegisterFunction("SaveGamePrefix");
scriptInterface.RegisterFunction("QuickSave");
scriptInterface.RegisterFunction("QuickLoad");
// Misc functions
scriptInterface.RegisterFunction("SetCursor");
scriptInterface.RegisterFunction("GetPlayerID");
scriptInterface.RegisterFunction("SetPlayerID");
scriptInterface.RegisterFunction("OpenURL");
scriptInterface.RegisterFunction("GetMatchID");
scriptInterface.RegisterFunction("RestartInAtlas");
scriptInterface.RegisterFunction("AtlasIsAvailable");
scriptInterface.RegisterFunction("IsAtlasRunning");
scriptInterface.RegisterFunction("LoadMapSettings");
scriptInterface.RegisterFunction("GetMapSettings");
scriptInterface.RegisterFunction("CameraGetX");
scriptInterface.RegisterFunction("CameraGetZ");
scriptInterface.RegisterFunction("CameraFollow");
scriptInterface.RegisterFunction("CameraFollowFPS");
scriptInterface.RegisterFunction("CameraMoveTo");
scriptInterface.RegisterFunction("GetFollowedEntity");
scriptInterface.RegisterFunction("HotkeyIsPressed");
scriptInterface.RegisterFunction("DisplayErrorDialog");
scriptInterface.RegisterFunction("GetProfilerState");
// User report functions
scriptInterface.RegisterFunction("IsUserReportEnabled");
scriptInterface.RegisterFunction("SetUserReportEnabled");
scriptInterface.RegisterFunction("GetUserReportStatus");
scriptInterface.RegisterFunction("SubmitUserReport");
- // Splash screen functions
- scriptInterface.RegisterFunction("IsSplashScreenEnabled");
- scriptInterface.RegisterFunction("SetSplashScreenEnabled");
-
// Development/debugging functions
scriptInterface.RegisterFunction("SetSimRate");
scriptInterface.RegisterFunction("GetSimRate");
scriptInterface.RegisterFunction("SetTurnLength");
scriptInterface.RegisterFunction("SetCameraTarget");
scriptInterface.RegisterFunction("Crash");
scriptInterface.RegisterFunction("DebugWarn");
scriptInterface.RegisterFunction("ForceGC");
scriptInterface.RegisterFunction("DumpSimState");
scriptInterface.RegisterFunction("DumpTerrainMipmap");
scriptInterface.RegisterFunction("EnableTimeWarpRecording");
scriptInterface.RegisterFunction("RewindTimeWarp");
scriptInterface.RegisterFunction("SetBoundingBoxDebugOverlay");
// Lobby functions
scriptInterface.RegisterFunction("HasXmppClient");
#if CONFIG2_LOBBY // Allow the lobby to be disabled
scriptInterface.RegisterFunction("StartXmppClient");
scriptInterface.RegisterFunction("StartRegisterXmppClient");
scriptInterface.RegisterFunction("StopXmppClient");
scriptInterface.RegisterFunction("ConnectXmppClient");
scriptInterface.RegisterFunction("DisconnectXmppClient");
scriptInterface.RegisterFunction("RecvXmppClient");
scriptInterface.RegisterFunction("SendGetGameList");
scriptInterface.RegisterFunction("SendGetBoardList");
scriptInterface.RegisterFunction("SendRegisterGame");
scriptInterface.RegisterFunction("SendGameReport");
scriptInterface.RegisterFunction("SendUnregisterGame");
scriptInterface.RegisterFunction("SendChangeStateGame");
scriptInterface.RegisterFunction("GetPlayerList");
scriptInterface.RegisterFunction("GetGameList");
scriptInterface.RegisterFunction("GetBoardList");
scriptInterface.RegisterFunction("LobbyGuiPollMessage");
scriptInterface.RegisterFunction("LobbySendMessage");
scriptInterface.RegisterFunction("LobbySetPlayerPresence");
scriptInterface.RegisterFunction("LobbySetNick");
scriptInterface.RegisterFunction("LobbyGetNick");
scriptInterface.RegisterFunction("LobbyKick");
scriptInterface.RegisterFunction("LobbyBan");
scriptInterface.RegisterFunction("LobbyGetPlayerPresence");
scriptInterface.RegisterFunction("EncryptPassword");
scriptInterface.RegisterFunction("IsRankedGame");
scriptInterface.RegisterFunction("SetRankedGame");
#endif // CONFIG2_LOBBY
}
Index: ps/trunk/source/network/NetServer.cpp
===================================================================
--- ps/trunk/source/network/NetServer.cpp (revision 14436)
+++ ps/trunk/source/network/NetServer.cpp (revision 14437)
@@ -1,1073 +1,1073 @@
/* Copyright (C) 2013 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "NetServer.h"
#include "NetClient.h"
#include "NetMessage.h"
#include "NetSession.h"
#include "NetStats.h"
#include "NetTurnManager.h"
#include "lib/external_libraries/enet.h"
#include "ps/CLogger.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
#include "ps/ConfigDB.h"
#if CONFIG2_MINIUPNPC
// Next four files are for UPnP port forwarding.
#include
#include
#include
#include
#endif
#define DEFAULT_SERVER_NAME L"Unnamed Server"
#define DEFAULT_WELCOME_MESSAGE L"Welcome"
#define MAX_CLIENTS 8
static const int CHANNEL_COUNT = 1;
/**
* enet_host_service timeout (msecs).
* Smaller numbers may hurt performance; larger numbers will
* hurt latency responding to messages from game thread.
*/
static const int HOST_SERVICE_TIMEOUT = 50;
CNetServer* g_NetServer = NULL;
static CStr DebugName(CNetServerSession* session)
{
if (session == NULL)
return "[unknown host]";
if (session->GetGUID().empty())
return "[unauthed host]";
return "[" + session->GetGUID().substr(0, 8) + "...]";
}
/**
* Async task for receiving the initial game state to be forwarded to another
* client that is rejoining an in-progress network game.
*/
class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask
{
NONCOPYABLE(CNetFileReceiveTask_ServerRejoin);
public:
CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID)
: m_Server(server), m_RejoinerHostID(hostID)
{
}
virtual void OnComplete()
{
// We've received the game state from an existing player - now
// we need to send it onwards to the newly rejoining player
// Find the session corresponding to the rejoining host (if any)
CNetServerSession* session = NULL;
for (size_t i = 0; i < m_Server.m_Sessions.size(); ++i)
{
if (m_Server.m_Sessions[i]->GetHostID() == m_RejoinerHostID)
{
session = m_Server.m_Sessions[i];
break;
}
}
if (!session)
{
LOGMESSAGE(L"Net server: rejoining client disconnected before we sent to it");
return;
}
// Store the received state file, and tell the client to start downloading it from us
// TODO: this will get kind of confused if there's multiple clients downloading in parallel;
// they'll race and get whichever happens to be the latest received by the server,
// which should still work but isn't great
m_Server.m_JoinSyncFile = m_Buffer;
CJoinSyncStartMessage message;
session->SendMessage(&message);
}
private:
CNetServerWorker& m_Server;
u32 m_RejoinerHostID;
};
/*
* XXX: We use some non-threadsafe functions from the worker thread.
* See http://trac.wildfiregames.com/ticket/654
*/
CNetServerWorker::CNetServerWorker(int autostartPlayers) :
m_AutostartPlayers(autostartPlayers),
m_Shutdown(false),
m_ScriptInterface(NULL),
m_NextHostID(1), m_Host(NULL), m_Stats(NULL)
{
m_State = SERVER_STATE_UNCONNECTED;
m_ServerTurnManager = NULL;
m_ServerName = DEFAULT_SERVER_NAME;
m_WelcomeMessage = DEFAULT_WELCOME_MESSAGE;
}
CNetServerWorker::~CNetServerWorker()
{
if (m_State != SERVER_STATE_UNCONNECTED)
{
// Tell the thread to shut down
{
CScopeLock lock(m_WorkerMutex);
m_Shutdown = true;
}
// Wait for it to shut down cleanly
pthread_join(m_WorkerThread, NULL);
}
// Clean up resources
delete m_Stats;
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
m_Sessions[i]->DisconnectNow(NDR_UNEXPECTED_SHUTDOWN);
delete m_Sessions[i];
}
if (m_Host)
{
enet_host_destroy(m_Host);
}
delete m_ServerTurnManager;
}
bool CNetServerWorker::SetupConnection()
{
ENSURE(m_State == SERVER_STATE_UNCONNECTED);
ENSURE(!m_Host);
// Bind to default host
ENetAddress addr;
addr.host = ENET_HOST_ANY;
addr.port = PS_DEFAULT_PORT;
// Create ENet server
m_Host = enet_host_create(&addr, MAX_CLIENTS, CHANNEL_COUNT, 0, 0);
if (!m_Host)
{
LOGERROR(L"Net server: enet_host_create failed");
return false;
}
m_Stats = new CNetStatsTable();
if (CProfileViewer::IsInitialised())
g_ProfileViewer.AddRootTable(m_Stats);
m_State = SERVER_STATE_PREGAME;
// Launch the worker thread
int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this);
ENSURE(ret == 0);
#if CONFIG2_MINIUPNPC
// Launch the UPnP thread
ret = pthread_create(&m_UPnPThread, NULL, &SetupUPnP, NULL);
ENSURE(ret == 0);
#endif
return true;
}
#if CONFIG2_MINIUPNPC
void* CNetServerWorker::SetupUPnP(void*)
{
// Values we want to set.
char psPort[6];
sprintf_s(psPort, ARRAY_SIZE(psPort), "%d", PS_DEFAULT_PORT);
const char* leaseDuration = "0"; // Indefinite/permanent lease duration.
const char* description = "0AD Multiplayer";
const char* protocall = "UDP";
char internalIPAddress[64];
char externalIPAddress[40];
// Variables to hold the values that actually get set.
char intClient[40];
char intPort[6];
char duration[16];
// Intermediate variables.
struct UPNPUrls urls;
struct IGDdatas data;
struct UPNPDev* devlist = 0;
// Cached root descriptor URL.
- std::string rootDescURL = "";
+ std::string rootDescURL;
CFG_GET_VAL("network.upnprootdescurl", String, rootDescURL);
- if (rootDescURL != "")
+ if (!rootDescURL.empty())
LOGMESSAGE(L"Net server: attempting to use cached root descriptor URL: %hs", rootDescURL.c_str());
// Init the return variable for UPNP_GetValidIGD to 1 so things behave when using cached URLs.
int ret = 1;
// If we have a cached URL, try that first, otherwise try getting a valid UPnP device for 10 seconds. We also get our LAN address here.
- if (!((rootDescURL != "" && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress)))
+ if (!((!rootDescURL.empty() && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress)))
|| ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 0)) && (ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress))))))
{
LOGMESSAGE(L"Net server: upnpDiscover failed and no working cached URL.");
return NULL;
}
switch (ret)
{
case 1:
LOGMESSAGE(L"Net server: found valid IGD = %hs", urls.controlURL);
break;
case 2:
LOGMESSAGE(L"Net server: found a valid, not connected IGD = %hs, will try to continue anyway", urls.controlURL);
break;
case 3:
LOGMESSAGE(L"Net server: found a UPnP device unrecognized as IGD = %hs, will try to continue anyway", urls.controlURL);
break;
default:
debug_warn(L"Unrecognized return value from UPNP_GetValidIGD");
}
// Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance.
ret = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIPAddress);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE(L"Net server: GetExternalIPAddress failed with code %d (%hs)", ret, strupnperror(ret));
return NULL;
}
LOGMESSAGE(L"Net server: ExternalIPAddress = %hs", externalIPAddress);
// Try to setup port forwarding.
ret = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, psPort, psPort,
internalIPAddress, description, protocall, 0, leaseDuration);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE(L"Net server: AddPortMapping(%hs, %hs, %hs) failed with code %d (%hs)",
psPort, psPort, internalIPAddress, ret, strupnperror(ret));
return NULL;
}
// Check that the port was actually forwarded.
ret = UPNP_GetSpecificPortMappingEntry(urls.controlURL,
data.first.servicetype,
psPort, protocall,
intClient, intPort, NULL/*desc*/,
NULL/*enabled*/, duration);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE(L"Net server: GetSpecificPortMappingEntry() failed with code %d (%hs)", ret, strupnperror(ret));
return NULL;
}
LOGMESSAGE(L"Net server: External %hs:%hs %hs is redirected to internal %hs:%hs (duration=%hs)",
externalIPAddress, psPort, protocall, intClient, intPort, duration);
// Cache root descriptor URL to try to avoid discovery next time.
- g_ConfigDB.CreateValue(CFG_USER, "network.upnprootdescurl")->m_String = urls.controlURL;
+ g_ConfigDB.SetValueString(CFG_USER, "network.upnprootdescurl", urls.controlURL);
g_ConfigDB.WriteFile(CFG_USER);
LOGMESSAGE(L"Net server: cached UPnP root descriptor URL as %hs", urls.controlURL);
// Make sure everything is properly freed.
FreeUPNPUrls(&urls);
freeUPNPDevlist(devlist);
return NULL;
}
#endif // CONFIG2_MINIUPNPC
bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message)
{
ENSURE(m_Host);
CNetServerSession* session = static_cast(peer->data);
return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
}
bool CNetServerWorker::Broadcast(const CNetMessage* message)
{
ENSURE(m_Host);
bool ok = true;
// Send to all sessions that are active and has finished authentication
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
if (m_Sessions[i]->GetCurrState() == NSS_PREGAME || m_Sessions[i]->GetCurrState() == NSS_INGAME)
{
if (!m_Sessions[i]->SendMessage(message))
ok = false;
// TODO: this does lots of repeated message serialisation if we have lots
// of remote peers; could do it more efficiently if that's a real problem
}
}
return ok;
}
void* CNetServerWorker::RunThread(void* data)
{
debug_SetThreadName("NetServer");
static_cast(data)->Run();
return NULL;
}
void CNetServerWorker::Run()
{
// To avoid the need for JS_SetContextThread, we create and use and destroy
// the script interface entirely within this network thread
m_ScriptInterface = new ScriptInterface("Engine", "Net server", ScriptInterface::CreateRuntime());
while (true)
{
if (!RunStep())
break;
// Implement autostart mode
if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers)
StartGame();
// Update profiler stats
m_Stats->LatchHostState(m_Host);
}
// Clear roots before deleting their context
m_GameAttributes = CScriptValRooted();
m_SavedCommands.clear();
SAFE_DELETE(m_ScriptInterface);
}
bool CNetServerWorker::RunStep()
{
// Check for messages from the game thread.
// (Do as little work as possible while the mutex is held open,
// to avoid performance problems and deadlocks.)
std::vector > newAssignPlayer;
std::vector newStartGame;
std::vector newGameAttributes;
std::vector newTurnLength;
{
CScopeLock lock(m_WorkerMutex);
if (m_Shutdown)
return false;
newStartGame.swap(m_StartGameQueue);
newAssignPlayer.swap(m_AssignPlayerQueue);
newGameAttributes.swap(m_GameAttributesQueue);
newTurnLength.swap(m_TurnLengthQueue);
}
for (size_t i = 0; i < newAssignPlayer.size(); ++i)
AssignPlayer(newAssignPlayer[i].first, newAssignPlayer[i].second);
if (!newGameAttributes.empty())
UpdateGameAttributes(GetScriptInterface().ParseJSON(newGameAttributes.back()));
if (!newTurnLength.empty())
SetTurnLength(newTurnLength.back());
// Do StartGame last, so we have the most up-to-date game attributes when we start
if (!newStartGame.empty())
StartGame();
// Perform file transfers
for (size_t i = 0; i < m_Sessions.size(); ++i)
m_Sessions[i]->GetFileTransferer().Poll();
// Process network events:
ENetEvent event;
int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT);
if (status < 0)
{
LOGERROR(L"CNetServerWorker: enet_host_service failed (%d)", status);
// TODO: notify game that the server has shut down
return false;
}
if (status == 0)
{
// Reached timeout with no events - try again
return true;
}
// Process the event:
switch (event.type)
{
case ENET_EVENT_TYPE_CONNECT:
{
// Report the client address
char hostname[256] = "(error)";
enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
LOGMESSAGE(L"Net server: Received connection from %hs:%u", hostname, (unsigned int)event.peer->address.port);
// Set up a session object for this peer
CNetServerSession* session = new CNetServerSession(*this, event.peer);
m_Sessions.push_back(session);
SetupSession(session);
ENSURE(event.peer->data == NULL);
event.peer->data = session;
HandleConnect(session);
break;
}
case ENET_EVENT_TYPE_DISCONNECT:
{
// If there is an active session with this peer, then reset and delete it
CNetServerSession* session = static_cast(event.peer->data);
if (session)
{
LOGMESSAGE(L"Net server: Disconnected %hs", DebugName(session).c_str());
// Remove the session first, so we won't send player-update messages to it
// when updating the FSM
m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end());
session->Update((uint)NMT_CONNECTION_LOST, NULL);
delete session;
event.peer->data = NULL;
}
break;
}
case ENET_EVENT_TYPE_RECEIVE:
{
// If there is an active session with this peer, then process the message
CNetServerSession* session = static_cast(event.peer->data);
if (session)
{
// Create message from raw data
CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
if (msg)
{
LOGMESSAGE(L"Net server: Received message %hs of size %lu from %hs", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str());
HandleMessageReceive(msg, session);
delete msg;
}
}
// Done using the packet
enet_packet_destroy(event.packet);
break;
}
case ENET_EVENT_TYPE_NONE:
break;
}
return true;
}
void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session)
{
// Handle non-FSM messages first
Status status = session->GetFileTransferer().HandleMessageReceive(message);
if (status != INFO::SKIPPED)
return;
if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
{
CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message;
// Rejoining client got our JoinSyncStart after we received the state from
// another client, and has now requested that we forward it to them
ENSURE(!m_JoinSyncFile.empty());
session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile);
return;
}
// Update FSM
bool ok = session->Update(message->GetType(), (void*)message);
if (!ok)
LOGERROR(L"Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState());
}
void CNetServerWorker::SetupSession(CNetServerSession* session)
{
void* context = session;
// Set up transitions for session
session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, (void*)&OnClientHandshake, context);
session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, (void*)&OnChat, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnJoinSyncingLoadedGame, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, (void*)&OnChat, context);
session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, (void*)&OnInGame, context);
session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnInGame, context);
session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnInGame, context);
// Set first state
session->SetFirstState(NSS_HANDSHAKE);
}
bool CNetServerWorker::HandleConnect(CNetServerSession* session)
{
CSrvHandshakeMessage handshake;
handshake.m_Magic = PS_PROTOCOL_MAGIC;
handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
return session->SendMessage(&handshake);
}
void CNetServerWorker::OnUserJoin(CNetServerSession* session)
{
AddPlayer(session->GetGUID(), session->GetUserName());
CGameSetupMessage gameSetupMessage(GetScriptInterface());
gameSetupMessage.m_Data = m_GameAttributes;
session->SendMessage(&gameSetupMessage);
CPlayerAssignmentMessage assignMessage;
ConstructPlayerAssignmentMessage(assignMessage);
session->SendMessage(&assignMessage);
}
void CNetServerWorker::OnUserLeave(CNetServerSession* session)
{
RemovePlayer(session->GetGUID());
if (m_ServerTurnManager)
m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers
// TODO: ought to switch the player controlled by that client
// back to AI control, or something?
}
void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name)
{
// Find all player IDs in active use; we mustn't give them to a second player
std::set usedIDs;
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
if (it->second.m_Enabled)
usedIDs.insert(it->second.m_PlayerID);
// If the player is rejoining after disconnecting, try to give them
// back their old player ID
i32 playerID = -1;
bool foundPlayerID = false;
// Try to match GUID first
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
{
playerID = it->second.m_PlayerID;
foundPlayerID = true;
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
break;
}
}
// Try to match username next
if (!foundPlayerID)
{
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
{
playerID = it->second.m_PlayerID;
foundPlayerID = true;
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
break;
}
}
}
// Otherwise pick the first free player ID
if (!foundPlayerID)
{
for (playerID = 1; usedIDs.find(playerID) != usedIDs.end(); ++playerID)
{
// (do nothing)
}
}
PlayerAssignment assignment;
assignment.m_Enabled = true;
assignment.m_Name = name;
assignment.m_PlayerID = playerID;
m_PlayerAssignments[guid] = assignment;
// Send the new assignments to all currently active players
// (which does not include the one that's just joining)
SendPlayerAssignments();
}
void CNetServerWorker::RemovePlayer(const CStr& guid)
{
m_PlayerAssignments[guid].m_Enabled = false;
SendPlayerAssignments();
}
void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid)
{
// Remove anyone who's already assigned to this player
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (it->second.m_PlayerID == playerID)
it->second.m_PlayerID = -1;
}
// Update this host's assignment if it exists
if (m_PlayerAssignments.find(guid) != m_PlayerAssignments.end())
m_PlayerAssignments[guid].m_PlayerID = playerID;
SendPlayerAssignments();
}
void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message)
{
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled)
continue;
CPlayerAssignmentMessage::S_m_Hosts h;
h.m_GUID = it->first;
h.m_Name = it->second.m_Name;
h.m_PlayerID = it->second.m_PlayerID;
message.m_Hosts.push_back(h);
}
}
void CNetServerWorker::SendPlayerAssignments()
{
CPlayerAssignmentMessage message;
ConstructPlayerAssignmentMessage(message);
Broadcast(&message);
}
ScriptInterface& CNetServerWorker::GetScriptInterface()
{
return *m_ScriptInterface;
}
void CNetServerWorker::SetTurnLength(u32 msecs)
{
if (m_ServerTurnManager)
m_ServerTurnManager->SetTurnLength(msecs);
}
bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef();
if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION)
{
session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION);
return false;
}
CSrvHandshakeResponseMessage handshakeResponse;
handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION;
handshakeResponse.m_Message = server.m_WelcomeMessage;
handshakeResponse.m_Flags = 0;
session->SendMessage(&handshakeResponse);
return true;
}
bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef();
CStrW username = server.DeduplicatePlayerName(SanitisePlayerName(message->m_Name));
bool isRejoining = false;
if (server.m_State != SERVER_STATE_PREGAME)
{
// isRejoining = true; // uncomment this to test rejoining even if the player wasn't connected previously
// Search for an old disconnected player of the same name
// (TODO: if GUIDs were stable, we should use them instead)
for (PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.begin(); it != server.m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled && it->second.m_Name == username)
{
isRejoining = true;
break;
}
}
// Players who weren't already in the game are not allowed to join now that it's started
if (!isRejoining)
{
LOGMESSAGE(L"Refused connection after game start from not-previously-known user \"%ls\"", username.c_str());
session->Disconnect(NDR_SERVER_ALREADY_IN_GAME);
return true;
}
}
// TODO: check server password etc?
u32 newHostID = server.m_NextHostID++;
session->SetUserName(username);
session->SetGUID(message->m_GUID);
session->SetHostID(newHostID);
CAuthenticateResultMessage authenticateResult;
authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK;
authenticateResult.m_HostID = newHostID;
authenticateResult.m_Message = L"Logged in";
session->SendMessage(&authenticateResult);
server.OnUserJoin(session);
if (isRejoining)
{
// Request a copy of the current game state from an existing player,
// so we can send it on to the new player
// Assume session 0 is most likely the local player, so they're
// the most efficient client to request a copy from
CNetServerSession* sourceSession = server.m_Sessions.at(0);
sourceSession->GetFileTransferer().StartTask(
shared_ptr(new CNetFileReceiveTask_ServerRejoin(server, newHostID))
);
session->SetNextState(NSS_JOIN_SYNCING);
}
return true;
}
bool CNetServerWorker::OnInGame(void* context, CFsmEvent* event)
{
// TODO: should split each of these cases into a separate method
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CNetMessage* message = (CNetMessage*)event->GetParamRef();
if (message->GetType() == (uint)NMT_SIMULATION_COMMAND)
{
CSimulationMessage* simMessage = static_cast (message);
// Send it back to all clients immediately
server.Broadcast(simMessage);
// Save all the received commands
if (server.m_SavedCommands.size() < simMessage->m_Turn + 1)
server.m_SavedCommands.resize(simMessage->m_Turn + 1);
server.m_SavedCommands[simMessage->m_Turn].push_back(*simMessage);
// TODO: we should do some validation of ownership (clients can't send commands on behalf of opposing players)
// TODO: we shouldn't send the message back to the client that first sent it
}
else if (message->GetType() == (uint)NMT_SYNC_CHECK)
{
CSyncCheckMessage* syncMessage = static_cast (message);
server.m_ServerTurnManager->NotifyFinishedClientUpdate(session->GetHostID(), syncMessage->m_Turn, syncMessage->m_Hash);
}
else if (message->GetType() == (uint)NMT_END_COMMAND_BATCH)
{
CEndCommandBatchMessage* endMessage = static_cast (message);
server.m_ServerTurnManager->NotifyFinishedClientCommands(session->GetHostID(), endMessage->m_Turn);
}
return true;
}
bool CNetServerWorker::OnChat(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CHAT);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CChatMessage* message = (CChatMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message);
return true;
}
bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// We're in the loading state, so wait until every player has loaded before
// starting the game
ENSURE(server.m_State == SERVER_STATE_LOADING);
server.CheckGameLoadStatus(session);
return true;
}
bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event)
{
// A client rejoining an in-progress game has now finished loading the
// map and deserialized the initial state.
// The simulation may have progressed since then, so send any subsequent
// commands to them and set them as an active player so they can participate
// in all future turns.
//
// (TODO: if it takes a long time for them to receive and execute all these
// commands, the other players will get frozen for that time and may be unhappy;
// we could try repeating this process a few times until the client converges
// on the up-to-date state, before setting them as active.)
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
u32 turn = message->m_CurrentTurn;
u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn();
// Send them all commands received since their saved state,
// and turn-ended messages for any turns that have already been processed
for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i)
{
if (i < server.m_SavedCommands.size())
for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j)
session->SendMessage(&server.m_SavedCommands[i][j]);
if (i <= readyTurn)
{
CEndCommandBatchMessage endMessage;
endMessage.m_Turn = i;
endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i);
session->SendMessage(&endMessage);
}
}
// Tell the turn manager to expect commands from this new client
server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn);
// Tell the client that everything has finished loading and it should start now
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = readyTurn;
session->SendMessage(&loaded);
return true;
}
bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
server.OnUserLeave(session);
return true;
}
void CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
{
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
if (m_Sessions[i] != changedSession && m_Sessions[i]->GetCurrState() != NSS_INGAME)
return;
}
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = 0;
Broadcast(&loaded);
m_State = SERVER_STATE_INGAME;
}
void CNetServerWorker::StartGame()
{
m_ServerTurnManager = new CNetServerTurnManager(*this);
for (size_t i = 0; i < m_Sessions.size(); ++i)
m_ServerTurnManager->InitialiseClient(m_Sessions[i]->GetHostID(), 0); // TODO: only for non-observers
m_State = SERVER_STATE_LOADING;
// Send the final setup state to all clients
UpdateGameAttributes(m_GameAttributes);
SendPlayerAssignments();
CGameStartMessage gameStart;
Broadcast(&gameStart);
}
void CNetServerWorker::UpdateGameAttributes(const CScriptValRooted& attrs)
{
m_GameAttributes = attrs;
if (!m_Host)
return;
CGameSetupMessage gameSetupMessage(GetScriptInterface());
gameSetupMessage.m_Data = m_GameAttributes;
Broadcast(&gameSetupMessage);
}
CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
{
const size_t MAX_LENGTH = 32;
CStrW name = original;
name.Replace(L"[", L"{"); // remove GUI tags
name.Replace(L"]", L"}"); // remove for symmetry
// Restrict the length
if (name.length() > MAX_LENGTH)
name = name.Left(MAX_LENGTH);
// Don't allow surrounding whitespace
name.Trim(PS_TRIM_BOTH);
// Don't allow empty name
if (name.empty())
name = L"Anonymous";
return name;
}
CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
{
CStrW name = original;
// Try names "Foo", "Foo (2)", "Foo (3)", etc
size_t id = 2;
while (true)
{
bool unique = true;
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
if (m_Sessions[i]->GetUserName() == name)
{
unique = false;
break;
}
}
if (unique)
return name;
name = original + L" (" + CStrW::FromUInt(id++) + L")";
}
}
CNetServer::CNetServer(int autostartPlayers) :
m_Worker(new CNetServerWorker(autostartPlayers))
{
}
CNetServer::~CNetServer()
{
delete m_Worker;
}
bool CNetServer::SetupConnection()
{
return m_Worker->SetupConnection();
}
void CNetServer::AssignPlayer(int playerID, const CStr& guid)
{
CScopeLock lock(m_Worker->m_WorkerMutex);
m_Worker->m_AssignPlayerQueue.push_back(std::make_pair(playerID, guid));
}
void CNetServer::StartGame()
{
CScopeLock lock(m_Worker->m_WorkerMutex);
m_Worker->m_StartGameQueue.push_back(true);
}
void CNetServer::UpdateGameAttributes(const CScriptVal& attrs, ScriptInterface& scriptInterface)
{
// Pass the attributes as JSON, since that's the easiest safe
// cross-thread way of passing script data
std::string attrsJSON = scriptInterface.StringifyJSON(attrs.get(), false);
CScopeLock lock(m_Worker->m_WorkerMutex);
m_Worker->m_GameAttributesQueue.push_back(attrsJSON);
}
void CNetServer::SetTurnLength(u32 msecs)
{
CScopeLock lock(m_Worker->m_WorkerMutex);
m_Worker->m_TurnLengthQueue.push_back(msecs);
}
Index: ps/trunk/source/ps/ConfigDB.cpp
===================================================================
--- ps/trunk/source/ps/ConfigDB.cpp (revision 14436)
+++ ps/trunk/source/ps/ConfigDB.cpp (revision 14437)
@@ -1,289 +1,325 @@
-/* Copyright (C) 2012 Wildfire Games.
+/* Copyright (C) 2013 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 0 A.D. If not, see .
*/
#include "precompiled.h"
#include
#include "Pyrogenesis.h"
#include "Parser.h"
#include "ConfigDB.h"
#include "CLogger.h"
#include "Filesystem.h"
#include "lib/allocators/shared_ptr.h"
typedef std::map TConfigMap;
TConfigMap CConfigDB::m_Map[CFG_LAST];
VfsPath CConfigDB::m_ConfigFile[CFG_LAST];
static pthread_mutex_t cfgdb_mutex = PTHREAD_MUTEX_INITIALIZER;
struct ScopedLock
{
ScopedLock() { pthread_mutex_lock(&cfgdb_mutex); }
~ScopedLock() { pthread_mutex_unlock(&cfgdb_mutex); }
};
CConfigDB::CConfigDB()
{
+ // Recursive mutex needed for WriteFile
pthread_mutexattr_t attr;
int err;
err = pthread_mutexattr_init(&attr);
ENSURE(err == 0);
err = pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
ENSURE(err == 0);
err = pthread_mutex_init(&cfgdb_mutex, &attr);
ENSURE(err == 0);
err = pthread_mutexattr_destroy(&attr);
ENSURE(err == 0);
}
-CConfigValue *CConfigDB::GetValue(EConfigNamespace ns, const CStr& name)
-{
- ScopedLock s;
- CConfigValueSet* values = GetValues(ns, name);
- if (!values)
- return NULL;
- return &((*values)[0]);
-}
+#define GETVAL(T, type) \
+ void CConfigDB::GetValue##T(EConfigNamespace ns, const CStr& name, type& value) \
+ { \
+ if (ns < 0 || ns >= CFG_LAST) \
+ { \
+ debug_warn(L"CConfigDB: Invalid ns value"); \
+ return; \
+ } \
+ ScopedLock s; \
+ TConfigMap::iterator it = m_Map[CFG_COMMAND].find(name); \
+ if (it != m_Map[CFG_COMMAND].end()) \
+ { \
+ it->second[0].Get##T(value); \
+ return; \
+ } \
+ \
+ for (int search_ns = ns; search_ns >= 0; search_ns--) \
+ { \
+ it = m_Map[search_ns].find(name); \
+ if (it != m_Map[search_ns].end()) \
+ { \
+ it->second[0].Get##T(value); \
+ return; \
+ } \
+ } \
+ }
+
+GETVAL(Bool, bool)
+GETVAL(Int, int)
+GETVAL(Float, float)
+GETVAL(Double, double)
+GETVAL(String, std::string)
+
+#undef GETVAL
-CConfigValueSet *CConfigDB::GetValues(EConfigNamespace ns, const CStr& name)
+void CConfigDB::GetValues(EConfigNamespace ns, const CStr& name, CConfigValueSet& values)
{
if (ns < 0 || ns >= CFG_LAST)
{
debug_warn(L"CConfigDB: Invalid ns value");
- return NULL;
+ return;
}
ScopedLock s;
TConfigMap::iterator it = m_Map[CFG_COMMAND].find(name);
if (it != m_Map[CFG_COMMAND].end())
- return &(it->second);
+ {
+ values = it->second;
+ return;
+ }
for (int search_ns = ns; search_ns >= 0; search_ns--)
{
- TConfigMap::iterator it = m_Map[search_ns].find(name);
+ it = m_Map[search_ns].find(name);
if (it != m_Map[search_ns].end())
- return &(it->second);
+ {
+ values = it->second;
+ return;
+ }
}
-
- return NULL;
}
EConfigNamespace CConfigDB::GetValueNamespace(EConfigNamespace ns, const CStr& name)
{
if (ns < 0 || ns >= CFG_LAST)
{
debug_warn(L"CConfigDB: Invalid ns value");
return CFG_LAST;
}
ScopedLock s;
TConfigMap::iterator it = m_Map[CFG_COMMAND].find(name);
if (it != m_Map[CFG_COMMAND].end())
return CFG_COMMAND;
for (int search_ns = ns; search_ns >= 0; search_ns--)
{
- TConfigMap::iterator it = m_Map[search_ns].find(name);
+ it = m_Map[search_ns].find(name);
if (it != m_Map[search_ns].end())
return (EConfigNamespace)search_ns;
}
return CFG_LAST;
}
std::map CConfigDB::GetValuesWithPrefix(EConfigNamespace ns, const CStr& prefix)
{
ScopedLock s;
std::map ret;
if (ns < 0 || ns >= CFG_LAST)
{
debug_warn(L"CConfigDB: Invalid ns value");
return ret;
}
// Loop upwards so that values in later namespaces can override
// values in earlier namespaces
for (int search_ns = 0; search_ns <= ns; search_ns++)
{
for (TConfigMap::iterator it = m_Map[search_ns].begin(); it != m_Map[search_ns].end(); ++it)
{
if (boost::algorithm::starts_with(it->first, prefix))
ret[it->first] = it->second;
}
}
+ for (TConfigMap::iterator it = m_Map[CFG_COMMAND].begin(); it != m_Map[CFG_COMMAND].end(); ++it)
+ {
+ if (boost::algorithm::starts_with(it->first, prefix))
+ ret[it->first] = it->second;
+ }
+
return ret;
}
-CConfigValue *CConfigDB::CreateValue(EConfigNamespace ns, const CStr& name)
+void CConfigDB::SetValueString(EConfigNamespace ns, const CStr& name, const CStr& value)
{
if (ns < 0 || ns >= CFG_LAST)
{
debug_warn(L"CConfigDB: Invalid ns value");
- return NULL;
+ return;
}
ScopedLock s;
TConfigMap::iterator it = m_Map[ns].find(name);
- if (it != m_Map[ns].end())
- return &(it->second[0]);
+ if (it == m_Map[ns].end())
+ it = m_Map[ns].insert(m_Map[ns].begin(), make_pair(name, CConfigValueSet(1)));
- it=m_Map[ns].insert(m_Map[ns].begin(), make_pair(name, CConfigValueSet(1)));
- return &(it->second[0]);
+ it->second[0].m_String = value;
}
void CConfigDB::SetConfigFile(EConfigNamespace ns, const VfsPath& path)
{
if (ns < 0 || ns >= CFG_LAST)
{
debug_warn(L"CConfigDB: Invalid ns value");
return;
}
ScopedLock s;
m_ConfigFile[ns]=path;
}
bool CConfigDB::Reload(EConfigNamespace ns)
{
if (ns < 0 || ns >= CFG_LAST)
{
debug_warn(L"CConfigDB: Invalid ns value");
return false;
}
ScopedLock s;
// Set up CParser
CParser parser;
CParserLine parserLine;
parser.InputTaskType("Assignment", "_$ident_=<_[-$arg(_minus)]_$value_,>_[-$arg(_minus)]_$value[[;]$rest]");
parser.InputTaskType("CommentOrBlank", "_[;[$rest]]");
// Open file with VFS
shared_ptr buffer; size_t buflen;
{
// Handle missing files quietly
if (g_VFS->GetFileInfo(m_ConfigFile[ns], NULL) < 0)
{
LOGMESSAGE(L"Cannot find config file \"%ls\" - ignoring", m_ConfigFile[ns].string().c_str());
return false;
}
else
{
LOGMESSAGE(L"Loading config file \"%ls\"", m_ConfigFile[ns].string().c_str());
Status ret = g_VFS->LoadFile(m_ConfigFile[ns], buffer, buflen);
if (ret != INFO::OK)
{
LOGERROR(L"CConfigDB::Reload(): vfs_load for \"%ls\" failed: return was %lld", m_ConfigFile[ns].string().c_str(), (long long)ret);
return false;
}
}
}
TConfigMap newMap;
char *filebuf=(char *)buffer.get();
char *filebufend=filebuf+buflen;
// Read file line by line
char *next=filebuf-1;
do
{
char *pos=next+1;
next=(char *)memchr(pos, '\n', filebufend-pos);
if (!next) next=filebufend;
char *lend=next;
if (lend > filebuf && *(lend-1) == '\r') lend--;
// Send line to parser
bool parseOk=parserLine.ParseString(parser, std::string(pos, lend));
// Get name and value from parser
std::string name;
std::string value;
if (parseOk &&
parserLine.GetArgCount()>=2 &&
parserLine.GetArgString(0, name) &&
parserLine.GetArgString(1, value))
{
// Add name and value to the map
size_t argCount = parserLine.GetArgCount();
newMap[name].clear();
for( size_t t = 0; t < argCount; t++ )
{
if( !parserLine.GetArgString( (int)t + 1, value ) )
continue;
CConfigValue argument;
argument.m_String = value;
newMap[name].push_back( argument );
LOGMESSAGE(L"Loaded config string \"%hs\" = \"%hs\"", name.c_str(), value.c_str());
}
}
}
while (next < filebufend);
m_Map[ns].swap(newMap);
return true;
}
bool CConfigDB::WriteFile(EConfigNamespace ns)
{
if (ns < 0 || ns >= CFG_LAST)
{
debug_warn(L"CConfigDB: Invalid ns value");
return false;
}
ScopedLock s;
return WriteFile(ns, m_ConfigFile[ns]);
}
bool CConfigDB::WriteFile(EConfigNamespace ns, const VfsPath& path)
{
if (ns < 0 || ns >= CFG_LAST)
{
debug_warn(L"CConfigDB: Invalid ns value");
return false;
}
ScopedLock s;
shared_ptr buf;
AllocateAligned(buf, 1*MiB, maxSectorSize);
char* pos = (char*)buf.get();
TConfigMap &map=m_Map[ns];
for(TConfigMap::const_iterator it = map.begin(); it != map.end(); ++it)
{
pos += sprintf(pos, "%s = \"%s\"\n", it->first.c_str(), it->second[0].m_String.c_str());
}
const size_t len = pos - (char*)buf.get();
Status ret = g_VFS->CreateFile(path, buf, len);
if(ret < 0)
{
LOGERROR(L"CConfigDB::WriteFile(): CreateFile \"%ls\" failed (error: %d)", path.string().c_str(), (int)ret);
return false;
}
return true;
}
Index: ps/trunk/source/ps/ConfigDB.h
===================================================================
--- ps/trunk/source/ps/ConfigDB.h (revision 14436)
+++ ps/trunk/source/ps/ConfigDB.h (revision 14437)
@@ -1,155 +1,150 @@
/* Copyright (C) 2013 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 0 A.D. If not, see .
*/
/*
CConfigDB - Load, access and store configuration variables
TDD : http://www.wildfiregames.com/forum/index.php?showtopic=1125
OVERVIEW:
JavaScript: Check this documentation: http://trac.wildfiregames.com/wiki/Exposed_ConfigDB_Functions
*/
#ifndef INCLUDED_CONFIGDB
#define INCLUDED_CONFIGDB
#include "Parser.h"
#include "CStr.h"
#include "Singleton.h"
#include "lib/file/vfs/vfs_path.h"
// Namespace priorities: User supersedes mod supersedes system.
// Command-line arguments override everything.
enum EConfigNamespace
{
CFG_DEFAULT,
CFG_SYSTEM,
CFG_MOD,
CFG_USER,
CFG_COMMAND,
CFG_LAST
};
typedef CParserValue CConfigValue;
typedef std::vector CConfigValueSet;
#define g_ConfigDB CConfigDB::GetSingleton()
class CConfigDB: public Singleton
{
static std::map m_Map[];
static VfsPath m_ConfigFile[];
public:
CConfigDB();
/**
- * Attempt to find a config variable with the given name; will search
- * CFG_COMMAND first, and then all namespaces from the specified namespace
- * down to system.
- *
- * Returns a pointer to the config value structure for the variable, or
- * NULL if such a variable could not be found.
+ * Attempt to retrieve the value of a config variable with the given name;
+ * will search CFG_COMMAND first, and then all namespaces from the specified
+ * namespace down.
*/
- CConfigValue *GetValue(EConfigNamespace ns, const CStr& name);
+ void GetValueBool(EConfigNamespace ns, const CStr& name, bool& value);
+ ///@copydoc CConfigDB::GetValueBool
+ void GetValueInt(EConfigNamespace ns, const CStr& name, int& value);
+ ///@copydoc CConfigDB::GetValueBool
+ void GetValueFloat(EConfigNamespace ns, const CStr& name, float& value);
+ ///@copydoc CConfigDB::GetValueBool
+ void GetValueDouble(EConfigNamespace ns, const CStr& name, double& value);
+ ///@copydoc CConfigDB::GetValueBool
+ void GetValueString(EConfigNamespace ns, const CStr& name, std::string& value);
/**
* Attempt to retrieve a vector of values corresponding to the given setting;
* will search CFG_COMMAND first, and then all namespaces from the specified
- * namespace down to system.
- *
- * Returns a pointer to the vector, or NULL if the setting could not be found.
+ * namespace down.
*/
- CConfigValueSet *GetValues(EConfigNamespace ns, const CStr& name);
+ void GetValues(EConfigNamespace ns, const CStr& name, CConfigValueSet& values);
/**
* Returns the namespace that the value returned by GetValues was defined in,
* or CFG_LAST if it wasn't defined at all.
*/
EConfigNamespace GetValueNamespace(EConfigNamespace ns, const CStr& name);
/**
* Retrieve a map of values corresponding to settings whose names begin
* with the given prefix;
- * will search all namespaces from system up to the specified namespace.
+ * will search all namespaces from default up to the specified namespace.
*/
std::map GetValuesWithPrefix(EConfigNamespace ns, const CStr& prefix);
/**
- * Create a new config value in the specified namespace. If such a
- * variable already exists in this namespace, the old value is returned.
- *
- * Returns a pointer to the value of the newly created config variable, or
- * that of the already existing config variable.
+ * Save a config value in the specified namespace. If the config variable
+ * existed the value is replaced.
*/
- CConfigValue *CreateValue(EConfigNamespace ns, const CStr& name);
+ void SetValueString(EConfigNamespace ns, const CStr& name, const CStr& value);
/**
* Set the path to the config file used to populate the specified namespace
* Note that this function does not actually load the config file. Use
* the Reload() method if you want to read the config file at the same time.
*
* 'path': The path to the config file.
*/
void SetConfigFile(EConfigNamespace ns, const VfsPath& path);
/**
* Reload the config file associated with the specified config namespace
* (the last config file path set with SetConfigFile)
*
* Returns:
* true: if the reload succeeded,
* false: if the reload failed
*/
bool Reload(EConfigNamespace);
/**
* Write the current state of the specified config namespace to the file
* specified by 'path'
*
* Returns:
* true: if the config namespace was successfully written to the file
* false: if an error occurred
*/
bool WriteFile(EConfigNamespace ns, const VfsPath& path);
/**
* Write the current state of the specified config namespace to the file
* it was originally loaded from.
*
* Returns:
* true: if the config namespace was successfully written to the file
* false: if an error occurred
*/
bool WriteFile(EConfigNamespace ns);
};
// stores the value of the given key into . this quasi-template
// convenience wrapper on top of CConfigValue::Get* simplifies user code and
// avoids "assignment within condition expression" warnings.
#define CFG_GET_VAL(name, type, destination)\
-STMT(\
- CConfigValue* val = g_ConfigDB.GetValue(CFG_USER, name);\
- if(val)\
- val->Get##type(destination);\
-)
-
+ g_ConfigDB.GetValue##type(CFG_USER, name, destination)
#endif
Index: ps/trunk/source/ps/GameSetup/Config.cpp
===================================================================
--- ps/trunk/source/ps/GameSetup/Config.cpp (revision 14436)
+++ ps/trunk/source/ps/GameSetup/Config.cpp (revision 14437)
@@ -1,232 +1,232 @@
/* Copyright (C) 2012 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "Config.h"
#include "ps/ConfigDB.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/GameSetup/CmdLineArgs.h"
#include "lib/timer.h"
#include "soundmanager/ISoundManager.h"
// (these variables are documented in the header.)
CStrW g_CursorName = L"test";
bool g_NoGLS3TC = false;
bool g_NoGLAutoMipmap = false;
bool g_NoGLVBO = false;
bool g_PauseOnFocusLoss = false;
bool g_Shadows = false;
bool g_ShadowPCF = false;
bool g_WaterNormal = false;
bool g_WaterRealDepth = false;
bool g_WaterFoam = false;
bool g_WaterCoastalWaves = false;
bool g_WaterRefraction = false;
bool g_WaterReflection = false;
bool g_WaterShadows = false;
bool g_Particles = false;
bool g_Silhouettes = false;
bool g_ShowSky = false;
float g_Gamma = 1.0f;
CStr g_RenderPath = "default";
int g_xres, g_yres;
bool g_VSync = false;
bool g_Quickstart = false;
bool g_DisableAudio = false;
bool g_JSDebuggerEnabled = false;
bool g_ScriptProfilingEnabled = false;
// flag to switch on drawing terrain overlays
bool g_ShowPathfindingOverlay = false;
// flag to switch on triangulation pathfinding
bool g_TriPathfind = false;
// If non-empty, specified map will be automatically loaded
CStr g_AutostartMap = "";
//----------------------------------------------------------------------------
// config
//----------------------------------------------------------------------------
// Fill in the globals from the config files.
static void LoadGlobals()
{
CFG_GET_VAL("vsync", Bool, g_VSync);
CFG_GET_VAL("nos3tc", Bool, g_NoGLS3TC);
CFG_GET_VAL("noautomipmap", Bool, g_NoGLAutoMipmap);
CFG_GET_VAL("novbo", Bool, g_NoGLVBO);
CFG_GET_VAL("pauseonfocusloss", Bool, g_PauseOnFocusLoss);
CFG_GET_VAL("shadows", Bool, g_Shadows);
CFG_GET_VAL("shadowpcf", Bool, g_ShadowPCF);
CFG_GET_VAL("waternormals",Bool, g_WaterNormal);
CFG_GET_VAL("waterrealdepth",Bool, g_WaterRealDepth);
CFG_GET_VAL("waterfoam",Bool, g_WaterFoam);
CFG_GET_VAL("watercoastalwaves",Bool, g_WaterCoastalWaves);
if (g_WaterCoastalWaves && !g_WaterNormal)
g_WaterCoastalWaves = false;
CFG_GET_VAL("waterrefraction",Bool, g_WaterRefraction);
CFG_GET_VAL("waterreflection",Bool, g_WaterReflection);
CFG_GET_VAL("watershadows",Bool, g_WaterShadows);
CFG_GET_VAL("renderpath", String, g_RenderPath);
CFG_GET_VAL("particles", Bool, g_Particles);
CFG_GET_VAL("silhouettes", Bool, g_Silhouettes);
CFG_GET_VAL("showsky", Bool, g_ShowSky);
if (g_SoundManager)
{
float gain = 0.5f;
float musicGain = 0.5f;
float ambientGain = 0.5f;
float actionGain = 0.5f;
float uiGain = 0.5f;
CFG_GET_VAL("sound.mastergain", Float, gain);
CFG_GET_VAL("sound.musicgain", Float, musicGain);
CFG_GET_VAL("sound.ambientgain", Float, ambientGain);
CFG_GET_VAL("sound.actiongain", Float, actionGain);
CFG_GET_VAL("sound.uigain", Float, uiGain);
g_SoundManager->SetMasterGain( gain );
g_SoundManager->SetMusicGain( musicGain );
g_SoundManager->SetAmbientGain( ambientGain );
g_SoundManager->SetActionGain( actionGain );
g_SoundManager->SetUIGain( uiGain );
}
CFG_GET_VAL("jsdebugger.enable", Bool, g_JSDebuggerEnabled);
CFG_GET_VAL("profiler2.script.enable", Bool, g_ScriptProfilingEnabled);
// Script Debugging and profiling does not make sense together because of the hooks
// that reduce performance a lot - and it wasn't tested if it even works together.
if (g_JSDebuggerEnabled && g_ScriptProfilingEnabled)
LOGERROR(L"Enabling both script profiling and script debugging is not supported!");
}
static void ProcessCommandLineArgs(const CmdLineArgs& args)
{
// TODO: all these options (and the ones processed elsewhere) should
// be documented somewhere for users.
// Handle "-conf=key:value" (potentially multiple times)
std::vector conf = args.GetMultiple("conf");
for (size_t i = 0; i < conf.size(); ++i)
{
CStr name_value = conf[i];
if (name_value.Find(':') != -1)
{
CStr name = name_value.BeforeFirst(":");
CStr value = name_value.AfterFirst(":");
- g_ConfigDB.CreateValue(CFG_COMMAND, name)->m_String = value;
+ g_ConfigDB.SetValueString(CFG_COMMAND, name, value);
}
}
if (args.Has("g"))
{
g_Gamma = args.Get("g").ToFloat();
if (g_Gamma == 0.0f)
g_Gamma = 1.0f;
}
// if (args.Has("listfiles"))
// trace_enable(true);
if (args.Has("profile"))
- g_ConfigDB.CreateValue(CFG_COMMAND, "profile")->m_String = args.Get("profile");
+ g_ConfigDB.SetValueString(CFG_COMMAND, "profile", args.Get("profile"));
if (args.Has("quickstart"))
{
g_Quickstart = true;
g_DisableAudio = true; // do this for backward-compatibility with user expectations
}
if (args.Has("nosound"))
g_DisableAudio = true;
if (args.Has("shadows"))
- g_ConfigDB.CreateValue(CFG_COMMAND, "shadows")->m_String = "true";
+ g_ConfigDB.SetValueString(CFG_COMMAND, "shadows", "true");
if (args.Has("xres"))
- g_ConfigDB.CreateValue(CFG_COMMAND, "xres")->m_String = args.Get("xres");
+ g_ConfigDB.SetValueString(CFG_COMMAND, "xres", args.Get("xres"));
if (args.Has("yres"))
- g_ConfigDB.CreateValue(CFG_COMMAND, "yres")->m_String = args.Get("yres");
+ g_ConfigDB.SetValueString(CFG_COMMAND, "yres", args.Get("yres"));
if (args.Has("vsync"))
- g_ConfigDB.CreateValue(CFG_COMMAND, "vsync")->m_String = "true";
+ g_ConfigDB.SetValueString(CFG_COMMAND, "vsync", "true");
if (args.Has("ooslog"))
- g_ConfigDB.CreateValue(CFG_COMMAND, "ooslog")->m_String = "true";
+ g_ConfigDB.SetValueString(CFG_COMMAND, "ooslog", "true");
if (args.Has("serializationtest"))
- g_ConfigDB.CreateValue(CFG_COMMAND, "serializationtest")->m_String = "true";
+ g_ConfigDB.SetValueString(CFG_COMMAND, "serializationtest", "true");
}
void CONFIG_Init(const CmdLineArgs& args)
{
TIMER(L"CONFIG_Init");
new CConfigDB;
// Load the global, default config file
g_ConfigDB.SetConfigFile(CFG_DEFAULT, L"config/default.cfg");
g_ConfigDB.Reload(CFG_DEFAULT); // 216ms
// Try loading the local system config file (which doesn't exist by
// default) - this is designed as a way of letting developers edit the
// system config without accidentally committing their changes back to SVN.
g_ConfigDB.SetConfigFile(CFG_SYSTEM, L"config/local.cfg");
g_ConfigDB.Reload(CFG_SYSTEM);
g_ConfigDB.SetConfigFile(CFG_USER, L"config/user.cfg");
g_ConfigDB.Reload(CFG_USER);
g_ConfigDB.SetConfigFile(CFG_MOD, L"config/mod.cfg");
// No point in reloading mod.cfg here - we haven't mounted mods yet
ProcessCommandLineArgs(args);
// Initialise console history file
int max_history_lines = 200;
CFG_GET_VAL("console.history.size", Int, max_history_lines);
g_Console->UseHistoryFile(L"config/console.txt", max_history_lines);
// Collect information from system.cfg, the profile file,
// and any command-line overrides to fill in the globals.
LoadGlobals(); // 64ms
}
Index: ps/trunk/source/ps/UserReport.cpp
===================================================================
--- ps/trunk/source/ps/UserReport.cpp (revision 14436)
+++ ps/trunk/source/ps/UserReport.cpp (revision 14437)
@@ -1,629 +1,629 @@
-/* Copyright (C) 2012 Wildfire Games.
+/* Copyright (C) 2013 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "UserReport.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include "lib/external_libraries/curl.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/external_libraries/zlib.h"
#include "lib/file/archive/stream.h"
#include "lib/sysdep/sysdep.h"
#include "ps/ConfigDB.h"
#include "ps/Filesystem.h"
#include "ps/Profiler2.h"
#include "ps/ThreadUtil.h"
#define DEBUG_UPLOADS 0
/*
* The basic idea is that the game submits reports to us, which we send over
* HTTP to a server for storage and analysis.
*
* We can't use libcurl's asynchronous 'multi' API, because DNS resolution can
* be synchronous and slow (which would make the game pause).
* So we use the 'easy' API in a background thread.
* The main thread submits reports, toggles whether uploading is enabled,
* and polls for the current status (typically to display in the GUI);
* the worker thread does all of the uploading.
*
* It'd be nice to extend this in the future to handle things like crash reports.
* The game should store the crashlogs (suitably anonymised) in a directory, and
* we should detect those files and upload them when we're restarted and online.
*/
/**
* Version number stored in config file when the user agrees to the reporting.
* Reporting will be disabled if the config value is missing or is less than
* this value. If we start reporting a lot more data, we should increase this
* value and get the user to re-confirm.
*/
static const int REPORTER_VERSION = 1;
/**
* Time interval (seconds) at which the worker thread will check its reconnection
* timers. (This should be relatively high so the thread doesn't waste much time
* continually waking up.)
*/
static const double TIMER_CHECK_INTERVAL = 10.0;
/**
* Seconds we should wait before reconnecting to the server after a failure.
*/
static const double RECONNECT_INVERVAL = 60.0;
CUserReporter g_UserReporter;
struct CUserReport
{
time_t m_Time;
std::string m_Type;
int m_Version;
std::string m_Data;
};
class CUserReporterWorker
{
public:
CUserReporterWorker(const std::string& userID, const std::string& url) :
m_URL(url), m_UserID(userID), m_Enabled(false), m_Shutdown(false), m_Status("disabled"),
m_PauseUntilTime(timer_Time()), m_LastUpdateTime(timer_Time())
{
// Set up libcurl:
m_Curl = curl_easy_init();
ENSURE(m_Curl);
#if DEBUG_UPLOADS
curl_easy_setopt(m_Curl, CURLOPT_VERBOSE, 1L);
#endif
// Capture error messages
curl_easy_setopt(m_Curl, CURLOPT_ERRORBUFFER, m_ErrorBuffer);
// Disable signal handlers (required for multithreaded applications)
curl_easy_setopt(m_Curl, CURLOPT_NOSIGNAL, 1L);
// To minimise security risks, don't support redirects
curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 0L);
// Set IO callbacks
curl_easy_setopt(m_Curl, CURLOPT_WRITEFUNCTION, ReceiveCallback);
curl_easy_setopt(m_Curl, CURLOPT_WRITEDATA, this);
curl_easy_setopt(m_Curl, CURLOPT_READFUNCTION, SendCallback);
curl_easy_setopt(m_Curl, CURLOPT_READDATA, this);
// Set URL to POST to
curl_easy_setopt(m_Curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(m_Curl, CURLOPT_POST, 1L);
// Set up HTTP headers
m_Headers = NULL;
// Set the UA string
std::string ua = "User-Agent: 0ad ";
ua += curl_version();
ua += " (http://play0ad.com/)";
m_Headers = curl_slist_append(m_Headers, ua.c_str());
// Override the default application/x-www-form-urlencoded type since we're not using that type
m_Headers = curl_slist_append(m_Headers, "Content-Type: application/octet-stream");
// Disable the Accept header because it's a waste of a dozen bytes
m_Headers = curl_slist_append(m_Headers, "Accept: ");
curl_easy_setopt(m_Curl, CURLOPT_HTTPHEADER, m_Headers);
// Set up the worker thread:
// Use SDL semaphores since OS X doesn't implement sem_init
m_WorkerSem = SDL_CreateSemaphore(0);
ENSURE(m_WorkerSem);
int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this);
ENSURE(ret == 0);
}
~CUserReporterWorker()
{
// Clean up resources
SDL_DestroySemaphore(m_WorkerSem);
curl_slist_free_all(m_Headers);
curl_easy_cleanup(m_Curl);
}
/**
* Called by main thread, when the online reporting is enabled/disabled.
*/
void SetEnabled(bool enabled)
{
CScopeLock lock(m_WorkerMutex);
if (enabled != m_Enabled)
{
m_Enabled = enabled;
// Wake up the worker thread
SDL_SemPost(m_WorkerSem);
}
}
/**
* Called by main thread to request shutdown.
* Returns true if we've shut down successfully.
* Returns false if shutdown is taking too long (we might be blocked on a
* sync network operation) - you mustn't destroy this object, just leak it
* and terminate.
*/
bool Shutdown()
{
{
CScopeLock lock(m_WorkerMutex);
m_Shutdown = true;
}
// Wake up the worker thread
SDL_SemPost(m_WorkerSem);
// Wait for it to shut down cleanly
// TODO: should have a timeout in case of network hangs
pthread_join(m_WorkerThread, NULL);
return true;
}
/**
* Called by main thread to determine the current status of the uploader.
*/
std::string GetStatus()
{
CScopeLock lock(m_WorkerMutex);
return m_Status;
}
/**
* Called by main thread to add a new report to the queue.
*/
void Submit(const shared_ptr& report)
{
{
CScopeLock lock(m_WorkerMutex);
m_ReportQueue.push_back(report);
}
// Wake up the worker thread
SDL_SemPost(m_WorkerSem);
}
/**
* Called by the main thread every frame, so we can check
* retransmission timers.
*/
void Update()
{
double now = timer_Time();
if (now > m_LastUpdateTime + TIMER_CHECK_INTERVAL)
{
// Wake up the worker thread
SDL_SemPost(m_WorkerSem);
m_LastUpdateTime = now;
}
}
private:
static void* RunThread(void* data)
{
debug_SetThreadName("CUserReportWorker");
g_Profiler2.RegisterCurrentThread("userreport");
static_cast(data)->Run();
return NULL;
}
void Run()
{
// Set libcurl's proxy configuration
// (This has to be done in the thread because it's potentially very slow)
SetStatus("proxy");
std::wstring proxy;
{
PROFILE2("get proxy config");
if (sys_get_proxy_config(wstring_from_utf8(m_URL), proxy) == INFO::OK)
curl_easy_setopt(m_Curl, CURLOPT_PROXY, utf8_from_wstring(proxy).c_str());
}
SetStatus("waiting");
/*
* We use a semaphore to let the thread be woken up when it has
* work to do. Various actions from the main thread can wake it:
* * SetEnabled()
* * Shutdown()
* * Submit()
* * Retransmission timeouts, once every several seconds
*
* If multiple actions have triggered wakeups, we might respond to
* all of those actions after the first wakeup, which is okay (we'll do
* nothing during the subsequent wakeups). We should never hang due to
* processing fewer actions than wakeups.
*
* Retransmission timeouts are triggered via the main thread - we can't simply
* use SDL_SemWaitTimeout because on Linux it's implemented as an inefficient
* busy-wait loop, and we can't use a manual busy-wait with a long delay time
* because we'd lose responsiveness. So the main thread pings the worker
* occasionally so it can check its timer.
*/
g_Profiler2.RecordRegionEnter("semaphore wait");
// Wait until the main thread wakes us up
while (SDL_SemWait(m_WorkerSem) == 0)
{
g_Profiler2.RecordRegionLeave("semaphore wait");
// Handle shutdown requests as soon as possible
if (GetShutdown())
return;
// If we're not enabled, ignore this wakeup
if (!GetEnabled())
continue;
// If we're still pausing due to a failed connection,
// go back to sleep again
if (timer_Time() < m_PauseUntilTime)
continue;
// We're enabled, so process as many reports as possible
while (ProcessReport())
{
// Handle shutdowns while we were sending the report
if (GetShutdown())
return;
}
}
g_Profiler2.RecordRegionLeave("semaphore wait");
}
bool GetEnabled()
{
CScopeLock lock(m_WorkerMutex);
return m_Enabled;
}
bool GetShutdown()
{
CScopeLock lock(m_WorkerMutex);
return m_Shutdown;
}
void SetStatus(const std::string& status)
{
CScopeLock lock(m_WorkerMutex);
m_Status = status;
#if DEBUG_UPLOADS
debug_printf(L">>> CUserReporterWorker status: %hs\n", status.c_str());
#endif
}
bool ProcessReport()
{
PROFILE2("process report");
shared_ptr report;
{
CScopeLock lock(m_WorkerMutex);
if (m_ReportQueue.empty())
return false;
report = m_ReportQueue.front();
m_ReportQueue.pop_front();
}
ConstructRequestData(*report);
m_RequestDataOffset = 0;
m_ResponseData.clear();
curl_easy_setopt(m_Curl, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)m_RequestData.size());
SetStatus("connecting");
#if DEBUG_UPLOADS
TIMER(L"CUserReporterWorker request");
#endif
CURLcode err = curl_easy_perform(m_Curl);
#if DEBUG_UPLOADS
printf(">>>\n%s\n<<<\n", m_ResponseData.c_str());
#endif
if (err == CURLE_OK)
{
long code = -1;
curl_easy_getinfo(m_Curl, CURLINFO_RESPONSE_CODE, &code);
SetStatus("completed:" + CStr::FromInt(code));
// Check for success code
if (code == 200)
return true;
// If the server returns the 410 Gone status, interpret that as meaning
// it no longer supports uploads (at least from this version of the game),
// so shut down and stop talking to it (to avoid wasting bandwidth)
if (code == 410)
{
CScopeLock lock(m_WorkerMutex);
m_Shutdown = true;
return false;
}
}
else
{
SetStatus("failed:" + CStr::FromInt(err) + ":" + m_ErrorBuffer);
}
// We got an unhandled return code or a connection failure;
// push this report back onto the queue and try again after
// a long interval
{
CScopeLock lock(m_WorkerMutex);
m_ReportQueue.push_front(report);
}
m_PauseUntilTime = timer_Time() + RECONNECT_INVERVAL;
return false;
}
void ConstructRequestData(const CUserReport& report)
{
// Construct the POST request data in the application/x-www-form-urlencoded format
std::string r;
r += "user_id=";
AppendEscaped(r, m_UserID);
r += "&time=" + CStr::FromInt64(report.m_Time);
r += "&type=";
AppendEscaped(r, report.m_Type);
r += "&version=" + CStr::FromInt(report.m_Version);
r += "&data=";
AppendEscaped(r, report.m_Data);
// Compress the content with zlib to save bandwidth.
// (Note that we send a request with unlabelled compressed data instead
// of using Content-Encoding, because Content-Encoding is a mess and causes
// problems with servers and breaks Content-Length and this is much easier.)
std::string compressed;
compressed.resize(compressBound(r.size()));
uLongf destLen = compressed.size();
int ok = compress((Bytef*)compressed.c_str(), &destLen, (const Bytef*)r.c_str(), r.size());
ENSURE(ok == Z_OK);
compressed.resize(destLen);
m_RequestData.swap(compressed);
}
void AppendEscaped(std::string& buffer, const std::string& str)
{
char* escaped = curl_easy_escape(m_Curl, str.c_str(), str.size());
buffer += escaped;
curl_free(escaped);
}
static size_t ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp)
{
CUserReporterWorker* self = static_cast(userp);
if (self->GetShutdown())
return 0; // signals an error
self->m_ResponseData += std::string((char*)buffer, (char*)buffer+size*nmemb);
return size*nmemb;
}
static size_t SendCallback(char* bufptr, size_t size, size_t nmemb, void* userp)
{
CUserReporterWorker* self = static_cast(userp);
if (self->GetShutdown())
return CURL_READFUNC_ABORT; // signals an error
// We can return as much data as available, up to the buffer size
size_t amount = std::min(self->m_RequestData.size() - self->m_RequestDataOffset, size*nmemb);
// ...But restrict to sending a small amount at once, so that we remain
// responsive to shutdown requests even if the network is pretty slow
amount = std::min((size_t)1024, amount);
if(amount != 0) // (avoids invalid operator[] call where index=size)
{
memcpy(bufptr, &self->m_RequestData[self->m_RequestDataOffset], amount);
self->m_RequestDataOffset += amount;
}
self->SetStatus("sending:" + CStr::FromDouble((double)self->m_RequestDataOffset / self->m_RequestData.size()));
return amount;
}
private:
// Thread-related members:
pthread_t m_WorkerThread;
CMutex m_WorkerMutex;
SDL_sem* m_WorkerSem;
// Shared by main thread and worker thread:
// These variables are all protected by m_WorkerMutex
std::deque > m_ReportQueue;
bool m_Enabled;
bool m_Shutdown;
std::string m_Status;
// Initialised in constructor by main thread; otherwise used only by worker thread:
std::string m_URL;
std::string m_UserID;
CURL* m_Curl;
curl_slist* m_Headers;
double m_PauseUntilTime;
// Only used by worker thread:
std::string m_ResponseData;
std::string m_RequestData;
size_t m_RequestDataOffset;
char m_ErrorBuffer[CURL_ERROR_SIZE];
// Only used by main thread:
double m_LastUpdateTime;
};
CUserReporter::CUserReporter() :
m_Worker(NULL)
{
}
CUserReporter::~CUserReporter()
{
ENSURE(!m_Worker); // Deinitialize should have been called before shutdown
}
std::string CUserReporter::LoadUserID()
{
std::string userID;
// Read the user ID from user.cfg (if there is one)
CFG_GET_VAL("userreport.id", String, userID);
// If we don't have a validly-formatted user ID, generate a new one
if (userID.length() != 16)
{
u8 bytes[8] = {0};
sys_generate_random_bytes(bytes, ARRAY_SIZE(bytes));
// ignore failures - there's not much we can do about it
userID = "";
for (size_t i = 0; i < ARRAY_SIZE(bytes); ++i)
{
char hex[3];
sprintf_s(hex, ARRAY_SIZE(hex), "%02x", (unsigned int)bytes[i]);
userID += hex;
}
- g_ConfigDB.CreateValue(CFG_USER, "userreport.id")->m_String = userID;
+ g_ConfigDB.SetValueString(CFG_USER, "userreport.id", userID);
g_ConfigDB.WriteFile(CFG_USER);
}
return userID;
}
bool CUserReporter::IsReportingEnabled()
{
int version = -1;
CFG_GET_VAL("userreport.enabledversion", Int, version);
return (version >= REPORTER_VERSION);
}
void CUserReporter::SetReportingEnabled(bool enabled)
{
CStr val = CStr::FromInt(enabled ? REPORTER_VERSION : 0);
- g_ConfigDB.CreateValue(CFG_USER, "userreport.enabledversion")->m_String = val;
+ g_ConfigDB.SetValueString(CFG_USER, "userreport.enabledversion", val);
g_ConfigDB.WriteFile(CFG_USER);
if (m_Worker)
m_Worker->SetEnabled(enabled);
}
std::string CUserReporter::GetStatus()
{
if (!m_Worker)
return "disabled";
return m_Worker->GetStatus();
}
void CUserReporter::Initialize()
{
ENSURE(!m_Worker); // must only be called once
std::string userID = LoadUserID();
std::string url;
CFG_GET_VAL("userreport.url", String, url);
// Initialise everything except Win32 sockets (because our networking
// system already inits those)
curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32);
m_Worker = new CUserReporterWorker(userID, url);
m_Worker->SetEnabled(IsReportingEnabled());
}
void CUserReporter::Deinitialize()
{
if (!m_Worker)
return;
if (m_Worker->Shutdown())
{
// Worker was shut down cleanly
SAFE_DELETE(m_Worker);
curl_global_cleanup();
}
else
{
// Worker failed to shut down in a reasonable time
// Leak the resources (since that's better than hanging or crashing)
m_Worker = NULL;
}
}
void CUserReporter::Update()
{
if (m_Worker)
m_Worker->Update();
}
void CUserReporter::SubmitReport(const char* type, int version, const std::string& data)
{
// If not initialised, discard the report
if (!m_Worker)
return;
shared_ptr report(new CUserReport);
report->m_Time = time(NULL);
report->m_Type = type;
report->m_Version = version;
report->m_Data = data;
m_Worker->Submit(report);
}
Index: ps/trunk/source/ps/scripting/JSInterface_ConfigDB.cpp
===================================================================
--- ps/trunk/source/ps/scripting/JSInterface_ConfigDB.cpp (revision 14436)
+++ ps/trunk/source/ps/scripting/JSInterface_ConfigDB.cpp (revision 14437)
@@ -1,112 +1,104 @@
/* Copyright (C) 2013 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "JSInterface_ConfigDB.h"
#include "ps/ConfigDB.h"
#include "ps/CLogger.h"
#include "scriptinterface/ScriptInterface.h"
bool JSI_ConfigDB::GetConfigNamespace(std::wstring cfgNsString, EConfigNamespace& cfgNs)
{
if (cfgNsString == L"default")
cfgNs = CFG_DEFAULT;
else if (cfgNsString == L"system")
cfgNs = CFG_SYSTEM;
else if (cfgNsString == L"user")
cfgNs = CFG_USER;
else if (cfgNsString == L"mod")
cfgNs = CFG_MOD;
else
{
LOGERROR(L"Invalid namespace name passed to the ConfigDB!");
cfgNs = CFG_DEFAULT;
return false;
}
return true;
}
std::string JSI_ConfigDB::GetValue(void* UNUSED(cbdata), std::wstring cfgNsString, std::string name)
{
EConfigNamespace cfgNs;
if (!GetConfigNamespace(cfgNsString, cfgNs))
return std::string();
- CConfigValue *val = g_ConfigDB.GetValue(cfgNs, name);
- if (val)
- {
- return val->m_String;
- }
- else
- {
- LOGMESSAGE(L"Config setting %hs does not exist!", name.c_str());
- return std::string();
- }
+ std::string value;
+ g_ConfigDB.GetValueString(cfgNs, name, value);
+ return value;
}
bool JSI_ConfigDB::CreateValue(void* UNUSED(cbdata), std::wstring cfgNsString, std::string name, std::string value)
{
EConfigNamespace cfgNs;
if (!GetConfigNamespace(cfgNsString, cfgNs))
return false;
- CConfigValue *val = g_ConfigDB.CreateValue(cfgNs, name);
- val->m_String = value;
+ g_ConfigDB.SetValueString(cfgNs, name, value);
return true;
}
bool JSI_ConfigDB::WriteFile(void* UNUSED(cbdata), std::wstring cfgNsString, Path path)
{
EConfigNamespace cfgNs;
if (!GetConfigNamespace(cfgNsString, cfgNs))
return false;
bool ret = g_ConfigDB.WriteFile(cfgNs, path);
return ret;
}
bool JSI_ConfigDB::Reload(void* UNUSED(cbdata), std::wstring cfgNsString)
{
EConfigNamespace cfgNs;
if (!GetConfigNamespace(cfgNsString, cfgNs))
return false;
bool ret = g_ConfigDB.Reload(cfgNs);
return ret;
}
bool JSI_ConfigDB::SetFile(void* UNUSED(cbdata), std::wstring cfgNsString, Path path)
{
EConfigNamespace cfgNs;
if (!GetConfigNamespace(cfgNsString, cfgNs))
return false;
g_ConfigDB.SetConfigFile(cfgNs, path);
return true;
}
void JSI_ConfigDB::RegisterScriptFunctions(ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("ConfigDB_GetValue");
scriptInterface.RegisterFunction("ConfigDB_CreateValue");
scriptInterface.RegisterFunction("ConfigDB_WriteFile");
scriptInterface.RegisterFunction("ConfigDB_SetFile");
scriptInterface.RegisterFunction("ConfigDB_Reload");
}