1
0
forked from 0ad/0ad

# Add opt-in automatic feedback system.

Add GL implementation limits to hwdetect.
Make the in-game manual support multiple text files.
Remove unnecessary instruction on how to close the manual window.
Make guitextobject.caption return the original value without stripping
formatting tags.
Remove unused non-VFS support from config system.
Support writing config files back to their original path.
Remove unnecessary user profile directory; use normal config directory.

This was SVN commit r8925.
This commit is contained in:
Ykkrosh 2011-02-16 20:40:15 +00:00
parent 1c3c962ea1
commit 0da7e822ff
24 changed files with 1165 additions and 105 deletions

View File

@ -53,9 +53,8 @@ renderpath = default
; -1.0 to -1.5 recommended for good results.
lodbias = 0
; Profile selection
profile = default
; Opt-in online user reporting system
userreport.url = "http://feedback.wildfiregames.com/report/upload/v1/"
; Font mappings:

View File

@ -2,8 +2,6 @@
[font="serif-14"]
Thank you for installing 0 A.D.! This page will give a brief overview of the features available in this incomplete, under-development, alpha version of the game.
[font="serif-12"]Use the "X" button in the top-right corner of this window to close it. Or click the "Close" button below.
[font="serif-bold-16"]Graphics settings
[font="serif-14"]You can switch between fullscreen and windowed mode by pressing Alt+Enter. In windowed mode, you can resize the window. If the game runs too slowly, you can change some settings in the configuration file: look for binaries/data/config/default.cfg in the location where the game is installed, which gives instructions for editing, and try disabling the "fancywater" and "shadows" options.

View File

@ -1,4 +1,4 @@
function init()
function init(data)
{
getGUIObjectByName("mainText").caption = readFile("gui/manual/intro.txt");
getGUIObjectByName("mainText").caption = readFile("gui/manual/" + data.page + ".txt");
}

View File

@ -0,0 +1,12 @@
As a free, open source game, we don't have the resources to test on a wide range of systems, but we want to provide the best quality experience to as many players as possible. When you enable automatic feedback, we'll receive data to help us understand the hardware we should focus on supporting, and to identify performance problems we should fix.
The following data will be sent to our server:
• A random user ID (stored in %APPDATA%\0ad\config\user.cfg on Windows, ~/.config/0ad/user.cfg on Unix), to let us detect repeated reports from the same user.
• Game version number and basic build settings (optimisation mode, CPU architecture).
• Hardware details: OS version, graphics driver version, OpenGL capabilities, screen size, CPU type, RAM.
• Performance details: a snapshot of timing data a few seconds after you start a match or change graphics settings.
The data will only be a few kilobytes each time you run the game, so bandwidth usage is minimal.
We will store the submitted data on our server, and may publish statistics or non-user-identifiable details to help other game developers with similar questions. We will store the IP address that submitted the data, to help detect abuse of the service, but will not publish it.

View File

@ -1,8 +1,74 @@
var userReportEnabledText; // contains the original version with "$status" placeholder
function init()
{
global.curr_music = newRandomSound("music", "menu");
if (global.curr_music)
global.curr_music.loop();
userReportEnabledText = getGUIObjectByName("userReportEnabledText").caption;
}
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";
}
function onTick()
{
if (Engine.IsUserReportEnabled())
{
getGUIObjectByName("userReportDisabled").hidden = true;
getGUIObjectByName("userReportEnabled").hidden = false;
getGUIObjectByName("userReportEnabledText").caption =
userReportEnabledText.replace(/\$status/,
formatUserReportStatus(Engine.GetUserReportStatus()));
}
else
{
getGUIObjectByName("userReportDisabled").hidden = false;
getGUIObjectByName("userReportEnabled").hidden = true;
}
}
// Helper function that enables the dark background mask, then reveals a given subwindow object.

View File

@ -17,6 +17,11 @@
sprite="pgBackground"
aspectratio="1.333333"
>
<action on="Tick">
onTick();
</action>
<!--
==========================================
- MAIN MENU - PRE-RELEASE WARNING
@ -33,7 +38,7 @@
Warning: This is an early development version of the game and many features have not been added yet, but we hope this gives you a glimpse of the game’s vision.
Watch for updates or get involved in the development: http://wildfiregames.com/0ad/
</object>
</object>
<!--
==========================================
@ -57,11 +62,11 @@ Watch for updates or get involved in the development: http://wildfiregames.com/0
==========================================
-->
<object size="100%-400 0 100% 30">
<object size="100%-400 0 100% 30">
<object type="button" style="wheatButton" size="0 0 100 30">
Manual
<action on="Press"><![CDATA[
Engine.PushGuiPage("page_manual.xml");
Engine.PushGuiPage("page_manual.xml", { "page": "intro" });
]]></action>
</object>
@ -268,6 +273,87 @@ Watch for updates or get involved in the development: http://wildfiregames.com/0
"to learn more about [icon=iconProduct] A.D., participate in the community, meet the developers, and learn how to become a part of development yourself.\n\n", "About [icon=iconProduct] A.D.", 2, [], []);
]]></action>
</object>
<!--
==========================================
- MAIN MENU - USER REPORT
==========================================
-->
<object
name="userReportDisabled"
size="4 300 400 460"
type="image"
style="userReportPanel"
>
<object
type="text"
style="userReportText"
>[font="serif-bold-16"]Help improve 0 A.D.![/font]
You can automatically send us anonymous feedback that will help us to improve performance and compatibility and to fix bugs.
</object>
<object type="button" style="wheatButton" size="4 100%-44 180 100%-4">
[font="serif-bold-16"]Enable feedback
<action on="Press">Engine.SetUserReportEnabled(true);</action>
</object>
<object type="button" style="wheatButton" size="100%-150 100%-35 100%-4 100%-4">
Technical details
<action on="Press">Engine.PushGuiPage("page_manual.xml", { "page": "userreport" });</action>
</object>
</object>
<object
name="userReportEnabled"
size="4 300 400 550"
type="image"
style="userReportPanel"
>
<object
name="userReportEnabledText"
type="text"
style="userReportText"
>[font="serif-bold-16"]Thank you for helping improve 0 A.D.![/font]
Anonymous feedback is currently enabled.
Status: $status.</object>
<!--
Put the rest of the text in a separate box, so that
very long $status messages don't mess up the layout
quite so badly:
-->
<object
type="text"
style="userReportText"
size="0 90 100% 100%"
>If you want to send a message to the developers, you can enter one here:
</object>
<object
name="userReportMessageInput"
size="2 130 100%-66 190"
type="input"
style="wheatInput"
multiline="true"
scrollbar="true"
scrollbar_style="wheatScrollBar"
/>
<object size="100%-65 160 100%-1 193" type="button" style="wheatButton">
Send
<action on="Press">submitUserReportMessage();</action>
</object>
<object type="button" style="wheatButton" size="4 100%-35 150 100%-4">
Disable feedback
<action on="Press">Engine.SetUserReportEnabled(false);</action>
</object>
<object type="button" style="wheatButton" size="100%-150 100%-35 100%-4 100%-4">
Technical details
<action on="Press">Engine.PushGuiPage("page_manual.xml", { "page": "userreport" });</action>
</object>
</object>
</object>
<!--

View File

@ -174,4 +174,14 @@
/>
</sprite>
<sprite name="pgUserReportBox">
<image
backcolor="0 0 0 220"
size="0 0 100% 100%"
border="true"
bordercolor="200 200 200"
/>
</sprite>
</sprites>

View File

@ -7,4 +7,15 @@
tooltip=""
/>
<style name="userReportPanel"
sprite="pgUserReportBox"
/>
<style name="userReportText"
ghost="true"
font="serif-14"
textcolor="white"
/>
</styles>

View File

@ -68,10 +68,10 @@ function RunDetection(settings)
var gfx_mem = settings.gfx_mem;
// Values from glGetString
var gl_vendor = settings.gl_vendor;
var gl_renderer = settings.gl_renderer;
var gl_version = settings.gl_version;
var gl_extensions = settings.gl_extensions.split(" "); // split on spaces
var GL_VENDOR = settings.GL_VENDOR;
var GL_RENDERER = settings.GL_RENDERER;
var GL_VERSION = settings.GL_VERSION;
var GL_EXTENSIONS = settings.GL_EXTENSIONS.split(" "); // split on spaces
var video_xres = settings.video_xres;
var video_yres = settings.video_yres;
@ -93,7 +93,7 @@ function RunDetection(settings)
// http://www.wildfiregames.com/forum/index.php?showtopic=13668
// Fixed in 260.19.21:
// "Fixed a race condition in OpenGL that could cause crashes with multithreaded applications."
if (os_unix && gl_version.match(/NVIDIA 260\.19\.(0[0-9]|1[0-9]|20)$/))
if (os_unix && GL_VERSION.match(/NVIDIA 260\.19\.(0[0-9]|1[0-9]|20)$/))
{
dialog_warnings.push("You are using 260.19.* series NVIDIA drivers, which may crash the game. Please upgrade to 260.19.21 or later.");
}

View File

@ -467,6 +467,7 @@ function setup_all_libs ()
"zlib",
"boost",
"enet",
"libcurl",
}
setup_static_lib_package("engine", source_dirs, extern_libs, {})
@ -613,6 +614,7 @@ used_extern_libs = {
"cxxtest",
"comsuppw",
"enet",
"libcurl",
"valgrind",

View File

@ -318,6 +318,8 @@ bool CGUIString::TextChunk::Tag::SetTagType(const CStr& tagtype)
void CGUIString::SetValue(const CStrW& str)
{
m_OriginalString = str;
// clear
m_TextChunks.clear();
m_Words.clear();

View File

@ -294,6 +294,11 @@ public:
*/
const CStrW& GetRawString() const { return m_RawString; }
/**
* Get String, with tags
*/
const CStrW& GetOriginalString() const { return m_OriginalString; }
/**
* Generate Text Call from specified range. The range
* must span only within ONE TextChunk though. Otherwise
@ -333,6 +338,11 @@ private:
* The full raw string. Stripped of tags.
*/
CStrW m_RawString;
/**
* The original string value passed to SetValue.
*/
CStrW m_OriginalString;
};
#endif

View File

@ -203,7 +203,7 @@ JSBool JSI_IGUIObject::getProperty(JSContext* cx, JSObject* obj, jsid id, jsval*
{
CGUIString value;
GUI<CGUIString>::GetSetting(e, propName, value);
*vp = ScriptInterface::ToJSVal(cx, value.GetRawString());
*vp = ScriptInterface::ToJSVal(cx, value.GetOriginalString());
break;
}
@ -273,7 +273,7 @@ JSBool JSI_IGUIObject::getProperty(JSContext* cx, JSObject* obj, jsid id, jsval*
for (size_t i = 0; i < value.m_Items.size(); ++i)
{
jsval val = ScriptInterface::ToJSVal(cx, value.m_Items[i].GetRawString());
jsval val = ScriptInterface::ToJSVal(cx, value.m_Items[i].GetOriginalString());
JS_SetElement(cx, obj, (jsint)i, &val);
}

View File

@ -24,6 +24,7 @@
#include "graphics/MapReader.h"
#include "gui/GUIManager.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include "lib/sysdep/sysdep.h"
#include "maths/FixedVector3D.h"
#include "network/NetClient.h"
@ -36,6 +37,7 @@
#include "ps/Hotkey.h"
#include "ps/Overlay.h"
#include "ps/Pyrogenesis.h"
#include "ps/UserReport.h"
#include "ps/GameSetup/Atlas.h"
#include "ps/GameSetup/Config.h"
@ -342,6 +344,29 @@ void DisplayErrorDialog(void* UNUSED(cbdata), std::wstring msg)
}
bool IsUserReportEnabled(void* UNUSED(cbdata))
{
return g_UserReporter.IsReportingEnabled();
}
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);
@ -449,6 +474,12 @@ void GuiScriptingInit(ScriptInterface& scriptInterface)
scriptInterface.RegisterFunction<bool, std::string, &HotkeyIsPressed_>("HotkeyIsPressed");
scriptInterface.RegisterFunction<void, std::wstring, &DisplayErrorDialog>("DisplayErrorDialog");
// User report functions
scriptInterface.RegisterFunction<bool, &IsUserReportEnabled>("IsUserReportEnabled");
scriptInterface.RegisterFunction<void, bool, &SetUserReportEnabled>("SetUserReportEnabled");
scriptInterface.RegisterFunction<std::string, &GetUserReportStatus>("GetUserReportStatus");
scriptInterface.RegisterFunction<void, std::string, int, std::wstring, &SubmitUserReport>("SubmitUserReport");
// Development/debugging functions
scriptInterface.RegisterFunction<void, float, &SetSimRate>("SetSimRate");
scriptInterface.RegisterFunction<void, int, &SetTurnLength>("SetTurnLength");

View File

@ -27,6 +27,8 @@
#ifndef INCLUDED_CURL
#define INCLUDED_CURL
#if OS_WIN
// curl.h wants to include winsock2.h which causes conflicts.
// provide some required definitions from winsock.h, then pretend
// we already included winsock.h
@ -43,6 +45,8 @@ struct fd_set;
#define _WINSOCKAPI_ // winsock.h include guard
#endif // OS_WIN
#include <curl/curl.h>
#endif // #ifndef INCLUDED_CURL

View File

@ -52,6 +52,7 @@ that of Atlas depending on commandline parameters.
#include "ps/Profile.h"
#include "ps/Pyrogenesis.h"
#include "ps/Replay.h"
#include "ps/UserReport.h"
#include "ps/Util.h"
#include "ps/VideoMode.h"
#include "ps/GameSetup/GameSetup.h"
@ -360,6 +361,8 @@ static void Frame()
g_NetClient->Flush();
PROFILE_END("network flush");
g_UserReporter.Update();
PROFILE_START( "update console" );
g_Console->Update(TimeSinceLastFrame);
PROFILE_END( "update console" );

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -30,8 +30,7 @@
typedef std::map <CStr, CConfigValueSet> TConfigMap;
TConfigMap CConfigDB::m_Map[CFG_LAST];
CStrW CConfigDB::m_ConfigFile[CFG_LAST];
bool CConfigDB::m_UseVFS[CFG_LAST];
VfsPath CConfigDB::m_ConfigFile[CFG_LAST];
#define GET_NS_PRIVATE(cx, obj) (EConfigNamespace)((intptr_t)JS_GetPrivate(cx, obj) >> 1)
@ -110,18 +109,14 @@ namespace ConfigNamespace_JS
if (cfgNs < 0 || cfgNs >= CFG_LAST)
return JS_FALSE;
if (argc != 2)
return JS_FALSE;
bool useVFS;
if (!ScriptInterface::FromJSVal(cx, JS_ARGV(cx, vp)[0], useVFS))
if (argc != 1)
return JS_FALSE;
std::wstring path;
if (!ScriptInterface::FromJSVal(cx, JS_ARGV(cx, vp)[1], path))
if (!ScriptInterface::FromJSVal(cx, JS_ARGV(cx, vp)[0], path))
return JS_FALSE;
bool res = g_ConfigDB.WriteFile(cfgNs, useVFS, path);
bool res = g_ConfigDB.WriteFile(cfgNs, path);
JS_SET_RVAL(cx, vp, BOOLEAN_TO_JSVAL(res));
return JS_TRUE;
@ -147,18 +142,14 @@ namespace ConfigNamespace_JS
if (cfgNs < 0 || cfgNs >= CFG_LAST)
return JS_FALSE;
if (argc != 2)
return JS_FALSE;
bool useVFS;
if (!ScriptInterface::FromJSVal(cx, JS_ARGV(cx, vp)[0], useVFS))
if (argc != 1)
return JS_FALSE;
std::wstring path;
if (!ScriptInterface::FromJSVal(cx, JS_ARGV(cx, vp)[1], path))
if (!ScriptInterface::FromJSVal(cx, JS_ARGV(cx, vp)[0], path))
return JS_FALSE;
g_ConfigDB.SetConfigFile(cfgNs, useVFS, path);
g_ConfigDB.SetConfigFile(cfgNs, path);
JS_SET_RVAL(cx, vp, JSVAL_VOID);
return JS_TRUE;
@ -297,7 +288,7 @@ CConfigValue *CConfigDB::CreateValue(EConfigNamespace ns, const CStr& name)
return &(it->second[0]);
}
void CConfigDB::SetConfigFile(EConfigNamespace ns, bool useVFS, const CStrW& path)
void CConfigDB::SetConfigFile(EConfigNamespace ns, const VfsPath& path)
{
if (ns < 0 || ns >= CFG_LAST)
{
@ -306,11 +297,16 @@ void CConfigDB::SetConfigFile(EConfigNamespace ns, bool useVFS, const CStrW& pat
}
m_ConfigFile[ns]=path;
m_UseVFS[ns]=useVFS;
}
bool CConfigDB::Reload(EConfigNamespace ns)
{
if (ns < 0 || ns >= CFG_LAST)
{
debug_warn(L"CConfigDB: Invalid ns value");
return false;
}
// Set up CParser
CParser parser;
CParserLine parserLine;
@ -323,16 +319,16 @@ bool CConfigDB::Reload(EConfigNamespace ns)
// Handle missing files quietly
if (g_VFS->GetFileInfo(m_ConfigFile[ns], NULL) < 0)
{
LOGMESSAGE(L"Cannot find config file \"%ls\" - ignoring", m_ConfigFile[ns].c_str());
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].c_str());
LOGMESSAGE(L"Loading config file \"%ls\"", m_ConfigFile[ns].string().c_str());
LibError ret = g_VFS->LoadFile(m_ConfigFile[ns], buffer, buflen);
if (ret != INFO::OK)
{
LOGERROR(L"CConfigDB::Reload(): vfs_load for \"%ls\" failed: return was %ld", m_ConfigFile[ns].c_str(), ret);
LOGERROR(L"CConfigDB::Reload(): vfs_load for \"%ls\" failed: return was %ld", m_ConfigFile[ns].string().c_str(), ret);
return false;
}
}
@ -388,10 +384,19 @@ bool CConfigDB::Reload(EConfigNamespace ns)
return true;
}
bool CConfigDB::WriteFile(EConfigNamespace ns, bool useVFS, const CStrW& path)
bool CConfigDB::WriteFile(EConfigNamespace ns)
{
debug_assert(useVFS);
if (ns < 0 || ns >= CFG_LAST)
{
debug_warn(L"CConfigDB: Invalid ns value");
return false;
}
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");
@ -410,7 +415,7 @@ bool CConfigDB::WriteFile(EConfigNamespace ns, bool useVFS, const CStrW& path)
LibError ret = g_VFS->CreateFile(path, buf, len);
if(ret < 0)
{
LOGERROR(L"CConfigDB::WriteFile(): CreateFile \"%ls\" failed (error: %d)", path.c_str(), (int)ret);
LOGERROR(L"CConfigDB::WriteFile(): CreateFile \"%ls\" failed (error: %d)", path.string().c_str(), (int)ret);
return false;
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -54,6 +54,8 @@
#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.
@ -75,8 +77,7 @@ typedef std::vector<CParserValue> CConfigValueSet;
class CConfigDB: public Singleton<CConfigDB>
{
static std::map <CStr, CConfigValueSet> m_Map[];
static CStrW m_ConfigFile[];
static bool m_UseVFS[];
static VfsPath m_ConfigFile[];
public:
// NOTE: Construct the Singleton Object *after* JavaScript init, so that
@ -123,11 +124,8 @@ public:
* the Reload() method if you want to read the config file at the same time.
*
* 'path': The path to the config file.
* VFS: relative to VFS root
* non-VFS: relative to current working directory (binaries/data/)
* 'useVFS': true if the path is a VFS path, false if it is a real path
*/
void SetConfigFile(EConfigNamespace ns, bool useVFS, const CStrW& path);
void SetConfigFile(EConfigNamespace ns, const VfsPath& path);
/**
* Reload the config file associated with the specified config namespace
@ -147,7 +145,17 @@ public:
* true: if the config namespace was successfully written to the file
* false: if an error occurred
*/
bool WriteFile(EConfigNamespace ns, bool useVFS, const CStrW& path);
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);
};

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -29,7 +29,6 @@
// (these variables are documented in the header.)
CStrW g_CursorName = L"test";
CStr g_ActiveProfile = "default";
bool g_NoGLS3TC = false;
bool g_NoGLAutoMipmap = false;
@ -63,32 +62,12 @@ CStr g_AutostartMap = "";
//----------------------------------------------------------------------------
// config and profile
// config
//----------------------------------------------------------------------------
static void LoadProfile( const CStr& profile )
{
VfsPath path = VfsPath(L"config/profiles") / wstring_from_utf8(profile);
VfsPath configFilename = path / L"settings/user.cfg";
g_ConfigDB.SetConfigFile(CFG_USER, true, configFilename.string().c_str());
g_ConfigDB.Reload(CFG_USER);
int max_history_lines = 200;
CFG_GET_USER_VAL("console.history.size", Int, max_history_lines);
g_Console->UseHistoryFile(path / L"settings/history", max_history_lines);
}
// Fill in the globals from the config files.
static void LoadGlobals()
{
CFG_GET_SYS_VAL("profile", String, g_ActiveProfile);
// Now load the profile before trying to retrieve the values of the rest of these.
LoadProfile( g_ActiveProfile );
CFG_GET_USER_VAL("vsync", Bool, g_VSync);
CFG_GET_USER_VAL("nos3tc", Bool, g_NoGLS3TC);
@ -180,19 +159,27 @@ void CONFIG_Init(const CmdLineArgs& args)
new CConfigDB;
// Load the global, default config file
g_ConfigDB.SetConfigFile(CFG_DEFAULT, false, L"config/default.cfg");
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, false, L"config/local.cfg");
g_ConfigDB.SetConfigFile(CFG_SYSTEM, L"config/local.cfg");
g_ConfigDB.Reload(CFG_SYSTEM);
g_ConfigDB.SetConfigFile(CFG_MOD, true, L"config/mod.cfg");
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_USER_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

View File

@ -49,6 +49,7 @@
#include "ps/Overlay.h"
#include "ps/Profile.h"
#include "ps/ProfileViewer.h"
#include "ps/UserReport.h"
#include "ps/Util.h"
#include "ps/VideoMode.h"
#include "ps/World.h"
@ -672,6 +673,10 @@ void Shutdown(int UNUSED(flags))
g_VideoMode.Shutdown();
TIMER_BEGIN(L"shutdown UserReporter");
g_UserReporter.Deinitialize();
TIMER_END(L"shutdown UserReporter");
TIMER_BEGIN(L"shutdown ScriptingHost");
delete &g_ScriptingHost;
TIMER_END(L"shutdown ScriptingHost");
@ -848,6 +853,9 @@ void Init(const CmdLineArgs& args, int UNUSED(flags))
// g_ConfigDB, command line args, globals
CONFIG_Init(args);
if (!g_Quickstart)
g_UserReporter.Initialize(); // after config
}
void InitGraphics(const CmdLineArgs& args, int flags)

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -21,12 +21,15 @@
#include "scriptinterface/ScriptInterface.h"
#include "lib/ogl.h"
#include "lib/svn_revision.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include "lib/sysdep/cpu.h"
#include "lib/sysdep/gfx.h"
#include "lib/sysdep/os_cpu.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/UserReport.h"
#include "ps/VideoMode.h"
#include "ps/GameSetup/Config.h"
@ -35,8 +38,12 @@ void SetDisableAudio(void* UNUSED(cbdata), bool disabled)
g_DisableAudio = disabled;
}
static void ReportGLLimits(ScriptInterface& scriptInterface, CScriptValRooted settings);
void RunHardwareDetection()
{
TIMER(L"RunHardwareDetection");
ScriptInterface& scriptInterface = g_ScriptingHost.GetScriptInterface();
scriptInterface.RegisterFunction<void, bool, &SetDisableAudio>("SetDisableAudio");
@ -57,47 +64,195 @@ void RunHardwareDetection()
scriptInterface.LoadScript(scriptName, code);
// Collect all the settings we'll pass to the script:
// (We'll use this same data for the opt-in online reporting system, so it
// includes some fields that aren't directly useful for the hwdetect script)
CScriptValRooted settings;
scriptInterface.Eval("({})", settings);
scriptInterface.SetProperty(settings.get(), "os_unix", OS_UNIX, false);
scriptInterface.SetProperty(settings.get(), "os_linux", OS_LINUX, false);
scriptInterface.SetProperty(settings.get(), "os_macosx", OS_MACOSX, false);
scriptInterface.SetProperty(settings.get(), "os_win", OS_WIN, false);
scriptInterface.SetProperty(settings.get(), "os_unix", OS_UNIX);
scriptInterface.SetProperty(settings.get(), "os_linux", OS_LINUX);
scriptInterface.SetProperty(settings.get(), "os_macosx", OS_MACOSX);
scriptInterface.SetProperty(settings.get(), "os_win", OS_WIN);
scriptInterface.SetProperty(settings.get(), "gfx_card", std::wstring(gfx_card), false);
scriptInterface.SetProperty(settings.get(), "gfx_drv_ver", std::wstring(gfx_drv_ver), false);
scriptInterface.SetProperty(settings.get(), "gfx_mem", gfx_mem, false);
scriptInterface.SetProperty(settings.get(), "arch_ia32", ARCH_IA32);
scriptInterface.SetProperty(settings.get(), "arch_amd64", ARCH_AMD64);
scriptInterface.SetProperty(settings.get(), "gl_vendor", std::string((const char*)glGetString(GL_VENDOR)), false);
scriptInterface.SetProperty(settings.get(), "gl_renderer", std::string((const char*)glGetString(GL_RENDERER)), false);
scriptInterface.SetProperty(settings.get(), "gl_version", std::string((const char*)glGetString(GL_VERSION)), false);
#ifdef NDEBUG
scriptInterface.SetProperty(settings.get(), "build_debug", 0);
#else
scriptInterface.SetProperty(settings.get(), "build_debug", 1);
#endif
scriptInterface.SetProperty(settings.get(), "build_datetime", std::string(__DATE__ " " __TIME__));
scriptInterface.SetProperty(settings.get(), "build_revision", std::wstring(svn_revision));
const char* exts = ogl_ExtensionString();
if (!exts) exts = "";
scriptInterface.SetProperty(settings.get(), "gl_extensions", std::string(exts), false);
scriptInterface.SetProperty(settings.get(), "gl_max_tex_size", (int)ogl_max_tex_size, false);
scriptInterface.SetProperty(settings.get(), "gl_max_tex_units", (int)ogl_max_tex_units, false);
scriptInterface.SetProperty(settings.get(), "gfx_card", std::wstring(gfx_card));
scriptInterface.SetProperty(settings.get(), "gfx_drv_ver", std::wstring(gfx_drv_ver));
scriptInterface.SetProperty(settings.get(), "gfx_mem", gfx_mem);
scriptInterface.SetProperty(settings.get(), "video_xres", g_VideoMode.GetXRes(), false);
scriptInterface.SetProperty(settings.get(), "video_yres", g_VideoMode.GetYRes(), false);
scriptInterface.SetProperty(settings.get(), "video_bpp", g_VideoMode.GetBPP(), false);
ReportGLLimits(scriptInterface, settings);
scriptInterface.SetProperty(settings.get(), "video_xres", g_VideoMode.GetXRes());
scriptInterface.SetProperty(settings.get(), "video_yres", g_VideoMode.GetYRes());
scriptInterface.SetProperty(settings.get(), "video_bpp", g_VideoMode.GetBPP());
struct utsname un;
uname(&un);
scriptInterface.SetProperty(settings.get(), "uname_sysname", std::string(un.sysname), false);
scriptInterface.SetProperty(settings.get(), "uname_release", std::string(un.release), false);
scriptInterface.SetProperty(settings.get(), "uname_version", std::string(un.version), false);
scriptInterface.SetProperty(settings.get(), "uname_machine", std::string(un.machine), false);
scriptInterface.SetProperty(settings.get(), "uname_sysname", std::string(un.sysname));
scriptInterface.SetProperty(settings.get(), "uname_release", std::string(un.release));
scriptInterface.SetProperty(settings.get(), "uname_version", std::string(un.version));
scriptInterface.SetProperty(settings.get(), "uname_machine", std::string(un.machine));
scriptInterface.SetProperty(settings.get(), "cpu_identifier", std::string(cpu_IdentifierString()), false);
scriptInterface.SetProperty(settings.get(), "cpu_frequency", os_cpu_ClockFrequency(), false);
scriptInterface.SetProperty(settings.get(), "cpu_identifier", std::string(cpu_IdentifierString()));
scriptInterface.SetProperty(settings.get(), "cpu_frequency", os_cpu_ClockFrequency());
scriptInterface.SetProperty(settings.get(), "ram_total", (int)os_cpu_MemorySize(), false);
scriptInterface.SetProperty(settings.get(), "ram_free", (int)os_cpu_MemoryAvailable(), false);
scriptInterface.SetProperty(settings.get(), "ram_total", (int)os_cpu_MemorySize());
scriptInterface.SetProperty(settings.get(), "ram_free", (int)os_cpu_MemoryAvailable());
// Send the same data to the reporting system
g_UserReporter.SubmitReport("hwdetect", 1, scriptInterface.StringifyJSON(settings.get(), false));
// Run the detection script:
scriptInterface.CallFunctionVoid(scriptInterface.GetGlobalObject(), "RunHardwareDetection", settings);
}
static void ReportGLLimits(ScriptInterface& scriptInterface, CScriptValRooted settings)
{
#define INTEGER(id) do { \
GLint i = -1; \
glGetIntegerv(GL_##id, &i); \
ogl_WarnIfError(); \
scriptInterface.SetProperty(settings.get(), "GL_" #id, i); \
} while (false)
#define INTEGER2(id) do { \
GLint i[2] = { -1, -1 }; \
glGetIntegerv(GL_##id, i); \
ogl_WarnIfError(); \
scriptInterface.SetProperty(settings.get(), "GL_" #id "[0]", i[0]); \
scriptInterface.SetProperty(settings.get(), "GL_" #id "[1]", i[1]); \
} while (false)
#define FLOAT(id) do { \
GLfloat f = std::numeric_limits<GLfloat>::quiet_NaN(); \
glGetFloatv(GL_##id, &f); \
ogl_WarnIfError(); \
scriptInterface.SetProperty(settings.get(), "GL_" #id, f); \
} while (false)
#define FLOAT2(id) do { \
GLfloat f[2] = { std::numeric_limits<GLfloat>::quiet_NaN(), std::numeric_limits<GLfloat>::quiet_NaN() }; \
glGetFloatv(GL_##id, f); \
ogl_WarnIfError(); \
scriptInterface.SetProperty(settings.get(), "GL_" #id "[0]", f[0]); \
scriptInterface.SetProperty(settings.get(), "GL_" #id "[1]", f[1]); \
} while (false)
#define STRING(id) do { \
const char* c = (const char*)glGetString(GL_##id); \
ogl_WarnIfError(); \
if (!c) c = ""; \
scriptInterface.SetProperty(settings.get(), "GL_" #id, std::string(c)); \
} while (false)
#define BOOL(id) INTEGER(id)
// Core OpenGL 1.3:
// (We don't bother checking extension strings for anything older than 1.3;
// it'll just produce harmless warnings)
STRING(VERSION);
STRING(VENDOR);
STRING(RENDERER);
STRING(EXTENSIONS);
INTEGER(MAX_LIGHTS);
INTEGER(MAX_CLIP_PLANES);
if (ogl_HaveExtension("GL_ARB_imaging")) // only in imaging subset
INTEGER(MAX_COLOR_MATRIX_STACK_DEPTH);
INTEGER(MAX_MODELVIEW_STACK_DEPTH);
INTEGER(MAX_PROJECTION_STACK_DEPTH);
INTEGER(MAX_TEXTURE_STACK_DEPTH);
INTEGER(SUBPIXEL_BITS);
INTEGER(MAX_3D_TEXTURE_SIZE);
INTEGER(MAX_TEXTURE_SIZE);
INTEGER(MAX_CUBE_MAP_TEXTURE_SIZE);
INTEGER(MAX_PIXEL_MAP_TABLE);
INTEGER(MAX_NAME_STACK_DEPTH);
INTEGER(MAX_LIST_NESTING);
INTEGER(MAX_EVAL_ORDER);
INTEGER2(MAX_VIEWPORT_DIMS);
INTEGER(MAX_ATTRIB_STACK_DEPTH);
INTEGER(MAX_CLIENT_ATTRIB_STACK_DEPTH);
INTEGER(AUX_BUFFERS);
BOOL(RGBA_MODE);
BOOL(INDEX_MODE);
BOOL(DOUBLEBUFFER);
BOOL(STEREO);
FLOAT2(ALIASED_POINT_SIZE_RANGE);
FLOAT2(SMOOTH_POINT_SIZE_RANGE);
FLOAT(SMOOTH_POINT_SIZE_GRANULARITY);
FLOAT2(ALIASED_LINE_WIDTH_RANGE);
FLOAT2(SMOOTH_LINE_WIDTH_RANGE);
FLOAT(SMOOTH_LINE_WIDTH_GRANULARITY);
// Skip MAX_CONVOLUTION_WIDTH, MAX_CONVOLUTION_HEIGHT since they'd need GetConvolutionParameteriv
INTEGER(MAX_ELEMENTS_INDICES);
INTEGER(MAX_ELEMENTS_VERTICES);
INTEGER(MAX_TEXTURE_UNITS);
INTEGER(SAMPLE_BUFFERS);
INTEGER(SAMPLES);
// TODO: compressed texture formats
INTEGER(RED_BITS);
INTEGER(GREEN_BITS);
INTEGER(BLUE_BITS);
INTEGER(ALPHA_BITS);
INTEGER(INDEX_BITS);
INTEGER(DEPTH_BITS);
INTEGER(STENCIL_BITS);
INTEGER(ACCUM_RED_BITS);
INTEGER(ACCUM_GREEN_BITS);
INTEGER(ACCUM_BLUE_BITS);
INTEGER(ACCUM_ALPHA_BITS);
// Core OpenGL 2.0 (treated as extensions):
if (ogl_HaveExtension("GL_EXT_texture_lod_bias"))
FLOAT(MAX_TEXTURE_LOD_BIAS_EXT);
// Skip GL_ARB_occlusion_query's QUERY_COUNTER_BITS_ARB since it'd need GetQueryiv
if (ogl_HaveExtension("GL_ARB_shading_language_100"))
STRING(SHADING_LANGUAGE_VERSION_ARB);
if (ogl_HaveExtension("GL_ARB_vertex_shader"))
{
INTEGER(MAX_VERTEX_ATTRIBS_ARB);
INTEGER(MAX_VERTEX_UNIFORM_COMPONENTS_ARB);
INTEGER(MAX_VARYING_FLOATS_ARB);
INTEGER(MAX_COMBINED_TEXTURE_IMAGE_UNITS_ARB);
INTEGER(MAX_VERTEX_TEXTURE_IMAGE_UNITS_ARB);
}
if (ogl_HaveExtension("GL_ARB_fragment_shader"))
INTEGER(MAX_FRAGMENT_UNIFORM_COMPONENTS_ARB);
if (ogl_HaveExtension("GL_ARB_vertex_shader") || ogl_HaveExtension("GL_ARB_fragment_shader"))
{
INTEGER(MAX_TEXTURE_IMAGE_UNITS_ARB);
INTEGER(MAX_TEXTURE_COORDS_ARB);
}
if (ogl_HaveExtension("GL_ARB_draw_buffers"))
INTEGER(MAX_DRAW_BUFFERS_ARB);
// Other interesting extensions:
if (ogl_HaveExtension("GL_EXT_framebuffer_object"))
{
INTEGER(MAX_COLOR_ATTACHMENTS_EXT);
INTEGER(MAX_RENDERBUFFER_SIZE_EXT);
}
if (ogl_HaveExtension("GL_EXT_texture_filter_anisotropic"))
FLOAT(MAX_TEXTURE_MAX_ANISOTROPY_EXT);
}

601
source/ps/UserReport.cpp Normal file
View File

@ -0,0 +1,601 @@
/* Copyright (C) 2011 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 <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "UserReport.h"
#include "lib/timer.h"
#include "lib/external_libraries/curl.h"
#include "lib/external_libraries/sdl.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/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_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();
debug_assert(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://wildfiregames.com/0ad/)";
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);
debug_assert(m_WorkerSem);
int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this);
debug_assert(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<CUserReport>& 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");
static_cast<CUserReporterWorker*>(data)->Run();
return NULL;
}
void Run()
{
/*
* 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.
*/
// Wait until the main thread wakes us up
while (SDL_SemWait(m_WorkerSem) == 0)
{
// 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;
}
}
}
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()
{
shared_ptr<CUserReport> 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(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(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(report.m_Time);
r += "&type=";
AppendEscaped(r, report.m_Type);
r += "&version=" + CStr(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());
debug_assert(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<CUserReporterWorker*>(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<CUserReporterWorker*>(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);
memcpy(bufptr, &self->m_RequestData[self->m_RequestDataOffset], amount);
self->m_RequestDataOffset += amount;
self->SetStatus("sending:" + CStr((float)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<shared_ptr<CUserReport> > 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_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()
{
debug_assert(!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_USER_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", bytes[i]);
userID += hex;
}
g_ConfigDB.CreateValue(CFG_USER, "userreport.id")->m_String = userID;
g_ConfigDB.WriteFile(CFG_USER);
}
return userID;
}
bool CUserReporter::IsReportingEnabled()
{
int version = -1;
CFG_GET_USER_VAL("userreport.enabledversion", Int, version);
return (version >= REPORTER_VERSION);
}
void CUserReporter::SetReportingEnabled(bool enabled)
{
CStr val(enabled ? REPORTER_VERSION : 0);
g_ConfigDB.CreateValue(CFG_USER, "userreport.enabledversion")->m_String = 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()
{
debug_assert(!m_Worker); // must only be called once
std::string userID = LoadUserID();
std::string url;
CFG_GET_SYS_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<CUserReport> report(new CUserReport);
report->m_Time = time(NULL);
report->m_Type = type;
report->m_Version = version;
report->m_Data = data;
m_Worker->Submit(report);
}

62
source/ps/UserReport.h Normal file
View File

@ -0,0 +1,62 @@
/* Copyright (C) 2011 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 <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_USERREPORT
#define INCLUDED_USERREPORT
class CUserReporterWorker;
class CUserReporter
{
public:
CUserReporter();
~CUserReporter();
void Initialize();
void Deinitialize();
/**
* Must be called frequently (preferably every frame), to update some
* internal reconnection timers.
*/
void Update();
// Functions for the GUI to control the reporting:
bool IsReportingEnabled();
void SetReportingEnabled(bool enabled);
std::string GetStatus();
/**
* Submit a report to be transmitted to the online server.
* Nothing will be transmitted until reporting is enabled by the user, so
* you don't need to check for that first.
* @param type short string identifying the type of data ("hwdetect", "message", etc)
* @param version positive integer that should be incremented if the data is changed in
* a non-compatible way and the server will have to distinguish old and new formats
* @param data the actual data (typically UTF-8-encoded text, or JSON, but could be binary)
*/
void SubmitReport(const char* type, int version, const std::string& data);
private:
std::string LoadUserID();
CUserReporterWorker* m_Worker;
};
extern CUserReporter g_UserReporter;
#endif // INCLUDED_USERREPORT

View File

@ -176,14 +176,14 @@ public:
* Optionally makes it {ReadOnly, DontDelete, DontEnum}.
*/
template<typename T>
bool SetProperty(jsval obj, const char* name, const T& value, bool constant, bool enumerate = true);
bool SetProperty(jsval obj, const char* name, const T& value, bool constant = false, bool enumerate = true);
/**
* Set the integer-named property on the given object.
* Optionally makes it {ReadOnly, DontDelete, DontEnum}.
*/
template<typename T>
bool SetPropertyInt(jsval obj, int name, const T& value, bool constant, bool enumerate = true);
bool SetPropertyInt(jsval obj, int name, const T& value, bool constant = false, bool enumerate = true);
template<typename T>
bool GetProperty(jsval obj, const char* name, T& out);