Index: ps/trunk/binaries/data/config/default.cfg
===================================================================
--- ps/trunk/binaries/data/config/default.cfg (revision 21758)
+++ ps/trunk/binaries/data/config/default.cfg (revision 21759)
@@ -1,494 +1,502 @@
; Global Configuration Settings
;
; **************************************************************
; * DO NOT EDIT THIS FILE if you want personal customisations: *
; * create a text file called "local.cfg" instead, and copy *
; * the lines from this file that you want to change. *
; * *
; * If a setting is part of a section (for instance [hotkey]) *
; * you need to append the section name at the beginning of *
; * your custom line (for instance you need to write *
; * "hotkey.pause = Space" if you want to change the pausing *
; * hotkey to the spacebar). *
; * *
; * On Linux, create: *
; * $XDG_CONFIG_HOME/0ad/config/local.cfg *
; * (Note: $XDG_CONFIG_HOME defaults to ~/.config) *
; * *
; * On OS X, create: *
; * ~/Library/Application\ Support/0ad/config/local.cfg *
; * *
; * On Windows, create: *
; * %appdata%\0ad\config\local.cfg *
; * *
; **************************************************************
; Enable/disable windowed mode by default. (Use Alt+Enter to toggle in the game.)
windowed = false
; Show detailed tooltips (Unit stats)
showdetailedtooltips = false
; Pause the game on window focus loss (Only applicable to single player mode)
pauseonfocusloss = true
; Persist settings after leaving the game setup screen
persistmatchsettings = true
; Default player name to use in multiplayer
; playername = "anonymous"
; Default server name or IP to use in multiplayer
multiplayerserver = "127.0.0.1"
; Force a particular resolution. (If these are 0, the default is
; to keep the current desktop resolution in fullscreen mode or to
; use 1024x768 in windowed mode.)
xres = 0
yres = 0
; Force a non-standard bit depth (if 0 then use the current desktop bit depth)
bpp = 0
; Preferred display (for multidisplay setups, only works with SDL 2.0)
display = 0
; Emulate right-click with Ctrl+Click on Mac mice
macmouse = false
; System settings:
; if false, actors won't be rendered but anything entity will be.
renderactors = true
watereffects=true ; When disabled, force usage of the fixed pipeline water. This is faster, but really, really ugly.
waterfancyeffects = false
waterrealdepth = true
waterrefraction = true
waterreflection = true
shadowsonwater = false
shadows = true
shadowquality = 0 ; Shadow map resolution. (-2 - Very Low, -1 - Low, 0 - Medium, 1 - High, 2 - Very High)
; High values can crash the game when using a graphics card with low memory!
shadowpcf = true
vsync = false
particles = true
fog = true
silhouettes = true
showsky = true
nos3tc = false
noautomipmap = true
novbo = false
noframebufferobject = false
; Disable hardware cursors
nohwcursor = false
; Linux only: Set the driconf force_s3tc_enable option at startup,
; for compressed texture support
force_s3tc_enable = true
; Specify the render path. This can be one of:
; default Automatically select one of the below, depending on system capabilities
; fixed Only use OpenGL fixed function pipeline
; shader Use vertex/fragment shaders for transform and lighting where possible
; Using 'fixed' instead of 'default' may work around some graphics-related problems,
; but will reduce performance and features when a modern graphics card is available.
renderpath = default
;;;;; EXPERIMENTAL ;;;;;
; Prefer GLSL shaders over ARB shaders. Allows fancier graphical effects.
preferglsl = false
; Experimental probably-non-working GPU skinning support; requires preferglsl; use at own risk
gpuskinning = false
; Use smooth LOS interpolation
smoothlos = false
; Use screen-space postprocessing filters (HDR, bloom, DOF, etc). Incompatible with fixed renderpath.
postproc = false
; Quality level of shader effects (set to 10 to display all effects)
materialmgr.quality = 2.0
; Maximum distance to display parallax effect. Set to 0 to disable parallax.
materialmgr.PARALLAX_DIST.max = 150
; Maximum distance to display high quality parallax effect.
materialmgr.PARALLAX_HQ_DIST.max = 75
; Maximum distance to display very high quality parallax effect. Set to 30 to enable.
materialmgr.PARALLAX_VHQ_DIST.max = 0
;;;;;;;;;;;;;;;;;;;;;;;;
; Replace alpha-blending with alpha-testing, for performance experiments
forcealphatest = false
; Color of the sky (in "r g b" format)
skycolor = "0 0 0"
[adaptivefps]
session = 60 ; Throttle FPS in running games (prevents 100% CPU workload).
menu = 30 ; Throttle FPS in menus only.
[hotkey]
; Each one of the specified keys will trigger the action on the left
; for multiple-key combinations, separate keys with '+'.
; See keys.txt for the list of key names.
; > SYSTEM SETTINGS
exit = "Ctrl+Break", "Super+Q" ; Exit to desktop
cancel = Escape ; Close or cancel the current dialog box/popup
leave = Escape ; End current game or Exit
confirm = Return ; Confirm the current command
pause = Pause ; Pause/unpause game
screenshot = F2 ; Take PNG screenshot
bigscreenshot = "Shift+F2" ; Take large BMP screenshot
togglefullscreen = "Alt+Return" ; Toggle fullscreen/windowed mode
screenshot.watermark = "Alt+K" ; Toggle product/company watermark for official screenshots
wireframe = "Alt+Shift+W" ; Toggle wireframe mode
silhouettes = "Alt+Shift+S" ; Toggle unit silhouettes
showsky = "Alt+Z" ; Toggle sky
; > DIALOG HOTKEYS
summary = "Ctrl+Tab" ; Toggle in-game summary
lobby = "Alt+L" ; Show the multiplayer lobby in a dialog window.
structree = "Alt+Shift+T" ; Show structure tree
civinfo = "Alt+Shift+H" ; Show civilization info
; > CLIPBOARD CONTROLS
copy = "Ctrl+C" ; Copy to clipboard
paste = "Ctrl+V" ; Paste from clipboard
cut = "Ctrl+X" ; Cut selected text and copy to the clipboard
; > CONSOLE SETTINGS
console.toggle = BackQuote, F9 ; Open/close console
; > OVERLAY KEYS
fps.toggle = "Alt+F" ; Toggle frame counter
realtime.toggle = "Alt+T" ; Toggle current display of computer time
session.devcommands.toggle = "Alt+D" ; Toggle developer commands panel
timeelapsedcounter.toggle = "F12" ; Toggle time elapsed counter
session.showstatusbars = Tab ; Toggle display of status bars
session.highlightguarding = PgDn ; Toggle highlight of guarding units
session.highlightguarded = PgUp ; Toggle highlight of guarded units
session.toggleattackrange = "Alt+C" ; Toggle display of attack range overlays of selected defensive structures
session.toggleaurasrange = "Alt+V" ; Toggle display of aura range overlays of selected units and structures
session.togglehealrange = "Alt+B" ; Toggle display of heal range overlays of selected units
session.diplomacycolors = "Alt+X" ; Toggle diplomacy colors
; > HOTKEYS ONLY
chat = Return ; Toggle chat window
teamchat = "T" ; Toggle chat window in team chat mode
privatechat = "L" ; Toggle chat window and select the previous private chat partner
; > QUICKSAVE
quicksave = "Shift+F5"
quickload = "Shift+F8"
[hotkey.camera]
reset = "R" ; Reset camera rotation to default.
follow = "F" ; Follow the first unit in the selection
rallypointfocus = unused ; Focus the camera on the rally point of the selected building
zoom.in = Plus, Equals, NumPlus ; Zoom camera in (continuous control)
zoom.out = Minus, NumMinus ; Zoom camera out (continuous control)
zoom.wheel.in = WheelUp ; Zoom camera in (stepped control)
zoom.wheel.out = WheelDown ; Zoom camera out (stepped control)
rotate.up = "Ctrl+UpArrow", "Ctrl+W" ; Rotate camera to look upwards
rotate.down = "Ctrl+DownArrow", "Ctrl+S" ; Rotate camera to look downwards
rotate.cw = "Ctrl+LeftArrow", "Ctrl+A", Q ; Rotate camera clockwise around terrain
rotate.ccw = "Ctrl+RightArrow", "Ctrl+D", E ; Rotate camera anticlockwise around terrain
rotate.wheel.cw = "Shift+WheelUp", MouseX1 ; Rotate camera clockwise around terrain (stepped control)
rotate.wheel.ccw = "Shift+WheelDown", MouseX2 ; Rotate camera anticlockwise around terrain (stepped control)
pan = MouseMiddle ; Enable scrolling by moving mouse
left = A, LeftArrow ; Scroll or rotate left
right = D, RightArrow ; Scroll or rotate right
up = W, UpArrow ; Scroll or rotate up/forwards
down = S, DownArrow ; Scroll or rotate down/backwards
scroll.speed.increase = "Ctrl+Shift+S" ; Increase scroll speed
scroll.speed.decrease = "Ctrl+Alt+S" ; Decrease scroll speed
rotate.speed.increase = "Ctrl+Shift+R" ; Increase rotation speed
rotate.speed.decrease = "Ctrl+Alt+R" ; Decrease rotation speed
zoom.speed.increase = "Ctrl+Shift+Z" ; Increase zoom speed
zoom.speed.decrease = "Ctrl+Alt+Z" ; Decrease zoom speed
[hotkey.camera.jump]
1 = F5 ; Jump to position N
2 = F6
3 = F7
4 = F8
;5 =
;6 =
;7 =
;8 =
;9 =
;10 =
[hotkey.camera.jump.set]
1 = "Ctrl+F5" ; Set jump position N
2 = "Ctrl+F6"
3 = "Ctrl+F7"
4 = "Ctrl+F8"
;5 =
;6 =
;7 =
;8 =
;9 =
;10 =
[hotkey.profile]
toggle = "F11" ; Enable/disable real-time profiler
save = "Shift+F11" ; Save current profiler data to logs/profile.txt
[hotkey.profile2]
toggle = "Ctrl+F11" ; Enable/disable HTTP/GPU modes for new profiler
[hotkey.selection]
add = Shift ; Add units to selection
militaryonly = Alt ; Add only military units to the selection
nonmilitaryonly = "Alt+Y" ; Add only non-military units to the selection
idleonly = "I" ; Select only idle units
woundedonly = "O" ; Select only wounded units
remove = Ctrl ; Remove units from selection
cancel = Esc ; Un-select all units and cancel building placement
idleworker = Period ; Select next idle worker
idlewarrior = ForwardSlash ; Select next idle warrior
idleunit = BackSlash ; Select next idle unit
offscreen = Alt ; Include offscreen units in selection
[hotkey.selection.group.add]
0 = "Shift+0"
1 = "Shift+1"
2 = "Shift+2"
3 = "Shift+3"
4 = "Shift+4"
5 = "Shift+5"
6 = "Shift+6"
7 = "Shift+7"
8 = "Shift+8"
9 = "Shift+9"
[hotkey.selection.group.save]
0 = "Ctrl+0"
1 = "Ctrl+1"
2 = "Ctrl+2"
3 = "Ctrl+3"
4 = "Ctrl+4"
5 = "Ctrl+5"
6 = "Ctrl+6"
7 = "Ctrl+7"
8 = "Ctrl+8"
9 = "Ctrl+9"
[hotkey.selection.group.select]
0 = 0
1 = 1
2 = 2
3 = 3
4 = 4
5 = 5
6 = 6
7 = 7
8 = 8
9 = 9
[hotkey.session]
kill = Delete ; Destroy selected units
stop = "H" ; Stop the current action
backtowork = "Y" ; The unit will go back to work
unload = "U" ; Unload garrisoned units when a building/mechanical unit is selected
move = unused ; Modifier to move to a point instead of another action (e.g. gather)
attack = Ctrl ; Modifier to attack instead of another action (e.g. capture)
attackmove = Ctrl ; Modifier to attackmove when clicking on a point
attackmoveUnit = "Ctrl+Q" ; Modifier to attackmove targeting only units when clicking on a point (should contain the attackmove keys)
garrison = Ctrl ; Modifier to garrison when clicking on building
autorallypoint = Ctrl ; Modifier to set the rally point on the building itself
guard = "G" ; Modifier to escort/guard when clicking on unit/building
patrol = "P" ; Modifier to patrol a unit
repair = "J" ; Modifier to repair when clicking on building/mechanical unit
queue = Shift ; Modifier to queue unit orders instead of replacing
orderone = Alt ; Modifier to order only one entity in selection.
batchtrain = Shift ; Modifier to train units in batches
massbarter = Shift ; Modifier to barter bunch of resources
masstribute = Shift ; Modifier to tribute bunch of resources
noconfirmation = Shift ; Do not ask confirmation when deleting a building/unit
fulltradeswap = Shift ; Modifier to put the desired trade resource to 100%
unloadtype = Shift ; Modifier to unload all units of type
deselectgroup = Ctrl ; Modifier to deselect units when clicking group icon, instead of selecting
rotate.cw = RightBracket ; Rotate building placement preview clockwise
rotate.ccw = LeftBracket ; Rotate building placement preview anticlockwise
[hotkey.session.gui]
toggle = "Alt+G" ; Toggle visibility of session GUI
menu.toggle = "F10" ; Toggle in-game menu
barter.toggle = "Ctrl+B" ; Toggle in-game barter/trade page
tutorial.toggle = "Ctrl+P" ; Toggle in-game tutorial panel
[hotkey.session.savedgames]
delete = Delete ; Delete the selected saved game asking confirmation
noconfirmation = Shift ; Do not ask confirmation when deleting a game
[hotkey.session.queueunit] ; > UNIT TRAINING
1 = "Z" ; add first unit type to queue
2 = "X" ; add second unit type to queue
3 = "C" ; add third unit type to queue
4 = "V" ; add fourth unit type to queue
5 = "B" ; add fivth unit type to queue
6 = "N" ; add sixth unit type to queue
7 = "M" ; add seventh unit type to queue
8 = Comma ; add eighth unit type to queue
[hotkey.session.timewarp]
fastforward = Space ; If timewarp mode enabled, speed up the game
rewind = Backspace ; If timewarp mode enabled, go back to earlier point in the game
[hotkey.tab]
next = "Tab", "Alt+S" ; Show the next tab
prev = "Shift+Tab", "Alt+W" ; Show the previous tab
[hotkey.text] ; > GUI TEXTBOX HOTKEYS
delete.left = "Ctrl+Backspace" ; Delete word to the left of cursor
delete.right = "Ctrl+Del" ; Delete word to the right of cursor
move.left = "Ctrl+LeftArrow" ; Move cursor to start of word to the left of cursor
move.right = "Ctrl+RightArrow" ; Move cursor to start of word to the right of cursor
[gui]
cursorblinkrate = 0.5 ; Cursor blink rate in seconds (0.0 to disable blinking)
scale = 1.0 ; GUI scaling factor, for improved compatibility with 4K displays
[gui.gamesetup]
enabletips = true ; Enable/Disable tips during gamesetup (for newcomers)
assignplayers = everyone ; Whether to assign joining clients to free playerslots. Possible values: everyone, buddies, disabled.
aidifficulty = 3 ; Difficulty level, from 0 (easiest) to 5 (hardest)
aibehavior = "random" ; Default behavior of the AI (random, balanced, aggressive or defensive)
settingsslide = true ; Enable/Disable settings panel slide
[gui.session]
camerajump.threshold = 40 ; How close do we have to be to the actual location in order to jump back to the previous one?
timeelapsedcounter = false ; Show the game duration in the top right corner
ceasefirecounter = false ; Show the remaining ceasefire time in the top right corner
batchtrainingsize = 5 ; Number of units to be trained per batch by default (when pressing the hotkey)
scrollbatchratio = 1 ; Number of times you have to scroll to increase/decrease the batchsize by 1
woundedunithotkeythreshold = 33 ; The wounded unit hotkey considers the selected units as wounded if their health percentage falls below this number
attackrange = true ; Display attack range overlays of selected defensive structures
aurasrange = true ; Display aura range overlays of selected units and structures
healrange = true ; Display heal range overlays of selected units
rankabovestatusbar = true ; Show rank icons above status bars
respoptooltipsort = 0 ; Sorting players in the resources and population tooltip by value (0 - no sort, -1 - ascending, 1 - descending)
[gui.session.minimap]
blinkduration = 1.7 ; The blink duration while pinging
pingduration = 50.0 ; The duration for which an entity will be pinged after an attack notification
[gui.session.notifications]
attack = true ; Show a chat notification if you are attacked by another player
tribute = true ; Show a chat notification if an ally tributes resources to another team member if teams are locked, and all tributes in observer mode
barter = true ; Show a chat notification to observers when a player bartered resources
phase = completed ; Show a chat notification if you or an ally have started, aborted or completed a new phase, and phases of all players in observer mode. Possible values: none, completed, all.
[gui.splashscreen]
enable = true ; Enable/disable the splashscreen
version = 0 ; Splashscreen version (date of last modification). By default, 0 to force splashscreen to appear at first launch
[gui.session.diplomacycolors]
self = "21 55 149" ; Color of your units when diplomacy colors are enabled
ally = "86 180 31" ; Color of allies when diplomacy colors are enabled
neutral = "231 200 5" ; Color of neutral players when diplomacy colors are enabled
enemy = "150 20 20" ; Color of enemies when diplomacy colors are enabled
[joystick] ; EXPERIMENTAL: joystick/gamepad settings
enable = false
deadzone = 8192
[joystick.camera]
pan.x = 0
pan.y = 1
rotate.x = 3
rotate.y = 2
zoom.in = 5
zoom.out = 4
[chat]
timestamp = true ; Show at which time chat messages have been sent
[chat.session]
extended = true ; Whether to display the chat history
[lobby]
history = 0 ; Number of past messages to display on join
room = "arena23" ; Default MUC room to join
server = "lobby.wildfiregames.com" ; Address of lobby server
xpartamupp = "wfgbot23" ; Name of the server-side XMPP-account that manage games
echelon = "echelon23" ; Name of the server-side XMPP-account that manages ratings
buddies = "," ; Comma separated list of playernames that the current user has marked as buddies
rememberpassword = true ; Whether to store the encrypted password in the user config
secureauth = true ; Secure Lobby Authentication: This prevents the impersonation of other players. The lobby server confirms the identity of the player before they join.
[lobby.columns]
gamerating = false ; Show the average rating of the participating players in a column of the gamelist
[lobby.stun]
enabled = true ; The STUN protocol allows hosting games without configuring the firewall and router.
; If STUN is disabled, the game relies on direct connection, UPnP and port forwarding.
server = "lobby.wildfiregames.com" ; Address of the STUN server.
port = 3478 ; Port of the STUN server.
delay = 200 ; Duration in milliseconds that is waited between STUN messages.
; Smaller numbers speed up joins but also become less stable.
[mod]
enabledmods = "mod public"
+[modio]
+public_key = "RWQBhIRg+dOifTWlwgYHe8RfD8bqoDh1cCvygboAl3GOUKiCo0NlF4fw" ; Public key corresponding to the private key valid mods are signed with
+
+[modio.v1]
+baseurl = "https://api.mod.io/v1"
+api_key = "23df258a71711ea6e4b50893acc1ba55"
+name_id = "0ad"
+
[network]
duplicateplayernames = false ; Rename joining player to "User (2)" if "User" is already connected, otherwise prohibit join.
lateobservers = everyone ; Allow observers to join the game after it started. Possible values: everyone, buddies, disabled.
observerlimit = 8 ; Prevent further observer joins in running games if this limit is reached
[overlay]
fps = "false" ; Show frames per second in top right corner
realtime = "false" ; Show current system time in top right corner
netwarnings = "true" ; Show warnings if the network connection is bad
[profiler2]
autoenable = false ; Enable HTTP server output at startup (default off for security/performance)
gpu.arb.enable = true ; Allow GL_ARB_timer_query timing mode when available
gpu.ext.enable = true ; Allow GL_EXT_timer_query timing mode when available
gpu.intel.enable = true ; Allow GL_INTEL_performance_queries timing mode when available
[sound]
mastergain = 0.9
musicgain = 0.2
ambientgain = 0.6
actiongain = 0.7
uigain = 0.7
[sound.notify]
nick = true ; Play a sound when someone mentions your name in the lobby or game
[tinygettext]
debug = false ; Print error messages each time a translation for an English string is not found.
[userreport] ; Opt-in online user reporting system
url = "http://feedback.wildfiregames.com/report/upload/v1/"
[view] ; Camera control settings
scroll.speed = 120.0
scroll.speed.modifier = 1.05 ; Multiplier for changing scroll speed
rotate.x.speed = 1.2
rotate.x.min = 28.0
rotate.x.max = 60.0
rotate.x.default = 35.0
rotate.y.speed = 2.0
rotate.y.speed.wheel = 0.45
rotate.y.default = 0.0
rotate.speed.modifier = 1.05 ; Multiplier for changing rotation speed
drag.speed = 0.5
zoom.speed = 256.0
zoom.speed.wheel = 32.0
zoom.min = 50.0
zoom.max = 200.0
zoom.default = 120.0
zoom.speed.modifier = 1.05 ; Multiplier for changing zoom speed
pos.smoothness = 0.1
zoom.smoothness = 0.4
rotate.x.smoothness = 0.5
rotate.y.smoothness = 0.3
near = 2.0 ; Near plane distance
far = 4096.0 ; Far plane distance
fov = 45.0 ; Field of view (degrees), lower is narrow, higher is wide
height.smoothness = 0.5
height.min = 16
Index: ps/trunk/binaries/data/mods/mod/gui/common/l10n.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/common/l10n.js (revision 21758)
+++ ps/trunk/binaries/data/mods/mod/gui/common/l10n.js (revision 21759)
@@ -1,197 +1,232 @@
/**
+ * @param filesize - In bytes.
+ * @return Object with quantized filesize and suitable unit of size.
+ */
+function filesizeToObj(filesize)
+{
+ // We are unlikely to download files measured in units greater than GiB.
+ let units = [
+ translateWithContext("filesize unit", "B"),
+ translateWithContext("filesize unit", "KiB"),
+ translateWithContext("filesize unit", "MiB"),
+ translateWithContext("filesize unit", "GiB")
+ ];
+
+ let i = 0;
+ while (i < units.length - 1)
+ {
+ if (filesize < 1024)
+ break;
+ filesize /= 1024;
+ ++i;
+ }
+
+ return {
+ "filesize": filesize.toFixed(i == 0 ? 0 : 1),
+ "unit": units[i]
+ };
+}
+
+function filesizeToString(filesize)
+{
+ // Translation: For example: 123.4 KiB
+ return sprintf(translate("%(filesize)s %(unit)s"), filesizeToObj(filesize));
+}
+
+/**
* Convert time in milliseconds to [HH:]mm:ss string representation.
*
* @param time Time period in milliseconds (integer)
* @return String representing time period
*/
function timeToString(time)
{
return Engine.FormatMillisecondsIntoDateStringGMT(time, time < 1000 * 60 * 60 ?
// Translation: Time-format string. See http://userguide.icu-project.org/formatparse/datetime for a guide to the meaning of the letters.
translate("mm:ss") : translate("HH:mm:ss"));
}
/**
* These functions rely on the JS cache where possible and
* should be prefered over the Engine.Translate ones to optimize the performance.
*/
var g_Translations = {};
var g_PluralTranslations = {};
var g_TranslationsWithContext = {};
var g_PluralTranslationsWithContext = {};
function isTranslatableString(message)
{
return typeof message == "string" && !!message.trim();
}
/**
* Translates the specified English message into the current language.
*/
function translate(message)
{
if (!g_Translations[message])
g_Translations[message] = Engine.Translate(message);
return g_Translations[message];
}
/**
* Translates the specified English message into the current language for the specified number.
*/
function translatePlural(singularMessage, pluralMessage, number)
{
if (!g_PluralTranslations[singularMessage])
g_PluralTranslations[singularMessage] = {};
if (!g_PluralTranslations[singularMessage][number])
g_PluralTranslations[singularMessage][number] = Engine.TranslatePlural(singularMessage, pluralMessage, number);
return g_PluralTranslations[singularMessage][number];
}
/**
* Translates the specified English message into the current language for the specified context.
*/
function translateWithContext(context, message)
{
if (!g_TranslationsWithContext[context])
g_TranslationsWithContext[context] = {};
if (!g_TranslationsWithContext[context][message])
g_TranslationsWithContext[context][message] = Engine.TranslateWithContext(context, message);
return g_TranslationsWithContext[context][message];
}
/**
* Translates the specified English message into the current language for the specified context and number.
*/
function translatePluralWithContext(context, singularMessage, pluralMessage, number)
{
if (!g_PluralTranslationsWithContext[context])
g_PluralTranslationsWithContext[context] = {};
if (!g_PluralTranslationsWithContext[context][singularMessage])
g_PluralTranslationsWithContext[context][singularMessage] = {};
if (!g_PluralTranslationsWithContext[context][singularMessage][number])
g_PluralTranslationsWithContext[context][singularMessage][number] =
Engine.TranslatePluralWithContext(context, singularMessage, pluralMessage, number);
return g_PluralTranslationsWithContext[context][singularMessage][number];
}
/**
* The input object should contain either of the following properties:
*
* • A ‘message’ property that contains a message to translate.
*
* • A ‘list’ property that contains a list of messages to translate as a
* comma-separated list of translated.
*
* Optionally, the input object may contain a ‘context’ property. In that case,
* the value of this property is used as translation context, that is, passed to
* the translateWithContext(context, message) function.
*/
function translateMessageObject(object)
{
let trans = translate;
if (object.context)
trans = msg => translateWithContext(object.context, msg);
if (object.message)
object = trans(object.message);
else if (object.list)
object = object.list.map(trans).join(translateWithContext("enumeration", ", "));
return object;
}
/**
* Translates any string value in the specified JavaScript object
* that is associated with a key included in the specified keys array.
*
* it accepts an object in the form of
*
* {
* translatedString1: "my first message",
* unTranslatedString1: "some English string",
* ignoredObject: {
* translatedString2: "my second message",
* unTranslatedString2: "some English string"
* },
* translatedObject1: {
* message: "my third singular message",
* context: "message context",
* },
* translatedObject2: {
* list: ["list", "of", "strings"],
* context: "message context",
* },
* }
*
* Together with a keys list to translate the strings and objects
* ["translatedString1", "translatedString2", "translatedObject1",
* "translatedObject2"]
*
* The result will be (f.e. in Dutch)
* {
* translatedString1: "mijn eerste bericht",
* unTranslatedString1: "some English string",
* ignoredObject: {
* translatedString2: "mijn tweede bericht",
* unTranslatedString2: "some English string"
* },
* translatedObject1: "mijn derde bericht",
* translatedObject2: "lijst, van, teksten",
* }
*
* So you see that the keys array can also contain lower-level keys,
* And that you can include objects in the keys array to translate
* them with a context, or to join a list of translations.
*
* Also, the keys array may be an object where properties are keys to translate
* and values are translation contexts to use for each key.
*/
function translateObjectKeys(object, keys)
{
if (keys instanceof Array)
{
for (let property in object)
{
if (keys.indexOf(property) > -1)
{
if (isTranslatableString(object[property]))
object[property] = translate(object[property]);
else if (object[property] instanceof Object)
object[property] = translateMessageObject(object[property]);
}
else if (object[property] instanceof Object)
translateObjectKeys(object[property], keys);
}
}
// If ‘keys’ is not an array, it is an object where keys are properties to
// translate and values are translation contexts to use for each key.
// An empty value means no context.
else
{
for (let property in object)
{
if (property in keys)
{
if (isTranslatableString(object[property]))
if (keys[property])
object[property] = translateWithContext(keys[property], object[property]);
else
object[property] = translate(object[property]);
else if (object[property] instanceof Object)
object[property] = translateMessageObject(object[property]);
}
else if (object[property] instanceof Object)
translateObjectKeys(object[property], keys);
}
}
}
Index: ps/trunk/binaries/data/mods/mod/gui/modio/modio.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/modio/modio.js (nonexistent)
+++ ps/trunk/binaries/data/mods/mod/gui/modio/modio.js (revision 21759)
@@ -0,0 +1,344 @@
+var g_ModsAvailableOnline = [];
+
+/**
+ * Indicates if we have encountered an error in one of the network-interaction attempts.
+ *
+ * We use a global so we don't get multiple messageBoxes appearing (one for each "tick").
+ *
+ * Set to `true` by showErrorMessageBox
+ * Set to `false` by init, updateModList, downloadFile, and cancelRequest
+ */
+var g_Failure;
+
+/**
+ * Indicates if the user has cancelled a request.
+ *
+ * Primarily used so the user can cancel the mod list fetch, as whenever that get cancelled,
+ * the modio state reverts to "ready", even if we've successfully listed mods before.
+ *
+ * Set to `true` by cancelRequest
+ * Set to `false` by updateModList, and downloadFile
+ */
+var g_RequestCancelled;
+
+var g_RequestStartTime;
+
+/**
+ * Returns true if ModIoAdvanceRequest should be called.
+ */
+var g_ModIOState = {
+ /**
+ * Finished status indicators
+ */
+ "ready": progressData => {
+ // GameID acquired, ready to fetch mod list
+ if (!g_RequestCancelled)
+ updateModList();
+ return true;
+ },
+ "listed": progressData => {
+ // List of available mods acquired
+
+ // Only run this once (for each update).
+ if (Engine.GetGUIObjectByName("modsAvailableList").list.length)
+ return true;
+
+ hideDialog();
+ Engine.GetGUIObjectByName("refreshButton").enabled = true;
+ g_ModsAvailableOnline = Engine.ModIoGetMods();
+ displayMods();
+ return true;
+ },
+ "success": progressData => {
+ // Successfully acquired a mod file
+ hideDialog();
+ Engine.GetGUIObjectByName("downloadButton").enabled = true;
+ return true;
+ },
+ /**
+ * In-progress status indicators.
+ */
+ "gameid": progressData => {
+ // Acquiring GameID from mod.io
+ return true;
+ },
+ "listing": progressData => {
+ // Acquiring list of available mods from mod.io
+ return true;
+ },
+ "downloading": progressData => {
+ // Downloading a mod file
+ updateProgressBar(progressData.progress, g_ModsAvailableOnline[selectedModIndex()].filesize);
+ return true;
+ },
+ /**
+ * Error/Failure status indicators.
+ */
+ "failed_gameid": progressData => {
+ // Game ID couldn't be retrieved
+ showErrorMessageBox(
+ sprintf(translateWithContext("mod.io error message", "Game ID could not be retrieved.\n\n%(technicalDetails)s"), {
+ "technicalDetails": progressData.error
+ }),
+ translateWithContext("mod.io error message", "Initialization Error"),
+ [translate("Abort"), translate("Retry")],
+ [closePage, init]);
+ return false;
+ },
+ "failed_listing": progressData => {
+ // Mod list couldn't be retrieved
+ showErrorMessageBox(
+ sprintf(translateWithContext("mod.io error message", "Mod List could not be retrieved.\n\n%(technicalDetails)s"), {
+ "technicalDetails": progressData.error
+ }),
+ translateWithContext("mod.io error message", "Fetch Error"),
+ [translate("Abort"), translate("Retry")],
+ [cancelModListUpdate, updateModList]);
+ return false;
+ },
+ "failed_downloading": progressData => {
+ // File couldn't be retrieved
+ showErrorMessageBox(
+ sprintf(translateWithContext("mod.io error message", "File download failed.\n\n%(technicalDetails)s"), {
+ "technicalDetails": progressData.error
+ }),
+ translateWithContext("mod.io error message", "Download Error"),
+ [translate("Abort"), translate("Retry")],
+ [cancelRequest, downloadMod]);
+ return false;
+ },
+ "failed_filecheck": progressData => {
+ // The file is corrupted
+ showErrorMessageBox(
+ sprintf(translateWithContext("mod.io error message", "File verification error.\n\n%(technicalDetails)s"), {
+ "technicalDetails": progressData.error
+ }),
+ translateWithContext("mod.io error message", "Verification Error"),
+ [translate("Abort")],
+ [cancelRequest]);
+ return false;
+ },
+ /**
+ * Default
+ */
+ "none": progressData => {
+ // Nothing has happened yet.
+ return true;
+ }
+};
+
+function init(data)
+{
+ progressDialog(
+ translate("Initializing mod.io interface."),
+ translate("Initializing"),
+ false,
+ translate("Cancel"),
+ closePage);
+
+ g_Failure = false;
+ Engine.ModIoStartGetGameId();
+}
+
+function onTick()
+{
+ let progressData = Engine.ModIoGetDownloadProgress();
+
+ let handler = g_ModIOState[progressData.status];
+ if (!handler)
+ {
+ warn("Unrecognized progress status: " + progressData.status);
+ return;
+ }
+
+ if (handler(progressData))
+ Engine.ModIoAdvanceRequest();
+}
+
+function displayMods()
+{
+ let modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList");
+ let selectedMod = modsAvailableList.list[modsAvailableList.selected];
+ modsAvailableList.selected = -1;
+
+ let displayedMods = clone(g_ModsAvailableOnline);
+ for (let i = 0; i < displayedMods.length; ++i)
+ displayedMods[i].i = i;
+
+ let filterColumns = ["name", "name_id", "summary"];
+ let filterText = Engine.GetGUIObjectByName("modFilter").caption.toLowerCase();
+ displayedMods = displayedMods.filter(mod => filterColumns.some(column => mod[column].toLowerCase().indexOf(filterText) != -1));
+
+ displayedMods.sort((mod1, mod2) =>
+ modsAvailableList.selected_column_order *
+ (modsAvailableList.selected_column == "filesize" ?
+ mod1.filesize - mod2.filesize :
+ String(mod1[modsAvailableList.selected_column]).localeCompare(String(mod2[modsAvailableList.selected_column]))));
+
+ modsAvailableList.list_name = displayedMods.map(mod => mod.name);
+ modsAvailableList.list_name_id = displayedMods.map(mod => mod.name_id);
+ modsAvailableList.list_version = displayedMods.map(mod => mod.version);
+ modsAvailableList.list_filesize = displayedMods.map(mod => filesizeToString(mod.filesize));
+ modsAvailableList.list_dependencies = displayedMods.map(mod => (mod.dependencies || []).join(" "));
+ modsAvailableList.list = displayedMods.map(mod => mod.i);
+ modsAvailableList.selected = modsAvailableList.list.indexOf(selectedMod);
+}
+
+function clearModList()
+{
+ let modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList");
+ modsAvailableList.selected = -1;
+ for (let listIdx of Object.keys(modsAvailableList).filter(key => key.startsWith("list")))
+ modsAvailableList[listIdx] = [];
+}
+
+function selectedModIndex()
+{
+ let modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList");
+
+ if (modsAvailableList.selected == -1)
+ return undefined;
+
+ return +modsAvailableList.list[modsAvailableList.selected];
+}
+
+function showModDescription()
+{
+ let selected = selectedModIndex();
+ Engine.GetGUIObjectByName("downloadButton").enabled = selected !== undefined;
+ Engine.GetGUIObjectByName("modDescription").caption = selected !== undefined ? g_ModsAvailableOnline[selected].summary : "";
+}
+
+function cancelModListUpdate()
+{
+ cancelRequest();
+
+ if (!g_ModsAvailableOnline.length)
+ {
+ closePage();
+ return;
+ }
+
+ displayMods();
+ Engine.GetGUIObjectByName('refreshButton').enabled = true;
+}
+
+function updateModList()
+{
+ clearModList();
+ Engine.GetGUIObjectByName("refreshButton").enabled = false;
+
+ progressDialog(
+ translate("Fetching and updating list of available mods."),
+ translate("Updating"),
+ false,
+ translate("Cancel Update"),
+ cancelModListUpdate);
+
+ g_Failure = false;
+ g_RequestCancelled = false;
+ Engine.ModIoStartListMods();
+}
+
+function downloadMod()
+{
+ let selected = selectedModIndex();
+
+ progressDialog(
+ sprintf(translate("Downloading “%(modname)s”"), {
+ "modname": g_ModsAvailableOnline[selected].name
+ }),
+ translate("Downloading"),
+ true,
+ translate("Cancel Download"),
+ () => { Engine.GetGUIObjectByName("downloadButton").enabled = true; });
+
+ Engine.GetGUIObjectByName("downloadButton").enabled = false;
+
+ g_Failure = false;
+ g_RequestCancelled = false;
+ Engine.ModIoStartDownloadMod(selected);
+}
+
+function cancelRequest()
+{
+ g_Failure = false;
+ g_RequestCancelled = true;
+ Engine.ModIoCancelRequest();
+ hideDialog();
+}
+
+function closePage(data)
+{
+ Engine.PopGuiPageCB(undefined);
+}
+
+function showErrorMessageBox(caption, title, buttonCaptions, buttonActions)
+{
+ if (g_Failure)
+ return;
+
+ messageBox(500, 250, caption, title, buttonCaptions, buttonActions);
+ g_Failure = true;
+}
+
+function progressDialog(dialogCaption, dialogTitle, showProgressBar, buttonCaption, buttonAction)
+{
+ Engine.GetGUIObjectByName("downloadDialog_title").caption = dialogTitle;
+
+ let downloadDialog_caption = Engine.GetGUIObjectByName("downloadDialog_caption");
+ downloadDialog_caption.caption = dialogCaption;
+
+ let size = downloadDialog_caption.size;
+ size.rbottom = showProgressBar ? 40 : 80;
+ downloadDialog_caption.size = size;
+
+ Engine.GetGUIObjectByName("downloadDialog_progress").hidden = !showProgressBar;
+ Engine.GetGUIObjectByName("downloadDialog_status").hidden = !showProgressBar;
+
+ let downloadDialog_button = Engine.GetGUIObjectByName("downloadDialog_button");
+ downloadDialog_button.caption = buttonCaption;
+ downloadDialog_button.onPress = () => { cancelRequest(); buttonAction(); };
+
+ Engine.GetGUIObjectByName("downloadDialog").hidden = false;
+
+ g_RequestStartTime = Date.now();
+}
+
+/*
+ * The "remaining time" and "average speed" texts both naively assume that
+ * the connection remains relatively stable throughout the download.
+ */
+function updateProgressBar(progress, totalSize)
+{
+ let progressPercent = Math.ceil(progress * 100);
+ Engine.GetGUIObjectByName("downloadDialog_progressBar").caption = progressPercent;
+
+ let transferredSize = progress * totalSize;
+ let transferredSizeObj = filesizeToObj(transferredSize);
+ // Translation: Mod file download indicator. Current size over expected final size, with percentage complete.
+ Engine.GetGUIObjectByName("downloadDialog_progressText").caption = sprintf(translate("%(current)s / %(total)s (%(percent)s%%)"), {
+ "current": filesizeToObj(totalSize).unit == transferredSizeObj.unit ? transferredSizeObj.filesize : filesizeToString(transferredSize),
+ "total": filesizeToString(totalSize),
+ "percent": progressPercent
+ });
+
+ let elapsedTime = Date.now() - g_RequestStartTime;
+ let remainingTime = progressPercent ? (100 - progressPercent) * elapsedTime / progressPercent : 0;
+ let avgSpeed = filesizeToObj(transferredSize / (elapsedTime / 1000));
+ // Translation: Mod file download status message.
+ Engine.GetGUIObjectByName("downloadDialog_status").caption = sprintf(translate("Time Elapsed: %(elapsed)s\nEstimated Time Remaining: %(remaining)s\nAverage Speed: %(avgSpeed)s"), {
+ "elapsed": timeToString(elapsedTime),
+ "remaining": remainingTime ? timeToString(remainingTime) : translate("∞"),
+ // Translation: Average download speed, used to give the user a very rough and naive idea of the download time. For example: 123.4 KiB/s
+ "avgSpeed": sprintf(translate("%(number)s %(unit)s/s"), {
+ "number": avgSpeed.filesize,
+ "unit": avgSpeed.unit
+ })
+ });
+}
+
+function hideDialog()
+{
+ Engine.GetGUIObjectByName("downloadDialog").hidden = true;
+}
Property changes on: ps/trunk/binaries/data/mods/mod/gui/modio/modio.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/mod/gui/modio/modio.xml
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/modio/modio.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/mod/gui/modio/modio.xml (revision 21759)
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/mod/gui/modio/modio.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/mod/gui/modmod/help/help.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/modmod/help/help.js (nonexistent)
+++ ps/trunk/binaries/data/mods/mod/gui/modmod/help/help.js (revision 21759)
@@ -0,0 +1,4 @@
+function init(data)
+{
+ Engine.GetGUIObjectByName("mainText").caption = Engine.TranslateLines(Engine.ReadFile("gui/modmod/help/help.txt"));
+}
Property changes on: ps/trunk/binaries/data/mods/mod/gui/modmod/help/help.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/mod/gui/modmod/help/help.txt
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/modmod/help/help.txt (nonexistent)
+++ ps/trunk/binaries/data/mods/mod/gui/modmod/help/help.txt (revision 21759)
@@ -0,0 +1,7 @@
+0 A.D. is designed to be easily modded. Mods are distributed in the form of .pyromod files, which can be opened like .zip files.
+
+In order to install a mod, just open the file with 0 A.D. (either double-click on the file and choose to open it with the game, or run "pyrogenesis file.pyromod" in a terminal). The mod will then be available in the mod selector. You can enable it and disable it at will. You can delete the mod manually using your file browser if needed (see https://trac.wildfiregames.com/wiki/GameDataPaths).
+
+For more information about modding the game, see the Modding Guide online (click the Modding Guide button below).
+
+The mod.io service is developed by DBolical, the company behind IndieDB and ModDB. Those websites have spread the word about 0 A.D. and other indie projects for a long time! Today, mod.io allows us to list and download all the mods that were verified by the team. Click "Download Mods" to try it out and install some!
Property changes on: ps/trunk/binaries/data/mods/mod/gui/modmod/help/help.txt
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/mod/gui/modmod/help/help.xml
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/modmod/help/help.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/mod/gui/modmod/help/help.xml (revision 21759)
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Pyrogenesis Mod Selector
+
+
+
+
+
+
+
+ Close
+ Engine.PopGuiPage();
+
+
+ Modding Guide
+ Engine.OpenURL("https://trac.wildfiregames.com/wiki/Modding_Guide");
+
+
+ Visit mod.io
+ Engine.OpenURL("https://mod.io");
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/mod/gui/modmod/help/help.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.js (revision 21758)
+++ ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.js (revision 21759)
@@ -1,398 +1,415 @@
/**
* @file This GUI page displays all available mods and allows the player to enabled and launch a set of compatible mods.
*/
/**
* A mod is defined by a mod.json file, for example
* {
* "name": "0ad",
* "version": "0.0.16",
* "label": "0 A.D. - Empires Ascendant",
* "url": "http://wildfiregames.com/",
* "description": "A free, open-source, historical RTS game.",
* "dependencies": []
* }
*
* Or:
* {
* "name": "mod2",
* "label": "Mod 2",
* "version": "1.1",
* "description": "",
* "dependencies": ["0ad<=0.0.16", "rote"]
* }
*
* A mod is identified by the directory name.
* A mod must define the "name", "version", "label", "description" and "dependencies" property.
* The "url" property is optional.
*
* The property "name" can consist alphanumeric characters, underscore and dash.
* The name is used for version comparison of mod dependencies.
* The property "version" may only contain numbers and up to two periods.
* The property "label" is a human-readable name of the mod.
* The property "description" is a human-readable summary of the features of the mod.
* The property "url" is reference to a website about the mod.
* The property "dependencies" is an array of strings. Each string is either a modname or a mod version comparison.
* A mod version comparison is a modname, followed by an operator (=, <, >, <= or >=), followed by a mod version.
* This allows mods to express upwards and downwards compatibility.
*/
/**
* Mod definitions loaded from the files, including invalid mods.
*/
var g_Mods = {};
/**
* Folder names of all mods that are or can be launched.
*/
var g_ModsEnabled = [];
var g_ModsDisabled = [];
/**
* Name of the mods installed by the ModInstaller.
*/
var g_InstalledMods;
var g_ColorNoModSelected = "255 255 100";
var g_ColorDependenciesMet = "100 255 100";
var g_ColorDependenciesNotMet = "255 100 100";
function init(data, hotloadData)
{
g_InstalledMods = data && data.installedMods || hotloadData && hotloadData.installedMods || [];
initMods();
initGUIButtons(data);
}
function initMods()
{
loadMods();
loadEnabledMods();
validateMods();
initGUIFilters();
}
function getHotloadData()
{
return { "installedMods": g_InstalledMods };
}
function loadMods()
{
g_Mods = Engine.GetAvailableMods();
deepfreeze(g_Mods);
}
function loadEnabledMods()
{
g_ModsEnabled = Engine.ConfigDB_GetValue("user", "mod.enabledmods").split(/\s+/).filter(folder => !!g_Mods[folder]);
g_ModsDisabled = Object.keys(g_Mods).filter(folder => g_ModsEnabled.indexOf(folder) == -1);
}
function validateMods()
{
for (let folder in g_Mods)
validateMod(folder, g_Mods[folder], true);
}
function initGUIFilters()
{
Engine.GetGUIObjectByName("negateFilter").checked = false;
Engine.GetGUIObjectByName("modGenericFilter").caption = translate("Filter");
displayModLists();
}
function initGUIButtons(data)
{
// Either get back to the previous page or quit if there is no previous page
let cancelButton = !data || data.cancelbutton;
Engine.GetGUIObjectByName("cancelButton").hidden = !cancelButton;
Engine.GetGUIObjectByName("quitButton").hidden = cancelButton;
}
function saveMods()
{
sortEnabledMods();
Engine.ConfigDB_CreateValue("user", "mod.enabledmods", ["mod"].concat(g_ModsEnabled).join(" "));
Engine.ConfigDB_WriteFile("user", "config/user.cfg");
}
function startMods()
{
sortEnabledMods();
Engine.SetMods(["mod"].concat(g_ModsEnabled));
Engine.RestartEngine();
}
function displayModLists()
{
displayModList("modsEnabledList", g_ModsEnabled);
displayModList("modsDisabledList", g_ModsDisabled);
}
function displayModList(listObjectName, folders)
{
let listObject = Engine.GetGUIObjectByName(listObjectName);
if (listObjectName == "modsDisabledList")
{
let sortFolder = folder => String(g_Mods[folder][listObject.selected_column] || folder);
folders.sort((folder1, folder2) =>
listObject.selected_column_order *
sortFolder(folder1).localeCompare(sortFolder(folder2)));
}
folders = folders.filter(filterMod);
listObject.list_name = folders.map(folder => g_Mods[folder].name).map(name => g_InstalledMods.indexOf(name) == -1 ? name : coloredText(name, "green"));
listObject.list_folder = folders;
listObject.list_label = folders.map(folder => g_Mods[folder].label);
listObject.list_url = folders.map(folder => g_Mods[folder].url || "");
listObject.list_version = folders.map(folder => g_Mods[folder].version);
listObject.list_dependencies = folders.map(folder => g_Mods[folder].dependencies.join(" "));
listObject.list = folders;
}
function enableMod()
{
let modsDisabledList = Engine.GetGUIObjectByName("modsDisabledList");
let pos = modsDisabledList.selected;
if (pos == -1 || !areDependenciesMet(g_ModsDisabled[pos]))
return;
g_ModsEnabled.push(g_ModsDisabled.splice(pos, 1)[0]);
if (pos >= g_ModsDisabled.length)
--pos;
modsDisabledList.selected = pos;
displayModLists();
}
function disableMod()
{
let modsEnabledList = Engine.GetGUIObjectByName("modsEnabledList");
let pos = modsEnabledList.selected;
if (pos == -1)
return;
g_ModsDisabled.push(g_ModsEnabled.splice(pos, 1)[0]);
// Remove mods that required the removed mod and cascade
// Sort them, so we know which ones can depend on the removed mod
// TODO: Find position where the removed mod would have fit (for now assume idx 0)
sortEnabledMods();
for (let i = 0; i < g_ModsEnabled.length; ++i)
if (!areDependenciesMet(g_ModsEnabled[i]))
{
g_ModsDisabled.push(g_ModsEnabled.splice(i, 1)[0]);
--i;
}
modsEnabledList.selected = Math.min(pos, g_ModsEnabled.length - 1);
displayModLists();
}
function applyFilters()
{
// Save selected rows
let modsDisabledList = Engine.GetGUIObjectByName("modsDisabledList");
let modsEnabledList = Engine.GetGUIObjectByName("modsEnabledList");
let selectedDisabledFolder = modsDisabledList.list_folder[modsDisabledList.selected];
let selectedEnabledFolder = modsEnabledList.list_folder[modsEnabledList.selected];
// Remove selected rows to prevent a link to a non existing item
modsDisabledList.selected = -1;
modsEnabledList.selected = -1;
displayModLists();
// Restore previously selected rows
modsDisabledList.selected = modsDisabledList.list_folder.indexOf(selectedDisabledFolder);
modsEnabledList.selected = modsEnabledList.list_folder.indexOf(selectedEnabledFolder);
Engine.GetGUIObjectByName("globalModDescription").caption = "";
}
function filterMod(folder)
{
let mod = g_Mods[folder];
let negateFilter = Engine.GetGUIObjectByName("negateFilter").checked;
let searchText = Engine.GetGUIObjectByName("modGenericFilter").caption;
if (searchText &&
searchText != translate("Filter") &&
folder.indexOf(searchText) == -1 &&
mod.name.indexOf(searchText) == -1 &&
mod.label.indexOf(searchText) == -1 &&
(mod.url || "").indexOf(searchText) == -1 &&
mod.version.indexOf(searchText) == -1 &&
mod.description.indexOf(searchText) == -1 &&
mod.dependencies.indexOf(searchText) == -1)
return negateFilter;
return !negateFilter;
}
function closePage()
{
Engine.SwitchGuiPage("page_pregame.xml", {});
}
/**
* Moves an item in the list up or down.
*/
function moveCurrItem(objectName, up)
{
let obj = Engine.GetGUIObjectByName(objectName);
let idx = obj.selected;
if (idx == -1)
return;
let num = obj.list.length;
let idx2 = idx + (up ? -1 : 1);
if (idx2 < 0 || idx2 >= num)
return;
let tmp = g_ModsEnabled[idx];
g_ModsEnabled[idx] = g_ModsEnabled[idx2];
g_ModsEnabled[idx2] = tmp;
obj.list = g_ModsEnabled;
obj.selected = idx2;
displayModList("modsEnabledList", g_ModsEnabled);
}
function areDependenciesMet(folder)
{
let guiObject = Engine.GetGUIObjectByName("message");
for (let dependency of g_Mods[folder].dependencies)
{
if (isDependencyMet(dependency))
continue;
guiObject.caption = coloredText(
sprintf(translate('Dependency not met: %(dep)s'), { "dep": dependency }),
g_ColorDependenciesNotMet);
return false;
}
guiObject.caption = coloredText(translate('All dependencies met'), g_ColorDependenciesMet);
return true;
}
/**
* @param dependency is a mod name or a mod version comparison.
*/
function isDependencyMet(dependency)
{
let operator = dependency.match(g_RegExpComparisonOperator);
let [name, version] = operator ? dependency.split(operator[0]) : [dependency, undefined];
return g_ModsEnabled.some(folder =>
g_Mods[folder].name == name &&
(!operator || versionSatisfied(g_Mods[folder].version, operator[0], version)));
}
+function modIo()
+{
+ messageBox(500, 250,
+ translate("You are about to connect to the mod.io online service. This provides easy access to community-made mods, but is not under the control of Wildfire Games.\n\nWhile we have taken care to make this secure, we cannot guarantee with absolute certainty that this is not a security risk.\n\nDo you really want to connect?"),
+ translate("Connect to mod.io?"),
+ [translate("Cancel"), translateWithContext("mod.io connection message box", "Connect")],
+ [
+ null,
+ () => {
+ Engine.PushGuiPage("page_modio.xml", {
+ "callback": "initMods"
+ });
+ }
+ ]
+ );
+}
+
/**
* Compares the given versions using the given operator.
* '-' or '_' is ignored. Only numbers are supported.
* @note "5.3" < "5.3.0"
*/
function versionSatisfied(version1, operator, version2)
{
let versionList1 = version1.split(/[-_]/)[0].split(/\./g);
let versionList2 = version2.split(/[-_]/)[0].split(/\./g);
let eq = operator.indexOf("=") != -1;
let lt = operator.indexOf("<") != -1;
let gt = operator.indexOf(">") != -1;
for (let i = 0; i < Math.min(versionList1.length, versionList2.length); ++i)
{
let diff = +versionList1[i] - +versionList2[i];
if (gt && diff > 0 || lt && diff < 0)
return true;
if (gt && diff < 0 || lt && diff > 0 || eq && diff)
return false;
}
// common prefix matches
let ldiff = versionList1.length - versionList2.length;
if (!ldiff)
return eq;
// NB: 2.3 != 2.3.0
if (ldiff < 0)
return lt;
return gt;
}
function sortEnabledMods()
{
let dependencies = {};
for (let folder of g_ModsEnabled)
dependencies[folder] = g_Mods[folder].dependencies.map(d => d.split(g_RegExpComparisonOperator)[0]);
g_ModsEnabled.sort((folder1, folder2) =>
dependencies[folder1].indexOf(g_Mods[folder2].name) != -1 ? 1 :
dependencies[folder2].indexOf(g_Mods[folder1].name) != -1 ? -1 : 0);
displayModList("modsEnabledList", g_ModsEnabled);
}
function selectedMod(listObjectName)
{
let listObject = Engine.GetGUIObjectByName(listObjectName);
let otherListObject = Engine.GetGUIObjectByName(listObjectName == "modsDisabledList" ?
"modsEnabledList" : "modsDisabledList");
if (listObject.selected != -1)
{
otherListObject.selected = -1;
Engine.GetGUIObjectByName("visitWebButton").enabled = true;
let toggleModButton = Engine.GetGUIObjectByName("toggleModButton");
toggleModButton.caption = listObjectName == "modsDisabledList" ? "Enable" : "Disable";
toggleModButton.enabled = true;
toggleModButton.onPress = listObjectName == "modsDisabledList" ? enableMod : disableMod;
Engine.GetGUIObjectByName("enabledModUp").enabled = listObjectName == "modsEnabledList";
Engine.GetGUIObjectByName("enabledModDown").enabled = listObjectName == "modsEnabledList";
}
Engine.GetGUIObjectByName("globalModDescription").caption =
listObject.list[listObject.selected] ?
g_Mods[listObject.list[listObject.selected]].description :
'[color="' + g_ColorNoModSelected + '"]' + translate("No mod has been selected.") + '[/color]';
}
function visitModWebsite()
{
let modsEnabledList = Engine.GetGUIObjectByName("modsEnabledList");
let modsDisabledList = Engine.GetGUIObjectByName("modsDisabledList");
let list = modsEnabledList.selected == -1 ? modsDisabledList : modsEnabledList;
let folder = list.list_folder[list.selected];
let url = folder && g_Mods[folder] && g_Mods[folder].url;
if (!url)
return;
if (!url.startsWith("http://") && !url.startsWith("https://"))
url = "http://" + url;
Engine.OpenURL(url);
}
Index: ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.xml
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.xml (revision 21758)
+++ ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.xml (revision 21759)
@@ -1,198 +1,208 @@
ModificationsapplyFilters();applyFilters();applyFilters();NegateDescriptionAvailable ModsselectedMod(this.name);applyFilters();enableMod();NameVersion(Folder)Mod LabelDependenciesWebsiteEnabled ModsselectedMod(this.name);disableMod();NameVersion(Folder)Mod LabelDependenciesWebsiteChange the order in which mods are launched. This should match the mods dependencies.moveCurrItem("modsEnabledList", true);Change the order in which mods are launched. This should match the mods dependencies.moveCurrItem("modsEnabledList", false);Enable/DisableVisit WebsitevisitModWebsite();
-
+ QuitEngine.Exit();
-
+ CancelclosePage();
+
+ Help
+ Engine.PushGuiPage("page_modhelp.xml");
+
+
+
+ Download Mods
+ modIo();
+
+
Save ConfigurationsaveMods();Start ModsstartMods();
Index: ps/trunk/binaries/data/mods/mod/gui/page_modhelp.xml
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/page_modhelp.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/mod/gui/page_modhelp.xml (revision 21759)
@@ -0,0 +1,8 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ modmod/help/help.xml
+
Property changes on: ps/trunk/binaries/data/mods/mod/gui/page_modhelp.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/mod/gui/page_modio.xml
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/page_modio.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/mod/gui/page_modio.xml (revision 21759)
@@ -0,0 +1,10 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ modmod/styles.xml
+
+ modio/modio.xml
+
Property changes on: ps/trunk/binaries/data/mods/mod/gui/page_modio.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/mod/l10n/messages.json
===================================================================
--- ps/trunk/binaries/data/mods/mod/l10n/messages.json (revision 21758)
+++ ps/trunk/binaries/data/mods/mod/l10n/messages.json (revision 21759)
@@ -1,45 +1,53 @@
[
{
"output": "mod-selector.pot",
"inputRoot": "..",
"project": "Pyrogenesis - Mod Selector",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"gui/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"gui/**.xml"
],
"options": {
"format": "none",
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
+ },
+ {
+ "extractor": "txt",
+ "filemasks": [
+ "gui/**.txt"
+ ],
+ "options": {
+ }
}
]
}
]
Index: ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.xml (revision 21758)
+++ ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.xml (revision 21759)
@@ -1,681 +1,681 @@
onTick();
[font="sans-bold-16"]Help improve 0 A.D.![/font]\nYou can automatically send us anonymous feedback that will help us fix bugs, and improve performance and compatibility.Enable FeedbackEnableUserReport(true);Technical Details
Engine.PushGuiPage("page_manual.xml", {
"page": "manual/userreport",
"title": getTechnicalDetails()
});
Disable FeedbackEnableUserReport(false);Technical Details
Engine.PushGuiPage("page_manual.xml", {
"page": "manual/userreport",
"title": getTechnicalDetails()
});
closeMenu();
closeMenu();
ManualOpen the 0 A.D. Game Manual.
closeMenu();
Engine.PushGuiPage("page_manual.xml", {
"page": "manual/intro",
"title": getManual(),
"url": "http://trac.wildfiregames.com/wiki/0adManual"
});
TutorialStart the economic tutorial.
Engine.SwitchGuiPage("page_gamesetup.xml", { "type": "offline", "tutorial": true });
Structure Tree
closeMenu();
Engine.PushGuiPage("page_structree.xml", {});
History
closeMenu();
Engine.PushGuiPage("page_civinfo.xml");
MatchesClick here to start a new single player game.
Engine.SwitchGuiPage("page_gamesetup.xml", { "type": "offline" });
CampaignsRelive history through historical military campaigns. \[NOT YET IMPLEMENTED]
closeMenu();
Load GameClick here to load a saved game.
closeMenu();
Engine.PushGuiPage("page_loadgame.xml", { "type": "offline" });
ReplaysPlayback previous games.
closeMenu();
Engine.SwitchGuiPage("page_replaymenu.xml", {
"replaySelectionData": {
"filters": {
"singleplayer": "Singleplayer"
}
}
});
Join GameJoining an existing multiplayer game.
closeMenu();
Engine.PushGuiPage("page_gamesetup_mp.xml", { "multiplayerGameType": "join" });
Host GameHost a multiplayer game.\n\nRequires UDP port 20595 to be open.
closeMenu();
Engine.PushGuiPage("page_gamesetup_mp.xml", { multiplayerGameType: "host" });
Game Lobby
if (!Engine.StartXmppClient)
return;
closeMenu();
Engine.PushGuiPage("page_prelobby.xml");
if (!Engine.StartXmppClient)
{
this.enabled = false;
this.tooltip = getLobbyDisabledByBuild();
}
ReplaysPlayback previous games.
closeMenu();
Engine.SwitchGuiPage("page_replaymenu.xml", {
"replaySelectionData": {
"filters": {
"singleplayer": "Multiplayer"
}
}
});
OptionsAdjust game settings.
closeMenu();
Engine.PushGuiPage("page_options.xml");
LanguageChoose the language of the game.
closeMenu();
Engine.PushGuiPage("page_locale.xml");
Mod Selection
- Select mods to use.
+ Select and download mods for the game.
Engine.SwitchGuiPage("page_modmod.xml");
Welcome ScreenShow the Welcome Screen. Useful if you hid it by mistake.
closeMenu();
Engine.PushGuiPage("page_splashscreen.xml", {
"page": "splashscreen",
"callback": "SplashScreenClosedCallback"
});
Learn to PlayLearn how to play, start the tutorial, discover the technology trees, and the history behind the civilizations
closeMenu();
openMenu("submenuLearn", this.parent.size.top + this.size.top, this.size.bottom - this.size.top, 4);
Single PlayerChallenge the computer player to a single player match.
closeMenu();
openMenu("submenuSinglePlayer", this.parent.size.top + this.size.top, this.size.bottom - this.size.top, 4);
MultiplayerFight against one or more human players in a multiplayer game.
closeMenu();
openMenu("submenuMultiplayer", this.parent.size.top + this.size.top, this.size.bottom - this.size.top, 4);
SettingsGame options and mod selection.
closeMenu();
openMenu("submenuOptions", this.parent.size.top + this.size.top, this.size.bottom - this.size.top, 4);
Scenario EditorOpen the Atlas Scenario Editor in a new window. You can run this more reliably by starting the game with the command-line argument "-editor".
pressedScenarioEditorButton();
ExitExits the game.exitGamePressed();[font="sans-bold-16"]Alpha XXIII: Ken Wood[/font]\n\nWARNING: This is an early development version of the game. Many features have not been added yet.WebsiteClick to open play0ad.com in your web browser.
openURL("http://play0ad.com/");
ChatClick to open the 0 A.D. IRC chat in your browser. (#0ad on webchat.quakenet.org)
openURL("http://webchat.quakenet.org/?channels=0ad");
Report a BugClick to visit 0 A.D. Trac to report a bug, crash, or error.
openURL("http://trac.wildfiregames.com/wiki/ReportingErrors/");
Translate the GameClick to open the 0 A.D. translate page in your browser.
openURL("http://trac.wildfiregames.com/wiki/Localization");
DonateHelp with the project expenses by donating.
openURL("https://play0ad.com/community/donate/");
CreditsClick to see the 0 A.D. credits.
Engine.PushGuiPage("page_credits.xml");
WILDFIRE GAMES
this.caption = getBuildString();
Index: ps/trunk/build/premake/premake5.lua
===================================================================
--- ps/trunk/build/premake/premake5.lua (revision 21758)
+++ ps/trunk/build/premake/premake5.lua (revision 21759)
@@ -1,1452 +1,1453 @@
newoption { trigger = "android", description = "Use non-working Android cross-compiling mode" }
newoption { trigger = "atlas", description = "Include Atlas scenario editor projects" }
newoption { trigger = "coverage", description = "Enable code coverage data collection (GCC only)" }
newoption { trigger = "gles", description = "Use non-working OpenGL ES 2.0 mode" }
newoption { trigger = "icc", description = "Use Intel C++ Compiler (Linux only; should use either \"--cc icc\" or --without-pch too, and then set CXX=icpc before calling make)" }
newoption { trigger = "jenkins-tests", description = "Configure CxxTest to use the XmlPrinter runner which produces Jenkins-compatible output" }
newoption { trigger = "minimal-flags", description = "Only set compiler/linker flags that are really needed. Has no effect on Windows builds" }
newoption { trigger = "outpath", description = "Location for generated project files" }
newoption { trigger = "with-system-mozjs38", description = "Search standard paths for libmozjs38, instead of using bundled copy" }
newoption { trigger = "with-system-nvtt", description = "Search standard paths for nvidia-texture-tools library, instead of using bundled copy" }
newoption { trigger = "without-audio", description = "Disable use of OpenAL/Ogg/Vorbis APIs" }
newoption { trigger = "without-lobby", description = "Disable the use of gloox and the multiplayer lobby" }
newoption { trigger = "without-miniupnpc", description = "Disable use of miniupnpc for port forwarding" }
newoption { trigger = "without-nvtt", description = "Disable use of NVTT" }
newoption { trigger = "without-pch", description = "Disable generation and usage of precompiled headers" }
newoption { trigger = "without-tests", description = "Disable generation of test projects" }
-- OS X specific options
newoption { trigger = "macosx-bundle", description = "Enable OSX bundle, the argument is the bundle identifier string (e.g. com.wildfiregames.0ad)" }
newoption { trigger = "macosx-version-min", description = "Set minimum required version of the OS X API, the build will possibly fail if an older SDK is used, while newer API functions will be weakly linked (i.e. resolved at runtime)" }
newoption { trigger = "sysroot", description = "Set compiler system root path, used for building against a non-system SDK. For example /usr/local becomes SYSROOT/user/local" }
-- Windows specific options
newoption { trigger = "build-shared-glooxwrapper", description = "Rebuild glooxwrapper DLL for Windows. Requires the same compiler version that gloox was built with" }
newoption { trigger = "use-shared-glooxwrapper", description = "Use prebuilt glooxwrapper DLL for Windows" }
newoption { trigger = "large-address-aware", description = "Make the executable large address aware. Do not use for development, in order to spot memory issues easily" }
-- Install options
newoption { trigger = "bindir", description = "Directory for executables (typically '/usr/games'); default is to be relocatable" }
newoption { trigger = "datadir", description = "Directory for data files (typically '/usr/share/games/0ad'); default is ../data/ relative to executable" }
newoption { trigger = "libdir", description = "Directory for libraries (typically '/usr/lib/games/0ad'); default is ./ relative to executable" }
-- Root directory of project checkout relative to this .lua file
rootdir = "../.."
dofile("extern_libs5.lua")
-- detect compiler for non-Windows
if os.istarget("macosx") then
cc = "clang"
elseif os.istarget("linux") and _OPTIONS["icc"] then
cc = "icc"
elseif not os.istarget("windows") then
cc = os.getenv("CC")
if cc == nil or cc == "" then
local hasgcc = os.execute("which gcc > .gccpath")
local f = io.open(".gccpath", "r")
local gccpath = f:read("*line")
f:close()
os.execute("rm .gccpath")
if gccpath == nil then
cc = "clang"
else
cc = "gcc"
end
end
end
-- detect CPU architecture (simplistic, currently only supports x86, amd64 and ARM)
arch = "x86"
if _OPTIONS["android"] then
arch = "arm"
elseif os.istarget("windows") then
if os.getenv("PROCESSOR_ARCHITECTURE") == "amd64" or os.getenv("PROCESSOR_ARCHITEW6432") == "amd64" then
arch = "amd64"
end
else
arch = os.getenv("HOSTTYPE")
if arch == "x86_64" or arch == "amd64" then
arch = "amd64"
else
os.execute(cc .. " -dumpmachine > .gccmachine.tmp")
local f = io.open(".gccmachine.tmp", "r")
local machine = f:read("*line")
f:close()
if string.find(machine, "x86_64") == 1 or string.find(machine, "amd64") == 1 then
arch = "amd64"
elseif string.find(machine, "i.86") == 1 then
arch = "x86"
elseif string.find(machine, "arm") == 1 then
arch = "arm"
elseif string.find(machine, "aarch64") == 1 then
arch = "aarch64"
else
print("WARNING: Cannot determine architecture from GCC, assuming x86")
end
end
end
-- Set up the Workspace
workspace "pyrogenesis"
targetdir(rootdir.."/binaries/system")
libdirs(rootdir.."/binaries/system")
if not _OPTIONS["outpath"] then
error("You must specify the 'outpath' parameter")
end
location(_OPTIONS["outpath"])
configurations { "Release", "Debug" }
source_root = rootdir.."/source/" -- default for most projects - overridden by local in others
-- Rationale: projects should not have any additional include paths except for
-- those required by external libraries. Instead, we should always write the
-- full relative path, e.g. #include "maths/Vector3d.h". This avoids confusion
-- ("which file is meant?") and avoids enormous include path lists.
-- projects: engine static libs, main exe, atlas, atlas frontends, test.
--------------------------------------------------------------------------------
-- project helper functions
--------------------------------------------------------------------------------
function project_set_target(project_name)
-- Note: On Windows, ".exe" is added on the end, on unices the name is used directly
local obj_dir_prefix = _OPTIONS["outpath"].."/obj/"..project_name.."_"
filter "Debug"
objdir(obj_dir_prefix.."Debug")
targetsuffix("_dbg")
filter "Release"
objdir(obj_dir_prefix.."Release")
filter { }
end
function project_set_build_flags()
editandcontinue "Off"
if not _OPTIONS["minimal-flags"] then
symbols "On"
end
if cc ~= "icc" and (os.istarget("windows") or not _OPTIONS["minimal-flags"]) then
-- adds the -Wall compiler flag
warnings "Extra" -- this causes far too many warnings/remarks on ICC
end
-- disable Windows debug heap, since it makes malloc/free hugely slower when
-- running inside a debugger
if os.istarget("windows") then
debugenvs { "_NO_DEBUG_HEAP=1" }
end
filter "Debug"
defines { "DEBUG" }
filter "Release"
if os.istarget("windows") or not _OPTIONS["minimal-flags"] then
optimize "Speed"
end
defines { "NDEBUG", "CONFIG_FINAL=1" }
filter { }
if _OPTIONS["gles"] then
defines { "CONFIG2_GLES=1" }
end
if _OPTIONS["without-audio"] then
defines { "CONFIG2_AUDIO=0" }
end
if _OPTIONS["without-nvtt"] then
defines { "CONFIG2_NVTT=0" }
end
if _OPTIONS["without-lobby"] then
defines { "CONFIG2_LOBBY=0" }
end
if _OPTIONS["without-miniupnpc"] then
defines { "CONFIG2_MINIUPNPC=0" }
end
-- required for the lowlevel library. must be set from all projects that use it, otherwise it assumes it is
-- being used as a DLL (which is currently not the case in 0ad)
defines { "LIB_STATIC_LINK" }
-- various platform-specific build flags
if os.istarget("windows") then
flags { "MultiProcessorCompile" }
-- use native wchar_t type (not typedef to unsigned short)
nativewchar "on"
else -- *nix
-- TODO, FIXME: This check is incorrect because it means that some additional flags will be added inside the "else" branch if the
-- compiler is ICC and minimal-flags is specified (ticket: #2994)
if cc == "icc" and not _OPTIONS["minimal-flags"] then
buildoptions {
"-w1",
-- "-Wabi",
-- "-Wp64", -- complains about OBJECT_TO_JSVAL which is annoying
"-Wpointer-arith",
"-Wreturn-type",
-- "-Wshadow",
"-Wuninitialized",
"-Wunknown-pragmas",
"-Wunused-function",
"-wd1292" -- avoid lots of 'attribute "__nonnull__" ignored'
}
filter "Debug"
buildoptions { "-O0" } -- ICC defaults to -O2
filter { }
if os.istarget("macosx") then
linkoptions { "-multiply_defined","suppress" }
end
else
-- exclude most non-essential build options for minimal-flags
if not _OPTIONS["minimal-flags"] then
buildoptions {
-- enable most of the standard warnings
"-Wno-switch", -- enumeration value not handled in switch (this is sometimes useful, but results in lots of noise)
"-Wno-reorder", -- order of initialization list in constructors (lots of noise)
"-Wno-invalid-offsetof", -- offsetof on non-POD types (see comment in renderer/PatchRData.cpp)
"-Wextra",
"-Wno-missing-field-initializers", -- (this is common in external headers we can't fix)
-- add some other useful warnings that need to be enabled explicitly
"-Wunused-parameter",
"-Wredundant-decls", -- (useful for finding some multiply-included header files)
-- "-Wformat=2", -- (useful sometimes, but a bit noisy, so skip it by default)
-- "-Wcast-qual", -- (useful for checking const-correctness, but a bit noisy, so skip it by default)
"-Wnon-virtual-dtor", -- (sometimes noisy but finds real bugs)
"-Wundef", -- (useful for finding macro name typos)
-- enable security features (stack checking etc) that shouldn't have
-- a significant effect on performance and can catch bugs
"-fstack-protector-all",
"-U_FORTIFY_SOURCE", -- (avoid redefinition warning if already defined)
"-D_FORTIFY_SOURCE=2",
-- always enable strict aliasing (useful in debug builds because of the warnings)
"-fstrict-aliasing",
-- don't omit frame pointers (for now), because performance will be impacted
-- negatively by the way this breaks profilers more than it will be impacted
-- positively by the optimisation
"-fno-omit-frame-pointer"
}
if not _OPTIONS["without-pch"] then
buildoptions {
-- do something (?) so that ccache can handle compilation with PCH enabled
-- (ccache 3.1+ also requires CCACHE_SLOPPINESS=time_macros for this to work)
"-fpch-preprocess"
}
end
if os.istarget("linux") or os.istarget("bsd") then
buildoptions { "-fPIC" }
linkoptions { "-Wl,--no-undefined", "-Wl,--as-needed", "-Wl,-z,relro" }
end
if arch == "x86" then
buildoptions {
-- To support intrinsics like __sync_bool_compare_and_swap on x86
-- we need to set -march to something that supports them (i686).
-- We use pentium3 to also enable other features like mmx and sse,
-- while tuning for generic to have good performance on every
-- supported CPU.
-- Note that all these features are already supported on amd64.
"-march=pentium3 -mtune=generic"
}
end
end
buildoptions {
-- Enable C++11 standard.
"-std=c++0x"
}
if arch == "arm" then
-- disable warnings about va_list ABI change and use
-- compile-time flags for futher configuration.
buildoptions { "-Wno-psabi" }
if _OPTIONS["android"] then
-- Android uses softfp, so we should too.
buildoptions { "-mfloat-abi=softfp" }
end
end
if _OPTIONS["coverage"] then
buildoptions { "-fprofile-arcs", "-ftest-coverage" }
links { "gcov" }
end
-- We don't want to require SSE2 everywhere yet, but OS X headers do
-- require it (and Intel Macs always have it) so enable it here
if os.istarget("macosx") then
buildoptions { "-msse2" }
end
-- Check if SDK path should be used
if _OPTIONS["sysroot"] then
buildoptions { "-isysroot " .. _OPTIONS["sysroot"] }
linkoptions { "-Wl,-syslibroot," .. _OPTIONS["sysroot"] }
end
-- On OS X, sometimes we need to specify the minimum API version to use
if _OPTIONS["macosx-version-min"] then
buildoptions { "-mmacosx-version-min=" .. _OPTIONS["macosx-version-min"] }
-- clang and llvm-gcc look at mmacosx-version-min to determine link target
-- and CRT version, and use it to set the macosx_version_min linker flag
linkoptions { "-mmacosx-version-min=" .. _OPTIONS["macosx-version-min"] }
end
-- Check if we're building a bundle
if _OPTIONS["macosx-bundle"] then
defines { "BUNDLE_IDENTIFIER=" .. _OPTIONS["macosx-bundle"] }
end
-- On OS X, force using libc++ since it has better C++11 support,
-- now required by the game
if os.istarget("macosx") then
buildoptions { "-stdlib=libc++" }
linkoptions { "-stdlib=libc++" }
end
end
buildoptions {
-- Hide symbols in dynamic shared objects by default, for efficiency and for equivalence with
-- Windows - they should be exported explicitly with __attribute__ ((visibility ("default")))
"-fvisibility=hidden"
}
if _OPTIONS["bindir"] then
defines { "INSTALLED_BINDIR=" .. _OPTIONS["bindir"] }
end
if _OPTIONS["datadir"] then
defines { "INSTALLED_DATADIR=" .. _OPTIONS["datadir"] }
end
if _OPTIONS["libdir"] then
defines { "INSTALLED_LIBDIR=" .. _OPTIONS["libdir"] }
end
if os.istarget("linux") or os.istarget("bsd") then
-- To use our local shared libraries, they need to be found in the
-- runtime dynamic linker path. Add their path to -rpath.
if _OPTIONS["libdir"] then
linkoptions {"-Wl,-rpath," .. _OPTIONS["libdir"] }
else
-- On FreeBSD we need to allow use of $ORIGIN
if os.istarget("bsd") then
linkoptions { "-Wl,-z,origin" }
end
-- Adding the executable path and taking care of correct escaping
if _ACTION == "gmake" then
linkoptions { "-Wl,-rpath,'$$ORIGIN'" }
elseif _ACTION == "codeblocks" then
linkoptions { "-Wl,-R\\\\$$$ORIGIN" }
end
end
end
end
end
-- add X11 includes paths after all the others so they don't conflict with
-- bundled libs
function project_add_x11_dirs()
if not os.istarget("windows") and not os.istarget("macosx") then
-- X11 includes may be installed in one of a gadzillion of five places
-- Famous last words: "You can't include too much! ;-)"
sysincludedirs {
"/usr/X11R6/include/X11",
"/usr/X11R6/include",
"/usr/local/include/X11",
"/usr/local/include",
"/usr/include/X11"
}
libdirs { "/usr/X11R6/lib" }
end
end
-- create a project and set the attributes that are common to all projects.
function project_create(project_name, target_type)
project(project_name)
language "C++"
kind(target_type)
filter "action:vs2013"
toolset "v120_xp"
filter "action:vs2015"
toolset "v140_xp"
filter {}
project_set_target(project_name)
project_set_build_flags()
end
-- OSX creates a .app bundle if the project type of the main application is set to "WindowedApp".
-- We don't want this because this bundle would be broken (it lacks all the resources and external dependencies, Info.plist etc...)
-- Windows opens a console in the background if it's set to ConsoleApp, which is not what we want.
-- I didn't check if this setting matters for linux, but WindowedApp works there.
function get_main_project_target_type()
if _OPTIONS["android"] then
return "SharedLib"
elseif os.istarget("macosx") then
return "ConsoleApp"
else
return "WindowedApp"
end
end
-- source_root: rel_source_dirs and rel_include_dirs are relative to this directory
-- rel_source_dirs: A table of subdirectories. All source files in these directories are added.
-- rel_include_dirs: A table of subdirectories to be included.
-- extra_params: table including zero or more of the following:
-- * no_pch: If specified, no precompiled headers are used for this project.
-- * pch_dir: If specified, this directory will be used for precompiled headers instead of the default
-- /pch//.
-- * extra_files: table of filenames (relative to source_root) to add to project
-- * extra_links: table of library names to add to link step
function project_add_contents(source_root, rel_source_dirs, rel_include_dirs, extra_params)
for i,v in pairs(rel_source_dirs) do
local prefix = source_root..v.."/"
files { prefix.."*.cpp", prefix.."*.h", prefix.."*.inl", prefix.."*.js", prefix.."*.asm", prefix.."*.mm" }
end
-- Put the project-specific PCH directory at the start of the
-- include path, so '#include "precompiled.h"' will look in
-- there first
local pch_dir
if not extra_params["pch_dir"] then
pch_dir = source_root .. "pch/" .. project().name .. "/"
else
pch_dir = extra_params["pch_dir"]
end
includedirs { pch_dir }
-- Precompiled Headers
-- rationale: we need one PCH per static lib, since one global header would
-- increase dependencies. To that end, we can either include them as
-- "projectdir/precompiled.h", or add "source/PCH/projectdir" to the
-- include path and put the PCH there. The latter is better because
-- many projects contain several dirs and it's unclear where there the
-- PCH should be stored. This way is also a bit easier to use in that
-- source files always include "precompiled.h".
-- Notes:
-- * Visual Assist manages to use the project include path and can
-- correctly open these files from the IDE.
-- * precompiled.cpp (needed to "Create" the PCH) also goes in
-- the abovementioned dir.
if (not _OPTIONS["without-pch"] and not extra_params["no_pch"]) then
filter "action:vs*"
pchheader("precompiled.h")
filter "action:xcode*"
pchheader("../"..pch_dir.."precompiled.h")
filter { "action:not vs*", "action:not xcode*" }
pchheader(pch_dir.."precompiled.h")
filter {}
pchsource(pch_dir.."precompiled.cpp")
defines { "USING_PCH" }
files { pch_dir.."precompiled.h", pch_dir.."precompiled.cpp" }
else
flags { "NoPCH" }
end
-- next is source root dir, for absolute (nonrelative) includes
-- (e.g. "lib/precompiled.h")
includedirs { source_root }
for i,v in pairs(rel_include_dirs) do
includedirs { source_root .. v }
end
if extra_params["extra_files"] then
for i,v in pairs(extra_params["extra_files"]) do
-- .rc files are only needed on Windows
if path.getextension(v) ~= ".rc" or os.istarget("windows") then
files { source_root .. v }
end
end
end
if extra_params["extra_links"] then
links { extra_params["extra_links"] }
end
end
-- Add command-line options to set up the manifest dependencies for Windows
-- (See lib/sysdep/os/win/manifest.cpp)
function project_add_manifest()
linkoptions { "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='X86' publicKeyToken='6595b64144ccf1df'\"" }
end
--------------------------------------------------------------------------------
-- engine static libraries
--------------------------------------------------------------------------------
-- the engine is split up into several static libraries. this eases separate
-- distribution of those components, reduces dependencies a bit, and can
-- also speed up builds.
-- more to the point, it is necessary to efficiently support a separate
-- test executable that also includes much of the game code.
-- names of all static libs created. automatically added to the
-- main app project later (see explanation at end of this file)
static_lib_names = {}
static_lib_names_debug = {}
static_lib_names_release = {}
-- set up one of the static libraries into which the main engine code is split.
-- extra_params:
-- no_default_link: If specified, linking won't be done by default.
-- For the rest of extra_params, see project_add_contents().
-- note: rel_source_dirs and rel_include_dirs are relative to global source_root.
function setup_static_lib_project (project_name, rel_source_dirs, extern_libs, extra_params)
local target_type = "StaticLib"
project_create(project_name, target_type)
project_add_contents(source_root, rel_source_dirs, {}, extra_params)
project_add_extern_libs(extern_libs, target_type)
project_add_x11_dirs()
if not extra_params["no_default_link"] then
table.insert(static_lib_names, project_name)
end
if os.istarget("windows") then
rtti "off"
end
end
function setup_third_party_static_lib_project (project_name, rel_source_dirs, extern_libs, extra_params)
setup_static_lib_project(project_name, rel_source_dirs, extern_libs, extra_params)
includedirs { source_root .. "third_party/" .. project_name .. "/include/" }
end
function setup_shared_lib_project (project_name, rel_source_dirs, extern_libs, extra_params)
local target_type = "SharedLib"
project_create(project_name, target_type)
project_add_contents(source_root, rel_source_dirs, {}, extra_params)
project_add_extern_libs(extern_libs, target_type)
project_add_x11_dirs()
if not extra_params["no_default_link"] then
table.insert(static_lib_names, project_name)
end
if os.istarget("windows") then
rtti "off"
links { "delayimp" }
end
end
-- this is where the source tree is chopped up into static libs.
-- can be changed very easily; just copy+paste a new setup_static_lib_project,
-- or remove existing ones. static libs are automagically added to
-- main_exe link step.
function setup_all_libs ()
-- relative to global source_root.
local source_dirs = {}
-- names of external libraries used (see libraries_dir comment)
local extern_libs = {}
source_dirs = {
"network",
}
extern_libs = {
"spidermonkey",
"enet",
"boost", -- dragged in via server->simulation.h->random
}
if not _OPTIONS["without-miniupnpc"] then
table.insert(extern_libs, "miniupnpc")
end
setup_static_lib_project("network", source_dirs, extern_libs, {})
source_dirs = {
"third_party/tinygettext/src",
}
extern_libs = {
"iconv",
"boost",
}
setup_third_party_static_lib_project("tinygettext", source_dirs, extern_libs, { } )
-- it's an external library and we don't want to modify its source to fix warnings, so we just disable them to avoid noise in the compile output
filter "action:vs*"
buildoptions {
"/wd4127",
"/wd4309",
"/wd4800",
"/wd4100",
"/wd4996",
"/wd4099",
"/wd4503"
}
filter {}
if not _OPTIONS["without-lobby"] then
source_dirs = {
"lobby",
"lobby/scripting",
"i18n",
"third_party/encryption"
}
extern_libs = {
"spidermonkey",
"boost",
"enet",
"gloox",
"icu",
"iconv",
"libsodium",
"tinygettext"
}
setup_static_lib_project("lobby", source_dirs, extern_libs, {})
if _OPTIONS["use-shared-glooxwrapper"] and not _OPTIONS["build-shared-glooxwrapper"] then
table.insert(static_lib_names_debug, "glooxwrapper_dbg")
table.insert(static_lib_names_release, "glooxwrapper")
else
source_dirs = {
"lobby/glooxwrapper",
}
extern_libs = {
"boost",
"gloox",
}
if _OPTIONS["build-shared-glooxwrapper"] then
setup_shared_lib_project("glooxwrapper", source_dirs, extern_libs, {})
else
setup_static_lib_project("glooxwrapper", source_dirs, extern_libs, {})
end
end
else
source_dirs = {
"lobby/scripting",
"third_party/encryption"
}
extern_libs = {
"spidermonkey",
"boost",
"libsodium"
}
setup_static_lib_project("lobby", source_dirs, extern_libs, {})
files { source_root.."lobby/Globals.cpp" }
end
source_dirs = {
"simulation2",
"simulation2/components",
"simulation2/helpers",
"simulation2/scripting",
"simulation2/serialization",
"simulation2/system",
"simulation2/testcomponents",
}
extern_libs = {
"boost",
"opengl",
"spidermonkey",
}
setup_static_lib_project("simulation2", source_dirs, extern_libs, {})
source_dirs = {
"scriptinterface",
"scriptinterface/third_party"
}
extern_libs = {
"boost",
"spidermonkey",
"valgrind",
"sdl",
}
setup_static_lib_project("scriptinterface", source_dirs, extern_libs, {})
source_dirs = {
"ps",
"ps/scripting",
"network/scripting",
"ps/GameSetup",
"ps/XML",
"soundmanager",
"soundmanager/data",
"soundmanager/items",
"soundmanager/scripting",
"maths",
"maths/scripting",
"i18n",
"i18n/scripting",
"third_party/cppformat",
}
extern_libs = {
"spidermonkey",
"sdl", -- key definitions
"libxml2",
"opengl",
"zlib",
"boost",
"enet",
"libcurl",
"tinygettext",
"icu",
"iconv",
+ "libsodium",
}
if not _OPTIONS["without-audio"] then
table.insert(extern_libs, "openal")
table.insert(extern_libs, "vorbis")
end
setup_static_lib_project("engine", source_dirs, extern_libs, {})
source_dirs = {
"graphics",
"graphics/scripting",
"renderer",
"renderer/scripting",
"third_party/mikktspace"
}
extern_libs = {
"opengl",
"sdl", -- key definitions
"spidermonkey", -- for graphics/scripting
"boost"
}
if not _OPTIONS["without-nvtt"] then
table.insert(extern_libs, "nvtt")
end
setup_static_lib_project("graphics", source_dirs, extern_libs, {})
source_dirs = {
"tools/atlas/GameInterface",
"tools/atlas/GameInterface/Handlers"
}
extern_libs = {
"boost",
"sdl", -- key definitions
"opengl",
"spidermonkey"
}
setup_static_lib_project("atlas", source_dirs, extern_libs, {})
source_dirs = {
"gui",
"gui/scripting",
"i18n"
}
extern_libs = {
"spidermonkey",
"sdl", -- key definitions
"opengl",
"boost",
"enet",
"tinygettext",
"icu",
"iconv",
}
if not _OPTIONS["without-audio"] then
table.insert(extern_libs, "openal")
end
setup_static_lib_project("gui", source_dirs, extern_libs, {})
source_dirs = {
"lib",
"lib/adts",
"lib/allocators",
"lib/external_libraries",
"lib/file",
"lib/file/archive",
"lib/file/common",
"lib/file/io",
"lib/file/vfs",
"lib/pch",
"lib/posix",
"lib/res",
"lib/res/graphics",
"lib/sysdep",
"lib/tex"
}
extern_libs = {
"boost",
"sdl",
"openal",
"opengl",
"libpng",
"zlib",
"valgrind",
"cxxtest",
}
-- CPU architecture-specific
if arch == "amd64" then
table.insert(source_dirs, "lib/sysdep/arch/amd64");
table.insert(source_dirs, "lib/sysdep/arch/x86_x64");
elseif arch == "x86" then
table.insert(source_dirs, "lib/sysdep/arch/ia32");
table.insert(source_dirs, "lib/sysdep/arch/x86_x64");
elseif arch == "arm" then
table.insert(source_dirs, "lib/sysdep/arch/arm");
elseif arch == "aarch64" then
table.insert(source_dirs, "lib/sysdep/arch/aarch64");
end
-- OS-specific
sysdep_dirs = {
linux = { "lib/sysdep/os/linux", "lib/sysdep/os/unix" },
-- note: RC file must be added to main_exe project.
-- note: don't add "lib/sysdep/os/win/aken.cpp" because that must be compiled with the DDK.
windows = { "lib/sysdep/os/win", "lib/sysdep/os/win/wposix", "lib/sysdep/os/win/whrt" },
macosx = { "lib/sysdep/os/osx", "lib/sysdep/os/unix" },
bsd = { "lib/sysdep/os/bsd", "lib/sysdep/os/unix", "lib/sysdep/os/unix/x" },
}
for i,v in pairs(sysdep_dirs[os.target()]) do
table.insert(source_dirs, v);
end
if os.istarget("linux") then
if _OPTIONS["android"] then
table.insert(source_dirs, "lib/sysdep/os/android")
else
table.insert(source_dirs, "lib/sysdep/os/unix/x")
end
end
-- On OSX, disable precompiled headers because C++ files and Objective-C++ files are
-- mixed in this project. To fix that, we would need per-file basis configuration which
-- is not yet supported by the gmake action in premake. We should look into using gmake2.
extra_params = {}
if os.istarget("macosx") then
extra_params = { no_pch = 1 }
end
-- runtime-library-specific
if _ACTION == "vs2013" or _ACTION == "vs2015" then
table.insert(source_dirs, "lib/sysdep/rtl/msc");
else
table.insert(source_dirs, "lib/sysdep/rtl/gcc");
end
setup_static_lib_project("lowlevel", source_dirs, extern_libs, extra_params)
-- Third-party libraries that are built as part of the main project,
-- not built externally and then linked
source_dirs = {
"third_party/mongoose",
}
extern_libs = {
}
setup_static_lib_project("mongoose", source_dirs, extern_libs, { no_pch = 1 })
-- CxxTest mock function support
extern_libs = {
"boost",
"cxxtest",
}
-- 'real' implementations, to be linked against the main executable
-- (files are added manually and not with setup_static_lib_project
-- because not all files in the directory are included)
setup_static_lib_project("mocks_real", {}, extern_libs, { no_default_link = 1, no_pch = 1 })
files { "mocks/*.h", source_root.."mocks/*_real.cpp" }
-- 'test' implementations, to be linked against the test executable
setup_static_lib_project("mocks_test", {}, extern_libs, { no_default_link = 1, no_pch = 1 })
files { source_root.."mocks/*.h", source_root.."mocks/*_test.cpp" }
end
--------------------------------------------------------------------------------
-- main EXE
--------------------------------------------------------------------------------
-- used for main EXE as well as test
used_extern_libs = {
"opengl",
"sdl",
"libpng",
"zlib",
"spidermonkey",
"libxml2",
"boost",
"cxxtest",
"comsuppw",
"enet",
"libcurl",
"tinygettext",
"icu",
"iconv",
"libsodium",
"valgrind",
}
if not os.istarget("windows") and not _OPTIONS["android"] and not os.istarget("macosx") then
-- X11 should only be linked on *nix
table.insert(used_extern_libs, "x11")
table.insert(used_extern_libs, "xcursor")
end
if not _OPTIONS["without-audio"] then
table.insert(used_extern_libs, "openal")
table.insert(used_extern_libs, "vorbis")
end
if not _OPTIONS["without-nvtt"] then
table.insert(used_extern_libs, "nvtt")
end
if not _OPTIONS["without-lobby"] then
table.insert(used_extern_libs, "gloox")
end
if not _OPTIONS["without-miniupnpc"] then
table.insert(used_extern_libs, "miniupnpc")
end
-- Bundles static libs together with main.cpp and builds game executable.
function setup_main_exe ()
local target_type = get_main_project_target_type()
project_create("pyrogenesis", target_type)
filter "system:not macosx"
linkgroups 'On'
filter {}
links { "mocks_real" }
local extra_params = {
extra_files = { "main.cpp" },
no_pch = 1
}
project_add_contents(source_root, {}, {}, extra_params)
project_add_extern_libs(used_extern_libs, target_type)
project_add_x11_dirs()
dependson { "Collada" }
-- Platform Specifics
if os.istarget("windows") then
files { source_root.."lib/sysdep/os/win/icon.rc" }
-- from "lowlevel" static lib; must be added here to be linked in
files { source_root.."lib/sysdep/os/win/error_dialog.rc" }
rtti "off"
linkoptions {
-- wraps main thread in a __try block(see wseh.cpp). replace with mainCRTStartup if that's undesired.
"/ENTRY:wseh_EntryPoint",
-- see wstartup.h
"/INCLUDE:_wstartup_InitAndRegisterShutdown",
-- allow manual unload of delay-loaded DLLs
"/DELAY:UNLOAD",
}
-- allow the executable to use more than 2GB of RAM.
-- this should not be enabled during development, so that memory issues are easily spotted.
if _OPTIONS["large-address-aware"] then
linkoptions { "/LARGEADDRESSAWARE" }
end
-- see manifest.cpp
project_add_manifest()
elseif os.istarget("linux") or os.istarget("bsd") then
if not _OPTIONS["android"] and not (os.getversion().description == "OpenBSD") then
links { "rt" }
end
if _OPTIONS["android"] then
-- NDK's STANDALONE-TOOLCHAIN.html says this is required
linkoptions { "-Wl,--fix-cortex-a8" }
links { "log" }
end
if os.istarget("linux") or os.getversion().description == "GNU/kFreeBSD" then
links {
-- Dynamic libraries (needed for linking for gold)
"dl",
}
elseif os.istarget("bsd") then
links {
-- Needed for backtrace* on BSDs
"execinfo",
}
end
-- Threading support
buildoptions { "-pthread" }
if not _OPTIONS["android"] then
linkoptions { "-pthread" }
end
-- For debug_resolve_symbol
filter "Debug"
linkoptions { "-rdynamic" }
filter { }
elseif os.istarget("macosx") then
links { "pthread" }
links { "ApplicationServices.framework", "Cocoa.framework", "CoreFoundation.framework" }
end
end
--------------------------------------------------------------------------------
-- atlas
--------------------------------------------------------------------------------
-- setup a typical Atlas component project
-- extra_params, rel_source_dirs and rel_include_dirs: as in project_add_contents;
function setup_atlas_project(project_name, target_type, rel_source_dirs, rel_include_dirs, extern_libs, extra_params)
local source_root = rootdir.."/source/tools/atlas/" .. project_name .. "/"
project_create(project_name, target_type)
-- if not specified, the default for atlas pch files is in the project root.
if not extra_params["pch_dir"] then
extra_params["pch_dir"] = source_root
end
project_add_contents(source_root, rel_source_dirs, rel_include_dirs, extra_params)
project_add_extern_libs(extern_libs, target_type)
project_add_x11_dirs()
-- Platform Specifics
if os.istarget("windows") then
-- Link to required libraries
links { "winmm", "comctl32", "rpcrt4", "delayimp", "ws2_32" }
elseif os.istarget("linux") or os.istarget("bsd") then
buildoptions { "-rdynamic", "-fPIC" }
linkoptions { "-fPIC", "-rdynamic" }
-- warnings triggered by wxWidgets
buildoptions { "-Wno-unused-local-typedefs" }
elseif os.istarget("macosx") then
-- install_name settings aren't really supported yet by premake, but there are plans for the future.
-- we currently use this hack to work around some bugs with wrong install_names.
if target_type == "SharedLib" then
if _OPTIONS["macosx-bundle"] then
-- If we're building a bundle, it will be in ../Frameworks
filter "Debug"
linkoptions { "-install_name @executable_path/../Frameworks/lib"..project_name.."_dbg.dylib" }
filter "Release"
linkoptions { "-install_name @executable_path/../Frameworks/lib"..project_name..".dylib" }
filter { }
else
filter "Debug"
linkoptions { "-install_name @executable_path/lib"..project_name.."_dbg.dylib" }
filter "Release"
linkoptions { "-install_name @executable_path/lib"..project_name..".dylib" }
filter { }
end
end
end
end
-- build all Atlas component projects
function setup_atlas_projects()
setup_atlas_project("AtlasObject", "StaticLib",
{ -- src
".",
"../../../third_party/jsonspirit"
},{ -- include
"../../../third_party/jsonspirit"
},{ -- extern_libs
"boost",
"iconv",
"libxml2",
"wxwidgets"
},{ -- extra_params
no_pch = 1
})
atlas_src = {
"ActorEditor",
"CustomControls/Buttons",
"CustomControls/Canvas",
"CustomControls/ColorDialog",
"CustomControls/DraggableListCtrl",
"CustomControls/EditableListCtrl",
"CustomControls/FileHistory",
"CustomControls/HighResTimer",
"CustomControls/MapDialog",
"CustomControls/SnapSplitterWindow",
"CustomControls/VirtualDirTreeCtrl",
"CustomControls/Windows",
"General",
"General/VideoRecorder",
"Misc",
"ScenarioEditor",
"ScenarioEditor/Sections/Common",
"ScenarioEditor/Sections/Cinema",
"ScenarioEditor/Sections/Environment",
"ScenarioEditor/Sections/Map",
"ScenarioEditor/Sections/Object",
"ScenarioEditor/Sections/Player",
"ScenarioEditor/Sections/Terrain",
"ScenarioEditor/Tools",
"ScenarioEditor/Tools/Common",
}
atlas_extra_links = {
"AtlasObject"
}
atlas_extern_libs = {
"boost",
"comsuppw",
"iconv",
"libxml2",
"sdl", -- key definitions
"wxwidgets",
"zlib",
}
if not os.istarget("windows") and not os.istarget("macosx") then
-- X11 should only be linked on *nix
table.insert(atlas_extern_libs, "x11")
end
setup_atlas_project("AtlasUI", "SharedLib", atlas_src,
{ -- include
"..",
"CustomControls",
"Misc"
},
atlas_extern_libs,
{ -- extra_params
pch_dir = rootdir.."/source/tools/atlas/AtlasUI/Misc/",
no_pch = false,
extra_links = atlas_extra_links,
extra_files = { "Misc/atlas.rc" }
})
end
-- Atlas 'frontend' tool-launching projects
function setup_atlas_frontend_project (project_name)
local target_type = get_main_project_target_type()
project_create(project_name, target_type)
project_add_x11_dirs()
local source_root = rootdir.."/source/tools/atlas/AtlasFrontends/"
files { source_root..project_name..".cpp" }
if os.istarget("windows") then
files { source_root..project_name..".rc" }
end
includedirs { source_root .. ".." }
-- Platform Specifics
if os.istarget("windows") then
-- see manifest.cpp
project_add_manifest()
else -- Non-Windows, = Unix
links { "AtlasObject" }
end
links { "AtlasUI" }
end
function setup_atlas_frontends()
setup_atlas_frontend_project("ActorEditor")
end
--------------------------------------------------------------------------------
-- collada
--------------------------------------------------------------------------------
function setup_collada_project(project_name, target_type, rel_source_dirs, rel_include_dirs, extern_libs, extra_params)
project_create(project_name, target_type)
local source_root = source_root.."collada/"
extra_params["pch_dir"] = source_root
project_add_contents(source_root, rel_source_dirs, rel_include_dirs, extra_params)
project_add_extern_libs(extern_libs, target_type)
project_add_x11_dirs()
-- Platform Specifics
if os.istarget("windows") then
characterset "MBCS"
elseif os.istarget("linux") then
defines { "LINUX" }
links {
"dl",
}
-- FCollada is not aliasing-safe, so disallow dangerous optimisations
-- (TODO: It'd be nice to fix FCollada, but that looks hard)
buildoptions { "-fno-strict-aliasing" }
buildoptions { "-rdynamic" }
linkoptions { "-rdynamic" }
elseif os.istarget("bsd") then
if os.getversion().description == "OpenBSD" then
links { "c", }
end
if os.getversion().description == "GNU/kFreeBSD" then
links {
"dl",
}
end
buildoptions { "-fno-strict-aliasing" }
buildoptions { "-rdynamic" }
linkoptions { "-rdynamic" }
elseif os.istarget("macosx") then
-- define MACOS-something?
-- install_name settings aren't really supported yet by premake, but there are plans for the future.
-- we currently use this hack to work around some bugs with wrong install_names.
if target_type == "SharedLib" then
if _OPTIONS["macosx-bundle"] then
-- If we're building a bundle, it will be in ../Frameworks
linkoptions { "-install_name @executable_path/../Frameworks/lib"..project_name..".dylib" }
else
linkoptions { "-install_name @executable_path/lib"..project_name..".dylib" }
end
end
buildoptions { "-fno-strict-aliasing" }
-- On OSX, fcollada uses a few utility functions from coreservices
links { "CoreServices.framework" }
end
end
-- build all Collada component projects
function setup_collada_projects()
setup_collada_project("Collada", "SharedLib",
{ -- src
"."
},{ -- include
},{ -- extern_libs
"fcollada",
"iconv",
"libxml2"
},{ -- extra_params
})
end
--------------------------------------------------------------------------------
-- tests
--------------------------------------------------------------------------------
function setup_tests()
local cxxtest = require "cxxtest"
if os.istarget("windows") then
cxxtest.setpath(rootdir.."/build/bin/cxxtestgen.exe")
else
cxxtest.setpath(rootdir.."/libraries/source/cxxtest-4.4/bin/cxxtestgen")
end
local runner = "ErrorPrinter"
if _OPTIONS["jenkins-tests"] then
runner = "XmlPrinter"
end
local includefiles = {
-- Precompiled headers - the header is added to all generated .cpp files
-- note that the header isn't actually precompiled here, only #included
-- so that the build stage can use it as a precompiled header.
"precompiled.h",
-- This is required to build against SDL 2.0.4 on Windows.
"lib/external_libraries/libsdl.h",
}
cxxtest.init(source_root, true, runner, includefiles)
local target_type = get_main_project_target_type()
project_create("test", target_type)
-- Find header files in 'test' subdirectories
local all_files = os.matchfiles(source_root .. "**/tests/*.h")
local test_files = {}
for i,v in pairs(all_files) do
-- Don't include sysdep tests on the wrong sys
-- Don't include Atlas tests unless Atlas is being built
if not (string.find(v, "/sysdep/os/win/") and not os.istarget("windows")) and
not (string.find(v, "/tools/atlas/") and not _OPTIONS["atlas"]) and
not (string.find(v, "/sysdep/arch/x86_x64/") and ((arch ~= "amd64") or (arch ~= "x86")))
then
table.insert(test_files, v)
end
end
cxxtest.configure_project(test_files)
filter "system:not macosx"
linkgroups 'On'
filter {}
links { static_lib_names }
filter "Debug"
links { static_lib_names_debug }
filter "Release"
links { static_lib_names_release }
filter { }
links { "mocks_test" }
if _OPTIONS["atlas"] then
links { "AtlasObject" }
project_add_extern_libs({"wxwidgets"}, target_type)
end
extra_params = {
extra_files = { "test_setup.cpp" },
}
project_add_contents(source_root, {}, {}, extra_params)
project_add_extern_libs(used_extern_libs, target_type)
project_add_x11_dirs()
dependson { "Collada" }
-- TODO: should fix the duplication between this OS-specific linking
-- code, and the similar version in setup_main_exe
if os.istarget("windows") then
-- from "lowlevel" static lib; must be added here to be linked in
files { source_root.."lib/sysdep/os/win/error_dialog.rc" }
rtti "off"
-- see wstartup.h
linkoptions { "/INCLUDE:_wstartup_InitAndRegisterShutdown" }
-- Enables console for the TEST project on Windows
linkoptions { "/SUBSYSTEM:CONSOLE" }
project_add_manifest()
elseif os.istarget("linux") or os.istarget("bsd") then
if not _OPTIONS["android"] and not (os.getversion().description == "OpenBSD") then
links { "rt" }
end
if _OPTIONS["android"] then
-- NDK's STANDALONE-TOOLCHAIN.html says this is required
linkoptions { "-Wl,--fix-cortex-a8" }
end
if os.istarget("linux") or os.getversion().description == "GNU/kFreeBSD" then
links {
-- Dynamic libraries (needed for linking for gold)
"dl",
}
elseif os.istarget("bsd") then
links {
-- Needed for backtrace* on BSDs
"execinfo",
}
end
-- Threading support
buildoptions { "-pthread" }
if not _OPTIONS["android"] then
linkoptions { "-pthread" }
end
-- For debug_resolve_symbol
filter "Debug"
linkoptions { "-rdynamic" }
filter { }
includedirs { source_root .. "pch/test/" }
end
end
-- must come first, so that VC sets it as the default project and therefore
-- allows running via F5 without the "where is the EXE" dialog.
setup_main_exe()
setup_all_libs()
-- add the static libs to the main EXE project. only now (after
-- setup_all_libs has run) are the lib names known. cannot move
-- setup_main_exe to run after setup_all_libs (see comment above).
-- we also don't want to hardcode the names - that would require more
-- work when changing the static lib breakdown.
project("pyrogenesis") -- Set the main project active
links { static_lib_names }
filter "Debug"
links { static_lib_names_debug }
filter "Release"
links { static_lib_names_release }
filter { }
if _OPTIONS["atlas"] then
setup_atlas_projects()
setup_atlas_frontends()
end
setup_collada_projects()
if not _OPTIONS["without-tests"] then
setup_tests()
end
Index: ps/trunk/source/main.cpp
===================================================================
--- ps/trunk/source/main.cpp (revision 21758)
+++ ps/trunk/source/main.cpp (revision 21759)
@@ -1,678 +1,678 @@
/* Copyright (C) 2018 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 .
*/
/*
This module drives the game when running without Atlas (our integrated
map editor). It receives input and OS messages via SDL and feeds them
into the input dispatcher, where they are passed on to the game GUI and
simulation.
It also contains main(), which either runs the above controller or
that of Atlas depending on commandline parameters.
*/
// not for any PCH effort, but instead for the (common) definitions
// included there.
#define MINIMAL_PCH 2
#include "lib/precompiled.h"
#include
#include "lib/debug.h"
#include "lib/status.h"
#include "lib/secure_crt.h"
#include "lib/frequency_filter.h"
#include "lib/input.h"
#include "lib/ogl.h"
#include "lib/timer.h"
#include "lib/external_libraries/libsdl.h"
#include "ps/ArchiveBuilder.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/Filesystem.h"
#include "ps/Game.h"
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "ps/Loader.h"
#include "ps/ModInstaller.h"
#include "ps/Profile.h"
#include "ps/Profiler2.h"
#include "ps/Pyrogenesis.h"
#include "ps/Replay.h"
#include "ps/TouchInput.h"
#include "ps/UserReport.h"
#include "ps/Util.h"
#include "ps/VideoMode.h"
#include "ps/World.h"
#include "ps/GameSetup/GameSetup.h"
#include "ps/GameSetup/Atlas.h"
#include "ps/GameSetup/Config.h"
#include "ps/GameSetup/CmdLineArgs.h"
#include "ps/GameSetup/Paths.h"
#include "ps/XML/Xeromyces.h"
#include "network/NetClient.h"
#include "network/NetServer.h"
#include "network/NetSession.h"
#include "lobby/IXmppClient.h"
#include "graphics/Camera.h"
#include "graphics/GameView.h"
#include "graphics/TextureManager.h"
#include "gui/GUIManager.h"
#include "renderer/Renderer.h"
#include "scriptinterface/ScriptEngine.h"
#include "simulation2/Simulation2.h"
#include "simulation2/system/TurnManager.h"
#if OS_UNIX
#include // geteuid
#endif // OS_UNIX
#if MSC_VERSION
#include
#define getpid _getpid // Use the non-deprecated function name
#endif
extern bool g_GameRestarted;
extern CStrW g_UniqueLogPostfix;
void kill_mainloop();
// to avoid redundant and/or recursive resizing, we save the new
// size after VIDEORESIZE messages and only update the video mode
// once per frame.
// these values are the latest resize message, and reset to 0 once we've
// updated the video mode
static int g_ResizedW;
static int g_ResizedH;
static std::chrono::high_resolution_clock::time_point lastFrameTime;
// main app message handler
static InReaction MainInputHandler(const SDL_Event_* ev)
{
switch(ev->ev.type)
{
case SDL_WINDOWEVENT:
switch(ev->ev.window.event)
{
case SDL_WINDOWEVENT_ENTER:
RenderCursor(true);
break;
case SDL_WINDOWEVENT_LEAVE:
RenderCursor(false);
break;
case SDL_WINDOWEVENT_RESIZED:
g_ResizedW = ev->ev.window.data1;
g_ResizedH = ev->ev.window.data2;
break;
case SDL_WINDOWEVENT_MOVED:
g_VideoMode.UpdatePosition(ev->ev.window.data1, ev->ev.window.data2);
}
break;
case SDL_QUIT:
kill_mainloop();
break;
case SDL_HOTKEYDOWN:
std::string hotkey = static_cast(ev->ev.user.data1);
if (hotkey == "exit")
{
kill_mainloop();
return IN_HANDLED;
}
else if (hotkey == "screenshot")
{
WriteScreenshot(L".png");
return IN_HANDLED;
}
else if (hotkey == "bigscreenshot")
{
WriteBigScreenshot(L".bmp", 10);
return IN_HANDLED;
}
else if (hotkey == "togglefullscreen")
{
g_VideoMode.ToggleFullscreen();
return IN_HANDLED;
}
else if (hotkey == "profile2.toggle")
{
g_Profiler2.Toggle();
return IN_HANDLED;
}
break;
}
return IN_PASS;
}
// dispatch all pending events to the various receivers.
static void PumpEvents()
{
JSContext* cx = g_GUI->GetScriptInterface()->GetContext();
JSAutoRequest rq(cx);
PROFILE3("dispatch events");
SDL_Event_ ev;
while (in_poll_event(&ev))
{
PROFILE2("event");
if (g_GUI)
{
JS::RootedValue tmpVal(cx);
ScriptInterface::ToJSVal(cx, &tmpVal, ev);
std::string data = g_GUI->GetScriptInterface()->StringifyJSON(&tmpVal);
PROFILE2_ATTR("%s", data.c_str());
}
in_dispatch_event(&ev);
}
g_TouchInput.Frame();
}
/**
* Optionally throttle the render frequency in order to
* prevent 100% workload of the currently used CPU core.
*/
inline static void LimitFPS()
{
if (g_VSync)
return;
double fpsLimit = 0.0;
CFG_GET_VAL(g_Game && g_Game->IsGameStarted() ? "adaptivefps.session" : "adaptivefps.menu", fpsLimit);
// Keep in sync with options.json
if (fpsLimit < 20.0 || fpsLimit >= 100.0)
return;
double wait = 1000.0 / fpsLimit -
std::chrono::duration_cast(
std::chrono::high_resolution_clock::now() - lastFrameTime).count() / 1000.0;
if (wait > 0.0)
SDL_Delay(wait);
lastFrameTime = std::chrono::high_resolution_clock::now();
}
static int ProgressiveLoad()
{
PROFILE3("progressive load");
wchar_t description[100];
int progress_percent;
try
{
Status ret = LDR_ProgressiveLoad(10e-3, description, ARRAY_SIZE(description), &progress_percent);
switch(ret)
{
// no load active => no-op (skip code below)
case INFO::OK:
return 0;
// current task didn't complete. we only care about this insofar as the
// load process is therefore not yet finished.
case ERR::TIMED_OUT:
break;
// just finished loading
case INFO::ALL_COMPLETE:
g_Game->ReallyStartGame();
wcscpy_s(description, ARRAY_SIZE(description), L"Game is starting..");
// LDR_ProgressiveLoad returns L""; set to valid text to
// avoid problems in converting to JSString
break;
// error!
default:
WARN_RETURN_STATUS_IF_ERR(ret);
// can't do this above due to legit ERR::TIMED_OUT
break;
}
}
catch (PSERROR_Game_World_MapLoadFailed& e)
{
// Map loading failed
// Call script function to do the actual work
// (delete game data, switch GUI page, show error, etc.)
CancelLoad(CStr(e.what()).FromUTF8());
}
GUI_DisplayLoadProgress(progress_percent, description);
return 0;
}
static void RendererIncrementalLoad()
{
PROFILE3("renderer incremental load");
const double maxTime = 0.1f;
double startTime = timer_Time();
bool more;
do {
more = g_Renderer.GetTextureManager().MakeProgress();
}
while (more && timer_Time() - startTime < maxTime);
}
static bool quit = false; // break out of main loop
static void Frame()
{
g_Profiler2.RecordFrameStart();
PROFILE2("frame");
g_Profiler2.IncrementFrameNumber();
PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
ogl_WarnIfError();
// get elapsed time
const double time = timer_Time();
g_frequencyFilter->Update(time);
// .. old method - "exact" but contains jumps
#if 0
static double last_time;
const double time = timer_Time();
const float TimeSinceLastFrame = (float)(time-last_time);
last_time = time;
ONCE(return); // first call: set last_time and return
// .. new method - filtered and more smooth, but errors may accumulate
#else
const float realTimeSinceLastFrame = 1.0 / g_frequencyFilter->SmoothedFrequency();
#endif
ENSURE(realTimeSinceLastFrame > 0.0f);
// Decide if update is necessary
bool need_update = true;
// If we are not running a multiplayer game, disable updates when the game is
// minimized or out of focus and relinquish the CPU a bit, in order to make
// debugging easier.
if (g_PauseOnFocusLoss && !g_NetClient && !g_app_has_focus)
{
PROFILE3("non-focus delay");
need_update = false;
// don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored
SDL_Delay(10);
}
// this scans for changed files/directories and reloads them, thus
// allowing hotloading (changes are immediately assimilated in-game).
ReloadChangedFiles();
ProgressiveLoad();
RendererIncrementalLoad();
PumpEvents();
// if the user quit by closing the window, the GL context will be broken and
// may crash when we call Render() on some drivers, so leave this loop
// before rendering
if (quit)
return;
// respond to pumped resize events
if (g_ResizedW || g_ResizedH)
{
g_VideoMode.ResizeWindow(g_ResizedW, g_ResizedH);
g_ResizedW = g_ResizedH = 0;
}
if (g_NetClient)
g_NetClient->Poll();
ogl_WarnIfError();
g_GUI->TickObjects();
ogl_WarnIfError();
if (g_Game && g_Game->IsGameStarted() && need_update)
{
g_Game->Update(realTimeSinceLastFrame);
g_Game->GetView()->Update(float(realTimeSinceLastFrame));
}
// Immediately flush any messages produced by simulation code
if (g_NetClient)
g_NetClient->Flush();
// Keep us connected to any XMPP servers
if (g_XmppClient)
g_XmppClient->recv();
g_UserReporter.Update();
g_Console->Update(realTimeSinceLastFrame);
ogl_WarnIfError();
// We do not have to render an inactive fullscreen frame, because it can
// lead to errors for some graphic card families.
if (!g_app_minimized && (g_app_has_focus || !g_VideoMode.IsInFullscreen()))
{
Render();
PROFILE3("swap buffers");
SDL_GL_SwapWindow(g_VideoMode.GetWindow());
}
ogl_WarnIfError();
g_Profiler.Frame();
g_GameRestarted = false;
LimitFPS();
}
static void NonVisualFrame()
{
g_Profiler2.RecordFrameStart();
PROFILE2("frame");
g_Profiler2.IncrementFrameNumber();
PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
static u32 turn = 0;
debug_printf("Turn %u (%u)...\n", turn++, DEFAULT_TURN_LENGTH_SP);
g_Game->GetSimulation2()->Update(DEFAULT_TURN_LENGTH_SP);
g_Profiler.Frame();
if (g_Game->IsGameFinished())
kill_mainloop();
}
static void MainControllerInit()
{
// add additional input handlers only needed by this controller:
// must be registered after gui_handler. Should mayhap even be last.
in_add_handler(MainInputHandler);
}
static void MainControllerShutdown()
{
in_reset_handlers();
}
// stop the main loop and trigger orderly shutdown. called from several
// places: the event handler (SDL_QUIT and hotkey) and JS exitProgram.
void kill_mainloop()
{
quit = true;
}
static bool restart_in_atlas = false;
// called by game code to indicate main() should restart in Atlas mode
// instead of terminating
void restart_mainloop_in_atlas()
{
quit = true;
restart_in_atlas = true;
}
static bool restart = false;
// trigger an orderly shutdown and restart the game.
void restart_engine()
{
quit = true;
restart = true;
}
extern CmdLineArgs g_args;
// moved into a helper function to ensure args is destroyed before
// exit(), which may result in a memory leak.
static void RunGameOrAtlas(int argc, const char* argv[])
{
CmdLineArgs args(argc, argv);
g_args = args;
if (args.Has("version"))
{
debug_printf("Pyrogenesis %s\n", engine_version);
return;
}
if (args.Has("autostart-nonvisual") && args.Get("autostart").empty())
{
LOGERROR("-autostart-nonvisual cant be used alone. A map with -autostart=\"TYPEDIR/MAPNAME\" is needed.");
return;
}
if (args.Has("unique-logs"))
g_UniqueLogPostfix = L"_" + std::to_wstring(std::time(nullptr)) + L"_" + std::to_wstring(getpid());
const bool isVisualReplay = args.Has("replay-visual");
const bool isNonVisualReplay = args.Has("replay");
const bool isNonVisual = args.Has("autostart-nonvisual");
const OsPath replayFile(
isVisualReplay ? args.Get("replay-visual") :
isNonVisualReplay ? args.Get("replay") : "");
if (isVisualReplay || isNonVisualReplay)
{
if (!FileExists(replayFile))
{
debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile.string8().c_str());
return;
}
if (DirectoryExists(replayFile))
{
debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.string8().c_str());
return;
}
}
std::vector modsToInstall;
for (const CStr& arg : args.GetArgsWithoutName())
{
const OsPath modPath(arg);
if (!CModInstaller::IsDefaultModExtension(modPath.Extension()))
{
debug_printf("Skipping file '%s' which does not have a mod file extension.\n", modPath.string8().c_str());
continue;
}
if (!FileExists(modPath))
{
debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath.string8().c_str());
continue;
}
if (DirectoryExists(modPath))
{
debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath.string8().c_str());
continue;
}
modsToInstall.emplace_back(std::move(modPath));
}
// We need to initialize SpiderMonkey and libxml2 in the main thread before
// any thread uses them. So initialize them here before we might run Atlas.
ScriptEngine scriptEngine;
CXeromyces::Startup();
if (ATLAS_RunIfOnCmdLine(args, false))
{
CXeromyces::Terminate();
return;
}
if (isNonVisualReplay)
{
if (!args.Has("mod"))
{
LOGERROR("At least one mod should be specified! Did you mean to add the argument '-mod=public'?");
CXeromyces::Terminate();
return;
}
Paths paths(args);
g_VFS = CreateVfs();
g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE);
MountMods(paths, GetMods(args, INIT_MODS));
{
CReplayPlayer replay;
replay.Load(replayFile);
replay.Replay(
args.Has("serializationtest"),
args.Has("rejointest") ? args.Get("rejointest").ToInt() : -1,
args.Has("ooslog"));
}
g_VFS.reset();
CXeromyces::Terminate();
return;
}
// run in archive-building mode if requested
if (args.Has("archivebuild"))
{
Paths paths(args);
OsPath mod(args.Get("archivebuild"));
OsPath zip;
if (args.Has("archivebuild-output"))
zip = args.Get("archivebuild-output");
else
zip = mod.Filename().ChangeExtension(L".zip");
CArchiveBuilder builder(mod, paths.Cache());
// Add mods provided on the command line
// NOTE: We do not handle mods in the user mod path here
std::vector mods = args.GetMultiple("mod");
for (size_t i = 0; i < mods.size(); ++i)
builder.AddBaseMod(paths.RData()/"mods"/mods[i]);
builder.Build(zip, args.Has("archivebuild-compress"));
CXeromyces::Terminate();
return;
}
const double res = timer_Resolution();
g_frequencyFilter = CreateFrequencyFilter(res, 30.0);
// run the game
int flags = INIT_MODS;
do
{
restart = false;
quit = false;
if (!Init(args, flags))
{
flags &= ~INIT_MODS;
Shutdown(SHUTDOWN_FROM_CONFIG);
continue;
}
std::vector installedMods;
if (!modsToInstall.empty())
{
Paths paths(args);
CModInstaller installer(paths.UserData() / "mods", paths.Cache());
// Install the mods without deleting the pyromod files
for (const OsPath& modPath : modsToInstall)
- installer.Install(modPath, g_ScriptRuntime, false);
+ installer.Install(modPath, g_ScriptRuntime, true);
installedMods = installer.GetInstalledMods();
}
if (isNonVisual)
{
InitNonVisual(args);
while (!quit)
NonVisualFrame();
}
else
{
InitGraphics(args, 0, installedMods);
MainControllerInit();
while (!quit)
Frame();
}
// Do not install mods again in case of restart (typically from the mod selector)
modsToInstall.clear();
Shutdown(0);
MainControllerShutdown();
flags &= ~INIT_MODS;
} while (restart);
if (restart_in_atlas)
ATLAS_RunIfOnCmdLine(args, true);
CXeromyces::Terminate();
}
#if OS_ANDROID
// In Android we compile the engine as a shared library, not an executable,
// so rename main() to a different symbol that the wrapper library can load
#undef main
#define main pyrogenesis_main
extern "C" __attribute__((visibility ("default"))) int main(int argc, char* argv[]);
#endif
extern "C" int main(int argc, char* argv[])
{
#if OS_UNIX
// Don't allow people to run the game with root permissions,
// because bad things can happen, check before we do anything
if (geteuid() == 0)
{
std::cerr << "********************************************************\n"
<< "WARNING: Attempted to run the game with root permission!\n"
<< "This is not allowed because it can alter home directory \n"
<< "permissions and opens your system to vulnerabilities. \n"
<< "(You received this message because you were either \n"
<<" logged in as root or used e.g. the 'sudo' command.) \n"
<< "********************************************************\n\n";
return EXIT_FAILURE;
}
#endif // OS_UNIX
EarlyInit(); // must come at beginning of main
RunGameOrAtlas(argc, const_cast(argv));
// Shut down profiler initialised by EarlyInit
g_Profiler2.Shutdown();
return EXIT_SUCCESS;
}
Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp
===================================================================
--- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 21758)
+++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 21759)
@@ -1,1651 +1,1661 @@
/* Copyright (C) 2018 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 "lib/app_hooks.h"
#include "lib/config2.h"
#include "lib/input.h"
#include "lib/ogl.h"
#include "lib/timer.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/file/common/file_stats.h"
#include "lib/res/h_mgr.h"
#include "lib/res/graphics/cursor.h"
#include "lib/sysdep/cursor.h"
#include "graphics/CinemaManager.h"
#include "graphics/FontMetrics.h"
#include "graphics/GameView.h"
#include "graphics/LightEnv.h"
#include "graphics/MapReader.h"
#include "graphics/MaterialManager.h"
#include "graphics/TerrainTextureManager.h"
#include "gui/GUI.h"
#include "gui/GUIManager.h"
#include "gui/scripting/ScriptFunctions.h"
#include "i18n/L10n.h"
#include "maths/MathUtil.h"
#include "network/NetServer.h"
#include "network/NetClient.h"
#include "network/NetMessage.h"
#include "network/NetMessages.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/Filesystem.h"
#include "ps/Game.h"
#include "ps/GameSetup/Atlas.h"
#include "ps/GameSetup/GameSetup.h"
#include "ps/GameSetup/Paths.h"
#include "ps/GameSetup/Config.h"
#include "ps/GameSetup/CmdLineArgs.h"
#include "ps/GameSetup/HWDetect.h"
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "ps/Joystick.h"
#include "ps/Loader.h"
#include "ps/Mod.h"
+#include "ps/ModIo.h"
#include "ps/Profile.h"
#include "ps/ProfileViewer.h"
#include "ps/Profiler2.h"
#include "ps/Pyrogenesis.h" // psSetLogDir
#include "ps/scripting/JSInterface_Console.h"
#include "ps/TouchInput.h"
#include "ps/UserReport.h"
#include "ps/Util.h"
#include "ps/VideoMode.h"
#include "ps/VisualReplay.h"
#include "ps/World.h"
#include "renderer/Renderer.h"
#include "renderer/VertexBufferManager.h"
#include "renderer/ModelRenderer.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptStats.h"
#include "scriptinterface/ScriptConversions.h"
#include "simulation2/Simulation2.h"
#include "lobby/IXmppClient.h"
#include "soundmanager/scripting/JSInterface_Sound.h"
#include "soundmanager/ISoundManager.h"
#include "tools/atlas/GameInterface/GameLoop.h"
#include "tools/atlas/GameInterface/View.h"
#if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets
#define MUST_INIT_X11 1
#include
#else
#define MUST_INIT_X11 0
#endif
extern void restart_engine();
#include
#include
#include
ERROR_GROUP(System);
ERROR_TYPE(System, SDLInitFailed);
ERROR_TYPE(System, VmodeFailed);
ERROR_TYPE(System, RequiredExtensionsMissing);
bool g_DoRenderGui = true;
bool g_DoRenderLogger = true;
bool g_DoRenderCursor = true;
shared_ptr g_ScriptRuntime;
static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code
bool g_InDevelopmentCopy;
bool g_CheckedIfInDevelopmentCopy = false;
static void SetTextureQuality(int quality)
{
int q_flags;
GLint filter;
retry:
// keep this in sync with SANE_TEX_QUALITY_DEFAULT
switch(quality)
{
// worst quality
case 0:
q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP;
filter = GL_NEAREST;
break;
// [perf] add bilinear filtering
case 1:
q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP;
filter = GL_LINEAR;
break;
// [vmem] no longer reduce resolution
case 2:
q_flags = OGL_TEX_HALF_BPP;
filter = GL_LINEAR;
break;
// [vmem] add mipmaps
case 3:
q_flags = OGL_TEX_HALF_BPP;
filter = GL_NEAREST_MIPMAP_LINEAR;
break;
// [perf] better filtering
case 4:
q_flags = OGL_TEX_HALF_BPP;
filter = GL_LINEAR_MIPMAP_LINEAR;
break;
// [vmem] no longer reduce bpp
case SANE_TEX_QUALITY_DEFAULT:
q_flags = OGL_TEX_FULL_QUALITY;
filter = GL_LINEAR_MIPMAP_LINEAR;
break;
// [perf] add anisotropy
case 6:
// TODO: add anisotropic filtering
q_flags = OGL_TEX_FULL_QUALITY;
filter = GL_LINEAR_MIPMAP_LINEAR;
break;
// invalid
default:
debug_warn(L"SetTextureQuality: invalid quality");
quality = SANE_TEX_QUALITY_DEFAULT;
// careful: recursion doesn't work and we don't want to duplicate
// the "sane" default values.
goto retry;
}
ogl_tex_set_defaults(q_flags, filter);
}
//----------------------------------------------------------------------------
// GUI integration
//----------------------------------------------------------------------------
// display progress / description in loading screen
void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task)
{
g_GUI->GetActiveGUI()->GetScriptInterface()->SetGlobal("g_Progress", percent, true);
g_GUI->GetActiveGUI()->GetScriptInterface()->SetGlobal("g_LoadDescription", pending_task, true);
g_GUI->GetActiveGUI()->SendEventToAll("progress");
}
void Render()
{
PROFILE3("render");
if (g_SoundManager)
g_SoundManager->IdleTask();
ogl_WarnIfError();
g_Profiler2.RecordGPUFrameStart();
ogl_WarnIfError();
// prepare before starting the renderer frame
if (g_Game && g_Game->IsGameStarted())
g_Game->GetView()->BeginFrame();
if (g_Game)
g_Renderer.SetSimulation(g_Game->GetSimulation2());
// start new frame
g_Renderer.BeginFrame();
ogl_WarnIfError();
if (g_Game && g_Game->IsGameStarted())
g_Game->GetView()->Render();
ogl_WarnIfError();
g_Renderer.RenderTextOverlays();
// If we're in Atlas game view, render special tools
if (g_AtlasGameLoop && g_AtlasGameLoop->view)
{
g_AtlasGameLoop->view->DrawCinemaPathTool();
ogl_WarnIfError();
}
if (g_Game && g_Game->IsGameStarted())
g_Game->GetView()->GetCinema()->Render();
ogl_WarnIfError();
if (g_DoRenderGui)
g_GUI->Draw();
ogl_WarnIfError();
// If we're in Atlas game view, render special overlays (e.g. editor bandbox)
if (g_AtlasGameLoop && g_AtlasGameLoop->view)
{
g_AtlasGameLoop->view->DrawOverlays();
ogl_WarnIfError();
}
// Text:
glDisable(GL_DEPTH_TEST);
g_Console->Render();
ogl_WarnIfError();
if (g_DoRenderLogger)
g_Logger->Render();
ogl_WarnIfError();
// Profile information
g_ProfileViewer.RenderProfile();
ogl_WarnIfError();
// Draw the cursor (or set the Windows cursor, on Windows)
if (g_DoRenderCursor)
{
PROFILE3_GPU("cursor");
CStrW cursorName = g_CursorName;
if (cursorName.empty())
{
cursor_draw(g_VFS, NULL, g_mouse_x, g_yres-g_mouse_y, g_GuiScale, false);
}
else
{
bool forceGL = false;
CFG_GET_VAL("nohwcursor", forceGL);
#if CONFIG2_GLES
#warning TODO: implement cursors for GLES
#else
// set up transform for GL cursor
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
CMatrix3D transform;
transform.SetOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f);
glLoadMatrixf(&transform._11);
#endif
#if OS_ANDROID
#warning TODO: cursors for Android
#else
if (cursor_draw(g_VFS, cursorName.c_str(), g_mouse_x, g_yres-g_mouse_y, g_GuiScale, forceGL) < 0)
LOGWARNING("Failed to draw cursor '%s'", utf8_from_wstring(cursorName));
#endif
#if CONFIG2_GLES
#warning TODO: implement cursors for GLES
#else
// restore transform
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
#endif
}
}
glEnable(GL_DEPTH_TEST);
g_Renderer.EndFrame();
PROFILE2_ATTR("draw calls: %d", (int)g_Renderer.GetStats().m_DrawCalls);
PROFILE2_ATTR("terrain tris: %d", (int)g_Renderer.GetStats().m_TerrainTris);
PROFILE2_ATTR("water tris: %d", (int)g_Renderer.GetStats().m_WaterTris);
PROFILE2_ATTR("model tris: %d", (int)g_Renderer.GetStats().m_ModelTris);
PROFILE2_ATTR("overlay tris: %d", (int)g_Renderer.GetStats().m_OverlayTris);
PROFILE2_ATTR("blend splats: %d", (int)g_Renderer.GetStats().m_BlendSplats);
PROFILE2_ATTR("particles: %d", (int)g_Renderer.GetStats().m_Particles);
ogl_WarnIfError();
g_Profiler2.RecordGPUFrameEnd();
ogl_WarnIfError();
}
ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED(flags))
{
// If we're fullscreen, then sometimes (at least on some particular drivers on Linux)
// displaying the error dialog hangs the desktop since the dialog box is behind the
// fullscreen window. So we just force the game to windowed mode before displaying the dialog.
// (But only if we're in the main thread, and not if we're being reentrant.)
if (ThreadUtil::IsMainThread())
{
static bool reentering = false;
if (!reentering)
{
reentering = true;
g_VideoMode.SetFullscreen(false);
reentering = false;
}
}
// We don't actually implement the error display here, so return appropriately
return ERI_NOT_IMPLEMENTED;
}
const std::vector& GetMods(const CmdLineArgs& args, int flags)
{
const bool init_mods = (flags & INIT_MODS) == INIT_MODS;
const bool add_user = !InDevelopmentCopy() && !args.Has("noUserMod");
const bool add_public = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC;
if (!init_mods)
{
// Add the user mod if it should be present
if (add_user && (g_modsLoaded.empty() || g_modsLoaded.back() != "user"))
g_modsLoaded.push_back("user");
return g_modsLoaded;
}
g_modsLoaded = args.GetMultiple("mod");
if (add_public)
g_modsLoaded.insert(g_modsLoaded.begin(), "public");
g_modsLoaded.insert(g_modsLoaded.begin(), "mod");
// Add the user mod if not explicitly disabled or we have a dev copy so
// that saved files end up in version control and not in the user mod.
if (add_user)
g_modsLoaded.push_back("user");
return g_modsLoaded;
}
void MountMods(const Paths& paths, const std::vector& mods)
{
OsPath modPath = paths.RData()/"mods";
OsPath modUserPath = paths.UserData()/"mods";
for (size_t i = 0; i < mods.size(); ++i)
{
size_t priority = (i+1)*2; // mods are higher priority than regular mountings, which default to priority 0
size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE|VFS_MOUNT_REPLACEABLE;
size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST;
OsPath modName(mods[i]);
if (InDevelopmentCopy())
{
// We are running a dev copy, so only mount mods in the user mod path
// if the mod does not exist in the data path.
if (DirectoryExists(modPath / modName/""))
g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority);
else
g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority);
}
else
{
g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority);
// Ensure that user modified files are loaded, if they are present
g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority+1);
}
}
}
static void InitVfs(const CmdLineArgs& args, int flags)
{
TIMER(L"InitVfs");
const bool setup_error = (flags & INIT_HAVE_DISPLAY_ERROR) == 0;
const Paths paths(args);
OsPath logs(paths.Logs());
CreateDirectories(logs, 0700);
psSetLogDir(logs);
// desired location for crashlog is now known. update AppHooks ASAP
// (particularly before the following error-prone operations):
AppHooks hooks = {0};
hooks.bundle_logs = psBundleLogs;
hooks.get_log_dir = psLogDir;
if (setup_error)
hooks.display_error = psDisplayError;
app_hooks_update(&hooks);
g_VFS = CreateVfs();
const OsPath readonlyConfig = paths.RData()/"config"/"";
g_VFS->Mount(L"config/", readonlyConfig);
// Engine localization files.
g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/"");
MountMods(paths, GetMods(args, flags));
// We mount these dirs last as otherwise writing could result in files being placed in a mod's dir.
g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"");
g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH);
// Mounting with highest priority, so that a mod supplied user.cfg is harmless
g_VFS->Mount(L"config/", readonlyConfig, 0, (size_t)-1);
if(readonlyConfig != paths.Config())
g_VFS->Mount(L"config/", paths.Config(), 0, (size_t)-1);
g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); // (adding XMBs to archive speeds up subsequent reads)
// note: don't bother with g_VFS->TextRepresentation - directories
// haven't yet been populated and are empty.
}
static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcScriptInterface, JS::HandleValue initData)
{
{
// console
TIMER(L"ps_console");
g_Console->UpdateScreenSize(g_xres, g_yres);
// Calculate and store the line spacing
CFontMetrics font(CStrIntern(CONSOLE_FONT));
g_Console->m_iFontHeight = font.GetLineSpacing();
g_Console->m_iFontWidth = font.GetCharacterWidth(L'C');
g_Console->m_charsPerPage = (size_t)(g_xres / g_Console->m_iFontWidth);
// Offset by an arbitrary amount, to make it fit more nicely
g_Console->m_iFontOffset = 7;
double blinkRate = 0.5;
CFG_GET_VAL("gui.cursorblinkrate", blinkRate);
g_Console->SetCursorBlinkRate(blinkRate);
}
// hotkeys
{
TIMER(L"ps_lang_hotkeys");
LoadHotkeys();
}
if (!setup_gui)
{
// We do actually need *some* kind of GUI loaded, so use the
// (currently empty) Atlas one
g_GUI->SwitchPage(L"page_atlas.xml", srcScriptInterface, initData);
return;
}
// GUI uses VFS, so this must come after VFS init.
g_GUI->SwitchPage(gui_page, srcScriptInterface, initData);
}
void InitPsAutostart(bool networked, JS::HandleValue attrs)
{
// The GUI has not been initialized yet, so use the simulation scriptinterface for this variable
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue playerAssignments(cx);
scriptInterface.Eval("({})", &playerAssignments);
if (!networked)
{
JS::RootedValue localPlayer(cx);
scriptInterface.Eval("({})", &localPlayer);
scriptInterface.SetProperty(localPlayer, "player", g_Game->GetPlayerID());
scriptInterface.SetProperty(playerAssignments, "local", localPlayer);
}
JS::RootedValue sessionInitData(cx);
scriptInterface.Eval("({})", &sessionInitData);
scriptInterface.SetProperty(sessionInitData, "attribs", attrs);
scriptInterface.SetProperty(sessionInitData, "playerAssignments", playerAssignments);
InitPs(true, L"page_loading.xml", &scriptInterface, sessionInitData);
}
static void InitInput()
{
g_Joystick.Initialise();
// register input handlers
// This stack is constructed so the first added, will be the last
// one called. This is important, because each of the handlers
// has the potential to block events to go further down
// in the chain. I.e. the last one in the list added, is the
// only handler that can block all messages before they are
// processed.
in_add_handler(game_view_handler);
in_add_handler(CProfileViewer::InputThunk);
in_add_handler(conInputHandler);
in_add_handler(HotkeyInputHandler);
// gui_handler needs to be registered after (i.e. called before!) the
// hotkey handler so that input boxes can be typed in without
// setting off hotkeys.
in_add_handler(gui_handler);
in_add_handler(touch_input_handler);
// must be registered after (called before) the GUI which relies on these globals
in_add_handler(GlobalsInputHandler);
}
static void ShutdownPs()
{
SAFE_DELETE(g_GUI);
UnloadHotkeys();
// disable the special Windows cursor, or free textures for OGL cursors
cursor_draw(g_VFS, 0, g_mouse_x, g_yres-g_mouse_y, 1.0, false);
}
static void InitRenderer()
{
TIMER(L"InitRenderer");
if(g_NoGLS3TC)
ogl_tex_override(OGL_TEX_S3TC, OGL_TEX_DISABLE);
if(g_NoGLAutoMipmap)
ogl_tex_override(OGL_TEX_AUTO_MIPMAP_GEN, OGL_TEX_DISABLE);
// create renderer
new CRenderer;
// set renderer options from command line options - NOVBO must be set before opening the renderer
// and init them in the ConfigDB when needed
g_Renderer.SetOptionBool(CRenderer::OPT_NOVBO, g_NoGLVBO);
g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWS, g_Shadows);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "shadows", g_Shadows);
g_Renderer.SetOptionBool(CRenderer::OPT_WATEREFFECTS, g_WaterEffects);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "watereffects", g_WaterEffects);
g_Renderer.SetOptionBool(CRenderer::OPT_WATERFANCYEFFECTS, g_WaterFancyEffects);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterfancyeffects", g_WaterFancyEffects);
g_Renderer.SetOptionBool(CRenderer::OPT_WATERREALDEPTH, g_WaterRealDepth);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterrealdepth", g_WaterRealDepth);
g_Renderer.SetOptionBool(CRenderer::OPT_WATERREFLECTION, g_WaterReflection);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterreflection", g_WaterReflection);
g_Renderer.SetOptionBool(CRenderer::OPT_WATERREFRACTION, g_WaterRefraction);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterrefraction", g_WaterRefraction);
g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWSONWATER, g_WaterShadows);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "watershadows", g_WaterShadows);
g_Renderer.SetRenderPath(CRenderer::GetRenderPathByName(g_RenderPath));
g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWPCF, g_ShadowPCF);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "shadowpcf", g_ShadowPCF);
g_Renderer.SetOptionBool(CRenderer::OPT_PARTICLES, g_Particles);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "particles", g_Particles);
g_Renderer.SetOptionBool(CRenderer::OPT_FOG, g_Fog);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "fog", g_Fog);
g_Renderer.SetOptionBool(CRenderer::OPT_SILHOUETTES, g_Silhouettes);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "silhouettes", g_Silhouettes);
g_Renderer.SetOptionBool(CRenderer::OPT_SHOWSKY, g_ShowSky);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "showsky", g_ShowSky);
g_Renderer.SetOptionBool(CRenderer::OPT_PREFERGLSL, g_PreferGLSL);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "preferglsl", g_PreferGLSL);
g_Renderer.SetOptionBool(CRenderer::OPT_POSTPROC, g_PostProc);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "postproc", g_PostProc);
g_Renderer.SetOptionBool(CRenderer::OPT_SMOOTHLOS, g_SmoothLOS);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "smoothlos", g_SmoothLOS);
// create terrain related stuff
new CTerrainTextureManager;
g_Renderer.Open(g_xres, g_yres);
// Setup lighting environment. Since the Renderer accesses the
// lighting environment through a pointer, this has to be done before
// the first Frame.
g_Renderer.SetLightEnv(&g_LightEnv);
// I haven't seen the camera affecting GUI rendering and such, but the
// viewport has to be updated according to the video mode
SViewPort vp;
vp.m_X = 0;
vp.m_Y = 0;
vp.m_Width = g_xres;
vp.m_Height = g_yres;
g_Renderer.SetViewport(vp);
ColorActivateFastImpl();
ModelRenderer::Init();
}
static void InitSDL()
{
#if OS_LINUX
// In fullscreen mode when SDL is compiled with DGA support, the mouse
// sensitivity often appears to be unusably wrong (typically too low).
// (This seems to be reported almost exclusively on Ubuntu, but can be
// reproduced on Gentoo after explicitly enabling DGA.)
// Disabling the DGA mouse appears to fix that problem, and doesn't
// have any obvious negative effects.
setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0);
#endif
if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_NOPARACHUTE) < 0)
{
LOGERROR("SDL library initialization failed: %s", SDL_GetError());
throw PSERROR_System_SDLInitFailed();
}
atexit(SDL_Quit);
// Text input is active by default, disable it until it is actually needed.
SDL_StopTextInput();
#if OS_MACOSX
// Some Mac mice only have one button, so they can't right-click
// but SDL2 can emulate that with Ctrl+Click
bool macMouse = false;
CFG_GET_VAL("macmouse", macMouse);
SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0");
#endif
}
static void ShutdownSDL()
{
SDL_Quit();
sys_cursor_reset();
}
void EndGame()
{
const bool nonVisual = g_Game && g_Game->IsGraphicsDisabled();
if (g_Game && g_Game->IsGameStarted() && !g_Game->IsVisualReplay() &&
g_AtlasGameLoop && !g_AtlasGameLoop->running && !nonVisual)
VisualReplay::SaveReplayMetadata(g_GUI->GetActiveGUI()->GetScriptInterface().get());
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_NetServer);
SAFE_DELETE(g_Game);
if (!nonVisual)
{
ISoundManager::CloseGame();
g_Renderer.ResetState();
}
}
void Shutdown(int flags)
{
const bool nonVisual = g_Game && g_Game->IsGraphicsDisabled();
if ((flags & SHUTDOWN_FROM_CONFIG))
goto from_config;
EndGame();
SAFE_DELETE(g_XmppClient);
+ SAFE_DELETE(g_ModIo);
+
ShutdownPs();
TIMER_BEGIN(L"shutdown TexMan");
delete &g_TexMan;
TIMER_END(L"shutdown TexMan");
// destroy renderer if it was initialised
if (!nonVisual)
{
TIMER_BEGIN(L"shutdown Renderer");
delete &g_Renderer;
g_VBMan.Shutdown();
TIMER_END(L"shutdown Renderer");
}
g_Profiler2.ShutdownGPU();
// Free cursors before shutting down SDL, as they may depend on SDL.
cursor_shutdown();
TIMER_BEGIN(L"shutdown SDL");
ShutdownSDL();
TIMER_END(L"shutdown SDL");
if (!nonVisual)
g_VideoMode.Shutdown();
TIMER_BEGIN(L"shutdown UserReporter");
g_UserReporter.Deinitialize();
TIMER_END(L"shutdown UserReporter");
+ // Cleanup curl now that g_ModIo and g_UserReporter have been shutdown.
+ curl_global_cleanup();
+
delete &g_L10n;
from_config:
TIMER_BEGIN(L"shutdown ConfigDB");
delete &g_ConfigDB;
TIMER_END(L"shutdown ConfigDB");
SAFE_DELETE(g_Console);
// This is needed to ensure that no callbacks from the JSAPI try to use
// the profiler when it's already destructed
g_ScriptRuntime.reset();
// resource
// first shut down all resource owners, and then the handle manager.
TIMER_BEGIN(L"resource modules");
ISoundManager::SetEnabled(false);
g_VFS.reset();
// this forcibly frees all open handles (thus preventing real leaks),
// and makes further access to h_mgr impossible.
h_mgr_shutdown();
file_stats_dump();
TIMER_END(L"resource modules");
TIMER_BEGIN(L"shutdown misc");
timer_DisplayClientTotals();
CNetHost::Deinitialize();
// should be last, since the above use them
SAFE_DELETE(g_Logger);
delete &g_Profiler;
delete &g_ProfileViewer;
SAFE_DELETE(g_ScriptStatsTable);
TIMER_END(L"shutdown misc");
}
#if OS_UNIX
static void FixLocales()
{
#if OS_MACOSX || OS_BSD
// OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle
// wide characters. Peculiarly the string "UTF-8" seems to be acceptable
// despite not being a real locale, and it's conveniently language-agnostic,
// so use that.
setlocale(LC_CTYPE, "UTF-8");
#endif
// On misconfigured systems with incorrect locale settings, we'll die
// with a C++ exception when some code (e.g. Boost) tries to use locales.
// To avoid death, we'll detect the problem here and warn the user and
// reset to the default C locale.
// For informing the user of the problem, use the list of env vars that
// glibc setlocale looks at. (LC_ALL is checked first, and LANG last.)
const char* const LocaleEnvVars[] = {
"LC_ALL",
"LC_COLLATE",
"LC_CTYPE",
"LC_MONETARY",
"LC_NUMERIC",
"LC_TIME",
"LC_MESSAGES",
"LANG"
};
try
{
// this constructor is similar to setlocale(LC_ALL, ""),
// but instead of returning NULL, it throws runtime_error
// when the first locale env variable found contains an invalid value
std::locale("");
}
catch (std::runtime_error&)
{
LOGWARNING("Invalid locale settings");
for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++)
{
if (char* envval = getenv(LocaleEnvVars[i]))
LOGWARNING(" %s=\"%s\"", LocaleEnvVars[i], envval);
else
LOGWARNING(" %s=\"(unset)\"", LocaleEnvVars[i]);
}
// We should set LC_ALL since it overrides LANG
if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1))
debug_warn(L"Invalid locale settings, and unable to set LC_ALL env variable.");
else
LOGWARNING("Setting LC_ALL env variable to: %s", getenv("LC_ALL"));
}
}
#else
static void FixLocales()
{
// Do nothing on Windows
}
#endif
void EarlyInit()
{
// If you ever want to catch a particular allocation:
//_CrtSetBreakAlloc(232647);
ThreadUtil::SetMainThread();
debug_SetThreadName("main");
// add all debug_printf "tags" that we are interested in:
debug_filter_add("TIMER");
timer_LatchStartTime();
// initialise profiler early so it can profile startup,
// but only after LatchStartTime
g_Profiler2.Initialise();
FixLocales();
// Because we do GL calls from a secondary thread, Xlib needs to
// be told to support multiple threads safely.
// This is needed for Atlas, but we have to call it before any other
// Xlib functions (e.g. the ones used when drawing the main menu
// before launching Atlas)
#if MUST_INIT_X11
int status = XInitThreads();
if (status == 0)
debug_printf("Error enabling thread-safety via XInitThreads\n");
#endif
// Initialise the low-quality rand function
srand(time(NULL)); // NOTE: this rand should *not* be used for simulation!
}
bool Autostart(const CmdLineArgs& args);
/**
* Returns true if the user has intended to start a visual replay from command line.
*/
bool AutostartVisualReplay(const std::string& replayFile);
bool Init(const CmdLineArgs& args, int flags)
{
h_mgr_init();
// Do this as soon as possible, because it chdirs
// and will mess up the error reporting if anything
// crashes before the working directory is set.
InitVfs(args, flags);
// This must come after VFS init, which sets the current directory
// (required for finding our output log files).
g_Logger = new CLogger;
new CProfileViewer;
new CProfileManager; // before any script code
g_ScriptStatsTable = new CScriptStatsTable;
g_ProfileViewer.AddRootTable(g_ScriptStatsTable);
// Set up the console early, so that debugging
// messages can be logged to it. (The console's size
// and fonts are set later in InitPs())
g_Console = new CConsole();
// g_ConfigDB, command line args, globals
CONFIG_Init(args);
// Using a global object for the runtime is a workaround until Simulation and AI use
// their own threads and also their own runtimes.
const int runtimeSize = 384 * 1024 * 1024;
const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024;
g_ScriptRuntime = ScriptInterface::CreateRuntime(shared_ptr(), runtimeSize, heapGrowthBytesGCTrigger);
// Special command-line mode to dump the entity schemas instead of running the game.
// (This must be done after loading VFS etc, but should be done before wasting time
// on anything else.)
if (args.Has("dumpSchema"))
{
CSimulation2 sim(NULL, g_ScriptRuntime, NULL);
sim.LoadDefaultScripts();
std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc);
f << sim.GenerateSchema();
std::cout << "Generated entity.rng\n";
exit(0);
}
CNetHost::Initialize();
#if CONFIG2_AUDIO
if (!args.Has("autostart-nonvisual"))
ISoundManager::CreateSoundManager();
#endif
// Check if there are mods specified on the command line,
// or if we already set the mods (~INIT_MODS),
// else check if there are mods that should be loaded specified
// in the config and load those (by aborting init and restarting
// the engine).
if (!args.Has("mod") && (flags & INIT_MODS) == INIT_MODS)
{
CStr modstring;
CFG_GET_VAL("mod.enabledmods", modstring);
if (!modstring.empty())
{
std::vector mods;
boost::split(mods, modstring, boost::is_any_of(" "), boost::token_compress_on);
std::swap(g_modsLoaded, mods);
// Abort init and restart
restart_engine();
return false;
}
}
new L10n;
// Optionally start profiler HTTP output automatically
// (By default it's only enabled by a hotkey, for security/performance)
bool profilerHTTPEnable = false;
CFG_GET_VAL("profiler2.autoenable", profilerHTTPEnable);
if (profilerHTTPEnable)
g_Profiler2.EnableHTTP();
+ // Initialise everything except Win32 sockets (because our networking
+ // system already inits those)
+ curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32);
+
if (!g_Quickstart)
g_UserReporter.Initialize(); // after config
PROFILE2_EVENT("Init finished");
return true;
}
void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods)
{
const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0;
if(setup_vmode)
{
InitSDL();
if (!g_VideoMode.InitSDL())
throw PSERROR_System_VmodeFailed(); // abort startup
}
RunHardwareDetection();
const int quality = SANE_TEX_QUALITY_DEFAULT; // TODO: set value from config file
SetTextureQuality(quality);
ogl_WarnIfError();
// Optionally start profiler GPU timings automatically
// (By default it's only enabled by a hotkey, for performance/compatibility)
bool profilerGPUEnable = false;
CFG_GET_VAL("profiler2.autoenable", profilerGPUEnable);
if (profilerGPUEnable)
g_Profiler2.EnableGPU();
if(!g_Quickstart)
{
WriteSystemInfo();
// note: no longer vfs_display here. it's dog-slow due to unbuffered
// file output and very rarely needed.
}
if(g_DisableAudio)
ISoundManager::SetEnabled(false);
g_GUI = new CGUIManager();
// (must come after SetVideoMode, since it calls ogl_Init)
if (ogl_HaveExtensions(0, "GL_ARB_vertex_program", "GL_ARB_fragment_program", NULL) != 0 // ARB
&& ogl_HaveExtensions(0, "GL_ARB_vertex_shader", "GL_ARB_fragment_shader", NULL) != 0) // GLSL
{
DEBUG_DISPLAY_ERROR(
L"Your graphics card doesn't appear to be fully compatible with OpenGL shaders."
L" In the future, the game will not support pre-shader graphics cards."
L" You are advised to try installing newer drivers and/or upgrade your graphics card."
L" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734"
);
// TODO: actually quit once fixed function support is dropped
}
const char* missing = ogl_HaveExtensions(0,
"GL_ARB_multitexture",
"GL_EXT_draw_range_elements",
"GL_ARB_texture_env_combine",
"GL_ARB_texture_env_dot3",
NULL);
if(missing)
{
wchar_t buf[500];
swprintf_s(buf, ARRAY_SIZE(buf),
L"The %hs extension doesn't appear to be available on your computer."
L" The game may still work, though - you are welcome to try at your own risk."
L" If not or it doesn't look right, upgrade your graphics card.",
missing
);
DEBUG_DISPLAY_ERROR(buf);
// TODO: i18n
}
if (!ogl_HaveExtension("GL_ARB_texture_env_crossbar"))
{
DEBUG_DISPLAY_ERROR(
L"The GL_ARB_texture_env_crossbar extension doesn't appear to be available on your computer."
L" Shadows are not available and overall graphics quality might suffer."
L" You are advised to try installing newer drivers and/or upgrade your graphics card.");
g_Shadows = false;
}
ogl_WarnIfError();
InitRenderer();
InitInput();
ogl_WarnIfError();
// TODO: Is this the best place for this?
if (VfsDirectoryExists(L"maps/"))
CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng");
try
{
if (!AutostartVisualReplay(args.Get("replay-visual")) && !Autostart(args))
{
const bool setup_gui = ((flags & INIT_NO_GUI) == 0);
// We only want to display the splash screen at startup
shared_ptr scriptInterface = g_GUI->GetScriptInterface();
JSContext* cx = scriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue data(cx);
if (g_GUI)
{
scriptInterface->Eval("({})", &data);
scriptInterface->SetProperty(data, "isStartup", true);
if (!installedMods.empty())
scriptInterface->SetProperty(data, "installedMods", installedMods);
}
InitPs(setup_gui, installedMods.empty() ? L"page_pregame.xml" : L"page_modmod.xml", g_GUI->GetScriptInterface().get(), data);
}
}
catch (PSERROR_Game_World_MapLoadFailed& e)
{
// Map Loading failed
// Start the engine so we have a GUI
InitPs(true, L"page_pregame.xml", NULL, JS::UndefinedHandleValue);
// Call script function to do the actual work
// (delete game data, switch GUI page, show error, etc.)
CancelLoad(CStr(e.what()).FromUTF8());
}
}
void InitNonVisual(const CmdLineArgs& args)
{
// Need some stuff for terrain movement costs:
// (TODO: this ought to be independent of any graphics code)
new CTerrainTextureManager;
g_TexMan.LoadTerrainTextures();
Autostart(args);
}
void RenderGui(bool RenderingState)
{
g_DoRenderGui = RenderingState;
}
void RenderLogger(bool RenderingState)
{
g_DoRenderLogger = RenderingState;
}
void RenderCursor(bool RenderingState)
{
g_DoRenderCursor = RenderingState;
}
/**
* Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON
* data from it.
* The scenario map format is used for scenario and skirmish map types (random
* games do not use a "map" (format) but a small JavaScript program which
* creates a map on the fly). It contains a section to initialize the game
* setup screen.
* @param mapPath Absolute path (from VFS root) to the map file to peek in.
* @return ScriptSettings in JSON format extracted from the map.
*/
CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath)
{
CXeromyces mapFile;
const char *pathToSettings[] =
{
"Scenario", "ScriptSettings", "" // Path to JSON data in map
};
Status loadResult = mapFile.Load(g_VFS, mapPath);
if (INFO::OK != loadResult)
{
LOGERROR("LoadSettingsOfScenarioMap: Unable to load map file '%s'", mapPath.string8());
throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos.");
}
XMBElement mapElement = mapFile.GetRoot();
// Select the ScriptSettings node in the map file...
for (int i = 0; pathToSettings[i][0]; ++i)
{
int childId = mapFile.GetElementID(pathToSettings[i]);
XMBElementList nodes = mapElement.GetChildNodes();
auto it = std::find_if(nodes.begin(), nodes.end(), [&childId](const XMBElement& child) {
return child.GetNodeName() == childId;
});
if (it != nodes.end())
mapElement = *it;
}
// ... they contain a JSON document to initialize the game setup
// screen
return mapElement.GetText();
}
/*
* Command line options for autostart
* (keep synchronized with binaries/system/readme.txt):
*
* -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME;
* TYPEDIR is skirmishes, scenarios, or random
* -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random)
* -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra)
* -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI
* (0: sandbox, 5: very hard)
* -autostart-aiseed=AISEED sets the seed used for the AI random
* generator (default 0, use -1 for random)
* -autostart-player=NUMBER sets the playerID in non-networked games (default 1, use -1 for observer)
* -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV
* (skirmish and random maps only)
* -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2).
* -autostart-ceasefire=NUM sets a ceasefire duration NUM
* (default 0 minutes)
* -autostart-nonvisual disable any graphics and sounds
* -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME
* located in simulation/data/settings/victory_conditions/
* (default conquest)
* -autostart-wonderduration=NUM sets the victory duration NUM for wonder victory condition
* (default 10 minutes)
* -autostart-relicduration=NUM sets the victory duration NUM for relic victory condition
* (default 10 minutes)
* -autostart-reliccount=NUM sets the number of relics for relic victory condition
* (default 2 relics)
*
* Multiplayer:
* -autostart-playername=NAME sets local player NAME (default 'anonymous')
* -autostart-host sets multiplayer host mode
* -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer
* game (default 2)
* -autostart-client=IP sets multiplayer client to join host at
* given IP address
* Random maps only:
* -autostart-size=TILES sets random map size in TILES (default 192)
* -autostart-players=NUMBER sets NUMBER of players on random map
* (default 2)
*
* Examples:
* 1) "Bob" will host a 2 player game on the Arcadia map:
* -autostart="scenarios/Arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob"
* "Alice" joins the match as player 2:
* -autostart="scenarios/Arcadia" -autostart-client=127.0.0.1 -autostart-playername="Alice"
* The players use the developer overlay to control players.
*
* 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot:
* -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra
*
* 3) Observe the PetraBot on a triggerscript map:
* -autostart="random/jebel_barkal" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=1:petra -autostart-ai=2:petra -autostart-player=-1
*/
bool Autostart(const CmdLineArgs& args)
{
CStr autoStartName = args.Get("autostart");
if (autoStartName.empty())
return false;
const bool nonVisual = args.Has("autostart-nonvisual");
g_Game = new CGame(nonVisual, !nonVisual);
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue attrs(cx);
scriptInterface.Eval("({})", &attrs);
JS::RootedValue settings(cx);
scriptInterface.Eval("({})", &settings);
JS::RootedValue playerData(cx);
scriptInterface.Eval("([])", &playerData);
// The directory in front of the actual map name indicates which type
// of map is being loaded. Drawback of this approach is the association
// of map types and folders is hard-coded, but benefits are:
// - No need to pass the map type via command line separately
// - Prevents mixing up of scenarios and skirmish maps to some degree
Path mapPath = Path(autoStartName);
std::wstring mapDirectory = mapPath.Parent().Filename().string();
std::string mapType;
if (mapDirectory == L"random")
{
// Random map definition will be loaded from JSON file, so we need to parse it
std::wstring scriptPath = L"maps/" + autoStartName.FromUTF8() + L".json";
JS::RootedValue scriptData(cx);
scriptInterface.ReadJSONFile(scriptPath, &scriptData);
if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "settings", &settings))
{
// JSON loaded ok - copy script name over to game attributes
std::wstring scriptFile;
scriptInterface.GetProperty(settings, "Script", scriptFile);
scriptInterface.SetProperty(attrs, "script", scriptFile); // RMS filename
}
else
{
// Problem with JSON file
LOGERROR("Autostart: Error reading random map script '%s'", utf8_from_wstring(scriptPath));
throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details.");
}
// Get optional map size argument (default 192)
uint mapSize = 192;
if (args.Has("autostart-size"))
{
CStr size = args.Get("autostart-size");
mapSize = size.ToUInt();
}
scriptInterface.SetProperty(settings, "Size", mapSize); // Random map size (in patches)
// Get optional number of players (default 2)
size_t numPlayers = 2;
if (args.Has("autostart-players"))
{
CStr num = args.Get("autostart-players");
numPlayers = num.ToUInt();
}
// Set up player data
for (size_t i = 0; i < numPlayers; ++i)
{
JS::RootedValue player(cx);
scriptInterface.Eval("({})", &player);
// We could load player_defaults.json here, but that would complicate the logic
// even more and autostart is only intended for developers anyway
scriptInterface.SetProperty(player, "Civ", std::string("athen"));
scriptInterface.SetPropertyInt(playerData, i, player);
}
mapType = "random";
}
else if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes")
{
// Initialize general settings from the map data so some values
// (e.g. name of map) are always present, even when autostart is
// partially configured
CStr8 mapSettingsJSON = LoadSettingsOfScenarioMap("maps/" + autoStartName + ".xml");
scriptInterface.ParseJSON(mapSettingsJSON, &settings);
// Initialize the playerData array being modified by autostart
// with the real map data, so sensible values are present:
scriptInterface.GetProperty(settings, "PlayerData", &playerData);
if (mapDirectory == L"scenarios")
mapType = "scenario";
else
mapType = "skirmish";
}
else
{
LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory));
throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types.");
}
scriptInterface.SetProperty(attrs, "mapType", mapType);
scriptInterface.SetProperty(attrs, "map", std::string("maps/" + autoStartName));
scriptInterface.SetProperty(settings, "mapType", mapType);
scriptInterface.SetProperty(settings, "CheatsEnabled", true);
// The seed is used for both random map generation and simulation
u32 seed = 0;
if (args.Has("autostart-seed"))
{
CStr seedArg = args.Get("autostart-seed");
if (seedArg == "-1")
seed = rand();
else
seed = seedArg.ToULong();
}
scriptInterface.SetProperty(settings, "Seed", seed);
// Set seed for AIs
u32 aiseed = 0;
if (args.Has("autostart-aiseed"))
{
CStr seedArg = args.Get("autostart-aiseed");
if (seedArg == "-1")
aiseed = rand();
else
aiseed = seedArg.ToULong();
}
scriptInterface.SetProperty(settings, "AISeed", aiseed);
// Set player data for AIs
// attrs.settings = { PlayerData: [ { AI: ... }, ... ] }
// or = { PlayerData: [ null, { AI: ... }, ... ] } when gaia set
int offset = 1;
JS::RootedValue player(cx);
if (scriptInterface.GetPropertyInt(playerData, 0, &player) && player.isNull())
offset = 0;
// Set teams
if (args.Has("autostart-team"))
{
std::vector civArgs = args.GetMultiple("autostart-team");
for (size_t i = 0; i < civArgs.size(); ++i)
{
int playerID = civArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-team option", playerID);
continue;
}
scriptInterface.Eval("({})", &player);
}
int teamID = civArgs[i].AfterFirst(":").ToInt() - 1;
scriptInterface.SetProperty(player, "Team", teamID);
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
int ceasefire = 0;
if (args.Has("autostart-ceasefire"))
ceasefire = args.Get("autostart-ceasefire").ToInt();
scriptInterface.SetProperty(settings, "Ceasefire", ceasefire);
if (args.Has("autostart-ai"))
{
std::vector aiArgs = args.GetMultiple("autostart-ai");
for (size_t i = 0; i < aiArgs.size(); ++i)
{
int playerID = aiArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-ai option", playerID);
continue;
}
scriptInterface.Eval("({})", &player);
}
CStr name = aiArgs[i].AfterFirst(":");
scriptInterface.SetProperty(player, "AI", std::string(name));
scriptInterface.SetProperty(player, "AIDiff", 3);
scriptInterface.SetProperty(player, "AIBehavior", std::string("balanced"));
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
// Set AI difficulty
if (args.Has("autostart-aidiff"))
{
std::vector civArgs = args.GetMultiple("autostart-aidiff");
for (size_t i = 0; i < civArgs.size(); ++i)
{
int playerID = civArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-aidiff option", playerID);
continue;
}
scriptInterface.Eval("({})", &player);
}
int difficulty = civArgs[i].AfterFirst(":").ToInt();
scriptInterface.SetProperty(player, "AIDiff", difficulty);
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
// Set player data for Civs
if (args.Has("autostart-civ"))
{
if (mapDirectory != L"scenarios")
{
std::vector civArgs = args.GetMultiple("autostart-civ");
for (size_t i = 0; i < civArgs.size(); ++i)
{
int playerID = civArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-civ option", playerID);
continue;
}
scriptInterface.Eval("({})", &player);
}
CStr name = civArgs[i].AfterFirst(":");
scriptInterface.SetProperty(player, "Civ", std::string(name));
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
else
LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios");
}
// Add player data to map settings
scriptInterface.SetProperty(settings, "PlayerData", playerData);
// Add map settings to game attributes
scriptInterface.SetProperty(attrs, "settings", settings);
// Get optional playername
CStrW userName = L"anonymous";
if (args.Has("autostart-playername"))
userName = args.Get("autostart-playername").FromUTF8();
// Add additional scripts to the TriggerScripts property
std::vector triggerScriptsVector;
JS::RootedValue triggerScripts(cx);
if (scriptInterface.HasProperty(settings, "TriggerScripts"))
{
scriptInterface.GetProperty(settings, "TriggerScripts", &triggerScripts);
FromJSVal_vector(cx, triggerScripts, triggerScriptsVector);
}
if (nonVisual)
{
CStr nonVisualScript = "scripts/NonVisualTrigger.js";
triggerScriptsVector.push_back(nonVisualScript.FromUTF8());
}
std::vector victoryConditions(1, "conquest");
if (args.Has("autostart-victory"))
victoryConditions = args.GetMultiple("autostart-victory");
if (victoryConditions.size() == 1 && victoryConditions[0] == "endless")
victoryConditions.clear();
scriptInterface.SetProperty(settings, "VictoryConditions", victoryConditions);
for (const CStr& victory : victoryConditions)
{
JS::RootedValue scriptData(cx);
JS::RootedValue data(cx);
JS::RootedValue victoryScripts(cx);
CStrW scriptPath = L"simulation/data/settings/victory_conditions/" + victory.FromUTF8() + L".json";
scriptInterface.ReadJSONFile(scriptPath, &scriptData);
if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "Data", &data) && !data.isUndefined()
&& scriptInterface.GetProperty(data, "Scripts", &victoryScripts) && !victoryScripts.isUndefined())
{
std::vector victoryScriptsVector;
FromJSVal_vector(cx, victoryScripts, victoryScriptsVector);
triggerScriptsVector.insert(triggerScriptsVector.end(), victoryScriptsVector.begin(), victoryScriptsVector.end());
}
else
{
LOGERROR("Autostart: Error reading victory script '%s'", utf8_from_wstring(scriptPath));
throw PSERROR_Game_World_MapLoadFailed("Error reading victory script.\nCheck application log for details.");
}
}
ToJSVal_vector(cx, &triggerScripts, triggerScriptsVector);
scriptInterface.SetProperty(settings, "TriggerScripts", triggerScripts);
int wonderDuration = 10;
if (args.Has("autostart-wonderduration"))
wonderDuration = args.Get("autostart-wonderduration").ToInt();
scriptInterface.SetProperty(settings, "WonderDuration", wonderDuration);
int relicDuration = 10;
if (args.Has("autostart-relicduration"))
relicDuration = args.Get("autostart-relicduration").ToInt();
scriptInterface.SetProperty(settings, "RelicDuration", relicDuration);
int relicCount = 2;
if (args.Has("autostart-reliccount"))
relicCount = args.Get("autostart-reliccount").ToInt();
scriptInterface.SetProperty(settings, "RelicCount", relicCount);
if (args.Has("autostart-host"))
{
InitPsAutostart(true, attrs);
size_t maxPlayers = 2;
if (args.Has("autostart-host-players"))
maxPlayers = args.Get("autostart-host-players").ToUInt();
g_NetServer = new CNetServer(false, maxPlayers);
g_NetServer->UpdateGameAttributes(&attrs, scriptInterface);
bool ok = g_NetServer->SetupConnection(PS_DEFAULT_PORT);
ENSURE(ok);
g_NetClient = new CNetClient(g_Game, true);
g_NetClient->SetUserName(userName);
g_NetClient->SetupConnection("127.0.0.1", PS_DEFAULT_PORT);
}
else if (args.Has("autostart-client"))
{
InitPsAutostart(true, attrs);
g_NetClient = new CNetClient(g_Game, false);
g_NetClient->SetUserName(userName);
CStr ip = args.Get("autostart-client");
if (ip.empty())
ip = "127.0.0.1";
bool ok = g_NetClient->SetupConnection(ip, PS_DEFAULT_PORT);
ENSURE(ok);
}
else
{
g_Game->SetPlayerID(args.Has("autostart-player") ? args.Get("autostart-player").ToInt() : 1);
g_Game->StartGame(&attrs, "");
if (nonVisual)
{
// TODO: Non progressive load can fail - need a decent way to handle this
LDR_NonprogressiveLoad();
ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK);
}
else
InitPsAutostart(false, attrs);
}
return true;
}
bool AutostartVisualReplay(const std::string& replayFile)
{
if (!FileExists(OsPath(replayFile)))
return false;
g_Game = new CGame(false, false);
g_Game->SetPlayerID(-1);
g_Game->StartVisualReplay(replayFile);
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue attrs(cx, g_Game->GetSimulation2()->GetInitAttributes());
InitPsAutostart(false, attrs);
return true;
}
void CancelLoad(const CStrW& message)
{
shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface();
JSContext* cx = pScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue global(cx, pScriptInterface->GetGlobalObject());
LDR_Cancel();
if (g_GUI &&
g_GUI->HasPages() &&
pScriptInterface->HasProperty(global, "cancelOnLoadGameError"))
pScriptInterface->CallFunctionVoid(global, "cancelOnLoadGameError", message);
}
bool InDevelopmentCopy()
{
if (!g_CheckedIfInDevelopmentCopy)
{
g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK);
g_CheckedIfInDevelopmentCopy = true;
}
return g_InDevelopmentCopy;
}
Index: ps/trunk/source/ps/ModInstaller.cpp
===================================================================
--- ps/trunk/source/ps/ModInstaller.cpp (revision 21758)
+++ ps/trunk/source/ps/ModInstaller.cpp (revision 21759)
@@ -1,111 +1,110 @@
/* Copyright (C) 2018 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 "ModInstaller.h"
#include "lib/file/vfs/vfs_util.h"
#include "ps/Filesystem.h"
#include "ps/XML/Xeromyces.h"
#include
CModInstaller::CModInstaller(const OsPath& modsdir, const OsPath& tempdir) :
m_ModsDir(modsdir), m_TempDir(tempdir / "_modscache"), m_CacheDir("cache/")
{
m_VFS = CreateVfs();
CreateDirectories(m_TempDir, 0700);
}
CModInstaller::~CModInstaller()
{
m_VFS.reset();
DeleteDirectory(m_TempDir);
}
CModInstaller::ModInstallationResult CModInstaller::Install(
const OsPath& mod,
const std::shared_ptr& scriptRuntime,
- bool deleteAfterInstall)
+ bool keepFile)
{
const OsPath modTemp = m_TempDir / mod.Basename() / mod.Filename().ChangeExtension(L".zip");
CreateDirectories(modTemp.Parent(), 0700);
- CopyFile(mod, modTemp, true);
+ if (keepFile)
+ CopyFile(mod, modTemp, true);
+ else
+ wrename(mod, modTemp);
// Load the mod to VFS
if (m_VFS->Mount(m_CacheDir, m_TempDir / "") != INFO::OK)
return FAIL_ON_VFS_MOUNT;
CVFSFile modinfo;
PSRETURN modinfo_status = modinfo.Load(m_VFS, m_CacheDir / modTemp.Basename() / "mod.json", false);
m_VFS->Clear();
if (modinfo_status != PSRETURN_OK)
return FAIL_ON_MOD_LOAD;
// Extract the name of the mod
ScriptInterface scriptInterface("Engine", "ModInstaller", scriptRuntime);
JSContext* cx = scriptInterface.GetContext();
JS::RootedValue json_val(cx);
if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json_val))
return FAIL_ON_PARSE_JSON;
JS::RootedObject json_obj(cx, json_val.toObjectOrNull());
JS::RootedValue name_val(cx);
if (!JS_GetProperty(cx, json_obj, "name", &name_val))
return FAIL_ON_EXTRACT_NAME;
CStr modName;
ScriptInterface::FromJSVal(cx, name_val, modName);
if (modName.empty())
return FAIL_ON_EXTRACT_NAME;
const OsPath modDir = m_ModsDir / modName;
const OsPath modPath = modDir / (modName + ".zip");
// Create a directory with the following structure:
// mod-name/
// mod-name.zip
CreateDirectories(modDir, 0700);
if (wrename(modTemp, modPath) != 0)
return FAIL_ON_MOD_MOVE;
DeleteDirectory(modTemp.Parent());
#ifdef OS_WIN
// On Windows, write the contents of mod.json to a separate file next to the archive:
// mod-name/
// mod-name.zip
// mod.json
std::ofstream mod_json((modDir / "mod.json").string8());
if (mod_json.good())
{
mod_json << modinfo.GetAsString();
mod_json.close();
}
#endif // OS_WIN
- // Remove the original file if requested
- if (deleteAfterInstall)
- wunlink(mod);
-
m_InstalledMods.emplace_back(modName);
return SUCCESS;
}
const std::vector& CModInstaller::GetInstalledMods() const
{
return m_InstalledMods;
}
Index: ps/trunk/source/ps/ModInstaller.h
===================================================================
--- ps/trunk/source/ps/ModInstaller.h (revision 21758)
+++ ps/trunk/source/ps/ModInstaller.h (revision 21759)
@@ -1,83 +1,84 @@
/* Copyright (C) 2018 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 .
*/
#ifndef INCLUDED_MODINSTALLER
#define INCLUDED_MODINSTALLER
#include "CStr.h"
#include "lib/file/vfs/vfs.h"
#include "scriptinterface/ScriptInterface.h"
#include
/**
* Install a mod into the mods directory.
*/
class CModInstaller
{
public:
enum ModInstallationResult
{
SUCCESS,
FAIL_ON_VFS_MOUNT,
FAIL_ON_MOD_LOAD,
FAIL_ON_PARSE_JSON,
FAIL_ON_EXTRACT_NAME,
FAIL_ON_MOD_MOVE
};
/**
* Initialise the mod installer for processing the given mod.
*
* @param modsdir path to the data directory that contains mods
* @param tempdir path to a writable directory for temporary files
*/
CModInstaller(const OsPath& modsdir, const OsPath& tempdir);
~CModInstaller();
/**
* Process and unpack the mod.
* @param mod path of .pyromod/.zip file
+ * @param keepFile if true, copy the file, if false move it
*/
ModInstallationResult Install(
const OsPath& mod,
const std::shared_ptr& scriptRuntime,
- bool deleteAfterInstall);
+ bool keepFile);
/**
* @return a list of all mods installed so far by this CModInstaller.
*/
const std::vector& GetInstalledMods() const;
/**
* @return whether the path has a mod-like extension.
*/
static bool IsDefaultModExtension(const Path& ext)
{
return ext == ".pyromod" || ext == ".zip";
}
private:
PIVFS m_VFS;
OsPath m_ModsDir;
OsPath m_TempDir;
VfsPath m_CacheDir;
std::vector m_InstalledMods;
};
#endif // INCLUDED_MODINSTALLER
Index: ps/trunk/source/ps/ModIo.cpp
===================================================================
--- ps/trunk/source/ps/ModIo.cpp (nonexistent)
+++ ps/trunk/source/ps/ModIo.cpp (revision 21759)
@@ -0,0 +1,822 @@
+/* Copyright (C) 2018 Wildfire Games.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "precompiled.h"
+
+#include "ModIo.h"
+
+#include "i18n/L10n.h"
+#include "lib/file/file_system.h"
+#include "lib/sysdep/filesystem.h"
+#include "lib/sysdep/sysdep.h"
+#include "maths/MD5.h"
+#include "ps/CLogger.h"
+#include "ps/ConfigDB.h"
+#include "ps/GameSetup/Paths.h"
+#include "ps/Mod.h"
+#include "ps/ModInstaller.h"
+#include "scriptinterface/ScriptConversions.h"
+#include "scriptinterface/ScriptInterface.h"
+
+#include
+#include
+
+ModIo* g_ModIo = nullptr;
+
+struct DownloadCallbackData
+{
+ DownloadCallbackData()
+ : fp(nullptr), md5()
+ {
+ }
+ DownloadCallbackData(FILE* _fp)
+ : fp(_fp), md5()
+ {
+ crypto_generichash_init(&hash_state, NULL, 0U, crypto_generichash_BYTES_MAX);
+ }
+ FILE* fp;
+ MD5 md5;
+ crypto_generichash_state hash_state;
+};
+
+ModIo::ModIo()
+ : m_GamesRequest("/games"), m_CallbackData(nullptr)
+{
+ // Get config values from the sytem namespace, or below (default).
+ // This can be overridden on the command line.
+ //
+ // We do this so a malicious mod cannot change the base url and
+ // get the user to make connections to someone else's endpoint.
+ // If another user of the engine wants to provide different values
+ // here, while still using the same engine version, they can just
+ // provide some shortcut/script that sets these using command line
+ // parameters.
+ std::string pk_str;
+ g_ConfigDB.GetValue(CFG_SYSTEM, "modio.public_key", pk_str);
+ g_ConfigDB.GetValue(CFG_SYSTEM, "modio.v1.baseurl", m_BaseUrl);
+ {
+ std::string api_key;
+ g_ConfigDB.GetValue(CFG_SYSTEM, "modio.v1.api_key", api_key);
+ m_ApiKey = "api_key=" + api_key;
+ }
+ {
+ std::string nameid;
+ g_ConfigDB.GetValue(CFG_SYSTEM, "modio.v1.name_id", nameid);
+ m_IdQuery = "name_id="+nameid;
+ }
+
+ m_CurlMulti = curl_multi_init();
+ ENSURE(m_CurlMulti);
+
+ m_Curl = curl_easy_init();
+ ENSURE(m_Curl);
+
+ // Capture error messages
+ curl_easy_setopt(m_Curl, CURLOPT_ERRORBUFFER, m_ErrorBuffer);
+
+ // Fail if the server did
+ curl_easy_setopt(m_Curl, CURLOPT_FAILONERROR, 1L);
+
+ // Disable signal handlers (required for multithreaded applications)
+ curl_easy_setopt(m_Curl, CURLOPT_NOSIGNAL, 1L);
+
+ // To minimise security risks, don't support redirects (except for file
+ // downloads, for which this setting will be enabled).
+ curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 0L);
+
+ // For file downloads, one redirect seems plenty for a CDN serving the files.
+ curl_easy_setopt(m_Curl, CURLOPT_MAXREDIRS, 1L);
+
+ m_Headers = NULL;
+ std::string ua = "User-Agent: pyrogenesis ";
+ ua += curl_version();
+ ua += " (https://play0ad.com/)";
+ m_Headers = curl_slist_append(m_Headers, ua.c_str());
+ curl_easy_setopt(m_Curl, CURLOPT_HTTPHEADER, m_Headers);
+
+ if (sodium_init() < 0)
+ ENSURE(0 && "Failed to initialize libsodium.");
+
+ size_t bin_len = 0;
+ if (sodium_base642bin((unsigned char*)&m_pk, sizeof m_pk, pk_str.c_str(), pk_str.size(), NULL, &bin_len, NULL, sodium_base64_VARIANT_ORIGINAL) != 0 || bin_len != sizeof m_pk)
+ ENSURE(0 && "Failed to decode base64 public key. Please fix your configuration or mod.io will be unusable.");
+}
+
+ModIo::~ModIo()
+{
+ // Clean things up to avoid unpleasant surprises,
+ // and delete the temporary file if any.
+ TearDownRequest();
+ if (m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING)
+ DeleteDownloadedFile();
+
+ curl_slist_free_all(m_Headers);
+ curl_easy_cleanup(m_Curl);
+ curl_multi_cleanup(m_CurlMulti);
+
+ delete m_CallbackData;
+}
+
+size_t ModIo::ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp)
+{
+ ModIo* self = static_cast(userp);
+
+ self->m_ResponseData += std::string((char*)buffer, (char*)buffer+size*nmemb);
+
+ return size*nmemb;
+}
+
+size_t ModIo::DownloadCallback(void* buffer, size_t size, size_t nmemb, void* userp)
+{
+ DownloadCallbackData* data = static_cast(userp);
+ if (!data->fp)
+ return 0;
+
+ size_t len = fwrite(buffer, size, nmemb, data->fp);
+
+ // Only update the hash with data we actually managed to write.
+ // In case we did not write all of it we will fail the download,
+ // but we do not want to have a possibly valid hash in that case.
+ size_t written = len*size;
+
+ data->md5.Update((const u8*)buffer, written);
+ crypto_generichash_update(&data->hash_state, (const u8*)buffer, written);
+
+ return written;
+}
+
+int ModIo::DownloadProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t UNUSED(ultotal), curl_off_t UNUSED(ulnow))
+{
+ DownloadProgressData* data = static_cast(clientp);
+
+ // If we got more data than curl expected, something is very wrong, abort.
+ if (dltotal != 0 && dlnow > dltotal)
+ return 1;
+
+ data->progress = dltotal == 0 ? 0 : static_cast(dlnow) / static_cast(dltotal);
+
+ return 0;
+}
+
+CURLMcode ModIo::SetupRequest(const std::string& url, bool fileDownload)
+{
+ if (fileDownload)
+ {
+ // The download link will most likely redirect elsewhere, so allow that.
+ // We verify the validity of the file later.
+ curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 1L);
+ // Enable the progress meter
+ curl_easy_setopt(m_Curl, CURLOPT_NOPROGRESS, 0L);
+
+ // Set IO callbacks
+ curl_easy_setopt(m_Curl, CURLOPT_WRITEFUNCTION, DownloadCallback);
+ curl_easy_setopt(m_Curl, CURLOPT_WRITEDATA, (void*)m_CallbackData);
+ curl_easy_setopt(m_Curl, CURLOPT_XFERINFOFUNCTION, DownloadProgressCallback);
+ curl_easy_setopt(m_Curl, CURLOPT_XFERINFODATA, (void*)&m_DownloadProgressData);
+
+ // Initialize the progress counter
+ m_DownloadProgressData.progress = 0;
+ }
+ else
+ {
+ // To minimise security risks, don't support redirects
+ curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 0L);
+ // Disable the progress meter
+ curl_easy_setopt(m_Curl, CURLOPT_NOPROGRESS, 1L);
+
+ // Set IO callbacks
+ curl_easy_setopt(m_Curl, CURLOPT_WRITEFUNCTION, ReceiveCallback);
+ curl_easy_setopt(m_Curl, CURLOPT_WRITEDATA, this);
+ }
+
+ m_ErrorBuffer[0] = '\0';
+ curl_easy_setopt(m_Curl, CURLOPT_URL, url.c_str());
+ return curl_multi_add_handle(m_CurlMulti, m_Curl);
+}
+
+void ModIo::TearDownRequest()
+{
+ ENSURE(curl_multi_remove_handle(m_CurlMulti, m_Curl) == CURLM_OK);
+
+ if (m_CallbackData)
+ {
+ if (m_CallbackData->fp)
+ fclose(m_CallbackData->fp);
+ m_CallbackData->fp = nullptr;
+ }
+}
+
+void ModIo::StartGetGameId()
+{
+ // Don't start such a request during active downloads.
+ if (m_DownloadProgressData.status == DownloadProgressStatus::GAMEID ||
+ m_DownloadProgressData.status == DownloadProgressStatus::LISTING ||
+ m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING)
+ return;
+
+ m_GameId.clear();
+
+ CURLMcode err = SetupRequest(m_BaseUrl+m_GamesRequest+"?"+m_ApiKey+"&"+m_IdQuery, false);
+ if (err != CURLM_OK)
+ {
+ TearDownRequest();
+ m_DownloadProgressData.status = DownloadProgressStatus::FAILED_GAMEID;
+ m_DownloadProgressData.error = fmt::sprintf(
+ g_L10n.Translate("Failure while starting querying for game id. Error: %s; %s."),
+ curl_multi_strerror(err), m_ErrorBuffer);
+ return;
+ }
+
+ m_DownloadProgressData.status = DownloadProgressStatus::GAMEID;
+}
+
+void ModIo::StartListMods()
+{
+ // Don't start such a request during active downloads.
+ if (m_DownloadProgressData.status == DownloadProgressStatus::GAMEID ||
+ m_DownloadProgressData.status == DownloadProgressStatus::LISTING ||
+ m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING)
+ return;
+
+ m_ModData.clear();
+
+ if (m_GameId.empty())
+ {
+ LOGERROR("Game ID not fetched from mod.io. Call StartGetGameId first and wait for it to finish.");
+ return;
+ }
+
+ CURLMcode err = SetupRequest(m_BaseUrl+m_GamesRequest+m_GameId+"/mods?"+m_ApiKey, false);
+ if (err != CURLM_OK)
+ {
+ TearDownRequest();
+ m_DownloadProgressData.status = DownloadProgressStatus::FAILED_LISTING;
+ m_DownloadProgressData.error = fmt::sprintf(
+ g_L10n.Translate("Failure while starting querying for mods. Error: %s; %s."),
+ curl_multi_strerror(err), m_ErrorBuffer);
+ return;
+ }
+
+ m_DownloadProgressData.status = DownloadProgressStatus::LISTING;
+}
+
+void ModIo::StartDownloadMod(size_t idx)
+{
+ // Don't start such a request during active downloads.
+ if (m_DownloadProgressData.status == DownloadProgressStatus::GAMEID ||
+ m_DownloadProgressData.status == DownloadProgressStatus::LISTING ||
+ m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING)
+ return;
+
+ if (idx >= m_ModData.size())
+ return;
+
+ const Paths paths(g_args);
+ const OsPath modUserPath = paths.UserData()/"mods";
+ const OsPath modPath = modUserPath/m_ModData[idx].properties["name_id"];
+ if (!DirectoryExists(modPath) && INFO::OK != CreateDirectories(modPath, 0700, false))
+ {
+ m_DownloadProgressData.status = DownloadProgressStatus::FAILED_DOWNLOADING;
+ m_DownloadProgressData.error = fmt::sprintf(
+ g_L10n.Translate("Could not create mod directory: %s."), modPath.string8());
+ return;
+ }
+
+ // Name the file after the name_id, since using the filename would mean that
+ // we could end up with multiple zip files in the folder that might not work
+ // as expected for a user (since a later version might remove some files
+ // that aren't compatible anymore with the engine version).
+ // So we ignore the filename provided by the API and assume that we do not
+ // care about handling update.zip files. If that is the case we would need
+ // a way to find out what files are required by the current one and which
+ // should be removed for everything to work. This seems to be too complicated
+ // so we just do not support that usage.
+ // NOTE: We do save the file under a slightly different name from the final
+ // one, to ensure that in case a download aborts and the file stays
+ // around, the game will not attempt to open the file which has not
+ // been verified.
+ m_DownloadFilePath = modPath/(m_ModData[idx].properties["name_id"]+".zip.temp");
+
+ delete m_CallbackData;
+ m_CallbackData = new DownloadCallbackData(sys_OpenFile(m_DownloadFilePath, "wb"));
+ if (!m_CallbackData->fp)
+ {
+ m_DownloadProgressData.status = DownloadProgressStatus::FAILED_DOWNLOADING;
+ m_DownloadProgressData.error = fmt::sprintf(
+ g_L10n.Translate("Could not open temporary file for mod download: %s."), m_DownloadFilePath.string8());
+ return;
+ }
+
+ CURLMcode err = SetupRequest(m_ModData[idx].properties["binary_url"], true);
+ if (err != CURLM_OK)
+ {
+ TearDownRequest();
+ m_DownloadProgressData.status = DownloadProgressStatus::FAILED_DOWNLOADING;
+ m_DownloadProgressData.error = fmt::sprintf(
+ g_L10n.Translate("Failed to start the download. Error: %s; %s."), curl_multi_strerror(err), m_ErrorBuffer);
+ return;
+ }
+
+ m_DownloadModID = idx;
+ m_DownloadProgressData.status = DownloadProgressStatus::DOWNLOADING;
+}
+
+void ModIo::CancelRequest()
+{
+ TearDownRequest();
+
+ switch (m_DownloadProgressData.status)
+ {
+ case DownloadProgressStatus::GAMEID:
+ case DownloadProgressStatus::FAILED_GAMEID:
+ m_DownloadProgressData.status = DownloadProgressStatus::NONE;
+ break;
+ case DownloadProgressStatus::LISTING:
+ case DownloadProgressStatus::FAILED_LISTING:
+ m_DownloadProgressData.status = DownloadProgressStatus::READY;
+ break;
+ case DownloadProgressStatus::DOWNLOADING:
+ case DownloadProgressStatus::FAILED_DOWNLOADING:
+ m_DownloadProgressData.status = DownloadProgressStatus::LISTED;
+ DeleteDownloadedFile();
+ break;
+ default:
+ break;
+ }
+}
+
+bool ModIo::AdvanceRequest(const ScriptInterface& scriptInterface)
+{
+ // If the request was cancelled, stop trying to advance it
+ if (m_DownloadProgressData.status != DownloadProgressStatus::GAMEID &&
+ m_DownloadProgressData.status != DownloadProgressStatus::LISTING &&
+ m_DownloadProgressData.status != DownloadProgressStatus::DOWNLOADING)
+ return true;
+
+ int stillRunning;
+ CURLMcode err = curl_multi_perform(m_CurlMulti, &stillRunning);
+ if (err != CURLM_OK)
+ {
+ std::string error = fmt::sprintf(
+ g_L10n.Translate("Asynchronous download failure: %s, %s."), curl_multi_strerror(err), m_ErrorBuffer);
+ TearDownRequest();
+ if (m_DownloadProgressData.status == DownloadProgressStatus::GAMEID)
+ m_DownloadProgressData.status = DownloadProgressStatus::FAILED_GAMEID;
+ else if (m_DownloadProgressData.status == DownloadProgressStatus::LISTING)
+ m_DownloadProgressData.status = DownloadProgressStatus::FAILED_LISTING;
+ else if (m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING)
+ {
+ m_DownloadProgressData.status = DownloadProgressStatus::FAILED_DOWNLOADING;
+ DeleteDownloadedFile();
+ }
+ m_DownloadProgressData.error = error;
+ return true;
+ }
+
+ CURLMsg* message;
+ do
+ {
+ int in_queue;
+ message = curl_multi_info_read(m_CurlMulti, &in_queue);
+ if (!message || message->msg == CURLMSG_DONE || message->easy_handle == m_Curl)
+ continue;
+
+ CURLcode err = message->data.result;
+ if (err == CURLE_OK)
+ continue;
+
+ std::string error = fmt::sprintf(
+ g_L10n.Translate("Download failure. Server response: %s; %s."), curl_easy_strerror(err), m_ErrorBuffer);
+ TearDownRequest();
+ if (m_DownloadProgressData.status == DownloadProgressStatus::GAMEID)
+ m_DownloadProgressData.status = DownloadProgressStatus::FAILED_GAMEID;
+ else if (m_DownloadProgressData.status == DownloadProgressStatus::LISTING)
+ m_DownloadProgressData.status = DownloadProgressStatus::FAILED_LISTING;
+ else if (m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING)
+ {
+ m_DownloadProgressData.status = DownloadProgressStatus::FAILED_DOWNLOADING;
+ DeleteDownloadedFile();
+ }
+ m_DownloadProgressData.error = error;
+ return true;
+ } while (message);
+
+ if (stillRunning)
+ return false;
+
+ // Download finished.
+ TearDownRequest();
+
+ // Perform parsing and/or checks
+ std::string error;
+ switch (m_DownloadProgressData.status)
+ {
+ case DownloadProgressStatus::GAMEID:
+ if (!ParseGameId(scriptInterface, error))
+ {
+ m_DownloadProgressData.status = DownloadProgressStatus::FAILED_GAMEID;
+ m_DownloadProgressData.error = error;
+ break;
+ }
+
+ m_DownloadProgressData.status = DownloadProgressStatus::READY;
+ break;
+ case DownloadProgressStatus::LISTING:
+ if (!ParseMods(scriptInterface, error))
+ {
+ m_ModData.clear(); // Failed during parsing, make sure we don't provide partial data
+ m_DownloadProgressData.status = DownloadProgressStatus::FAILED_LISTING;
+ m_DownloadProgressData.error = error;
+ break;
+ }
+
+ m_DownloadProgressData.status = DownloadProgressStatus::LISTED;
+ break;
+ case DownloadProgressStatus::DOWNLOADING:
+ if (!VerifyDownloadedFile(error))
+ {
+ m_DownloadProgressData.status = DownloadProgressStatus::FAILED_FILECHECK;
+ m_DownloadProgressData.error = error;
+ DeleteDownloadedFile();
+ break;
+ }
+
+ m_DownloadProgressData.status = DownloadProgressStatus::SUCCESS;
+
+ {
+ Paths paths(g_args);
+ CModInstaller installer(paths.UserData() / "mods", paths.Cache());
+ installer.Install(m_DownloadFilePath, g_ScriptRuntime, false);
+ }
+ break;
+ default:
+ break;
+ }
+
+ return true;
+}
+
+bool ModIo::ParseGameId(const ScriptInterface& scriptInterface, std::string& err)
+{
+ int id = -1;
+ bool ret = ParseGameIdResponse(scriptInterface, m_ResponseData, id, err);
+ m_ResponseData.clear();
+ if (!ret)
+ return false;
+
+ m_GameId = "/" + std::to_string(id);
+ return true;
+}
+
+bool ModIo::ParseMods(const ScriptInterface& scriptInterface, std::string& err)
+{
+ bool ret = ParseModsResponse(scriptInterface, m_ResponseData, m_ModData, m_pk, err);
+ m_ResponseData.clear();
+ return ret;
+}
+
+void ModIo::DeleteDownloadedFile()
+{
+ if (wunlink(m_DownloadFilePath) != 0)
+ LOGERROR("Failed to delete temporary file.");
+ m_DownloadFilePath = OsPath();
+}
+
+bool ModIo::VerifyDownloadedFile(std::string& err)
+{
+ // Verify filesize, as a first basic download check.
+ {
+ u64 filesize = std::stoull(m_ModData[m_DownloadModID].properties.at("filesize"));
+ if (filesize != FileSize(m_DownloadFilePath))
+ {
+ err = g_L10n.Translate("Mismatched filesize.");
+ return false;
+ }
+ }
+
+ ENSURE(m_CallbackData);
+
+ // MD5 (because upstream provides it)
+ // Just used to make sure there was no obvious corruption during transfer.
+ {
+ u8 digest[MD5::DIGESTSIZE];
+ m_CallbackData->md5.Final(digest);
+ std::stringstream md5digest;
+ md5digest << std::hex << std::setfill('0');
+ for (size_t i = 0; i < MD5::DIGESTSIZE; ++i)
+ md5digest << std::setw(2) << (int)digest[i];
+
+ if (m_ModData[m_DownloadModID].properties.at("filehash_md5") != md5digest.str())
+ {
+ err = fmt::sprintf(
+ g_L10n.Translate("Invalid file. Expected md5 %s, got %s."),
+ m_ModData[m_DownloadModID].properties.at("filehash_md5").c_str(),
+ md5digest.str());
+ return false;
+ }
+ }
+
+ // Verify file signature.
+ // Used to make sure that the downloaded file was actually checked and signed
+ // by Wildfire Games. And has not been tampered with by the API provider, or the CDN.
+
+ unsigned char hash_fin[crypto_generichash_BYTES_MAX] = {};
+ if (crypto_generichash_final(&m_CallbackData->hash_state, hash_fin, sizeof hash_fin) != 0)
+ {
+ err = g_L10n.Translate("Failed to compute final hash.");
+ return false;
+ }
+
+ if (crypto_sign_verify_detached(m_ModData[m_DownloadModID].sig.sig, hash_fin, sizeof hash_fin, m_pk.pk) != 0)
+ {
+ err = g_L10n.Translate("Failed to verify signature.");
+ return false;
+ }
+
+ return true;
+}
+
+#define FAIL(...) STMT(err = fmt::sprintf(__VA_ARGS__); CLEANUP(); return false;)
+
+/**
+ * Parses the current content of m_ResponseData to extract m_GameId.
+ *
+ * The JSON data is expected to look like
+ * { "data": [{"id": 42, ...}, ...], ... }
+ * where we are only interested in the value of the id property.
+ *
+ * @returns true iff it successfully parsed the id.
+ */
+bool ModIo::ParseGameIdResponse(const ScriptInterface& scriptInterface, const std::string& responseData, int& id, std::string& err)
+{
+#define CLEANUP() id = -1;
+ JSContext* cx = scriptInterface.GetContext();
+ JSAutoRequest rq(cx);
+
+ JS::RootedValue gameResponse(cx);
+
+ if (!scriptInterface.ParseJSON(responseData, &gameResponse))
+ FAIL("Failed to parse response as JSON.");
+
+ if (!gameResponse.isObject())
+ FAIL("response not an object.");
+
+ JS::RootedObject gameResponseObj(cx, gameResponse.toObjectOrNull());
+ JS::RootedValue dataVal(cx);
+ if (!JS_GetProperty(cx, gameResponseObj, "data", &dataVal))
+ FAIL("data property not in response.");
+
+ // [{"id": 42, ...}, ...]
+ if (!dataVal.isObject())
+ FAIL("data property not an object.");
+
+ JS::RootedObject data(cx, dataVal.toObjectOrNull());
+ u32 length;
+ if (!JS_IsArrayObject(cx, data) || !JS_GetArrayLength(cx, data, &length) || !length)
+ FAIL("data property not an array with at least one element.");
+
+ // {"id": 42, ...}
+ JS::RootedValue first(cx);
+ if (!JS_GetElement(cx, data, 0, &first))
+ FAIL("Couldn't get first element.");
+ if (!first.isObject())
+ FAIL("First element not an object.");
+
+ JS::RootedObject firstObj(cx, &first.toObject());
+ bool hasIdProperty;
+ if (!JS_HasProperty(cx, firstObj, "id", &hasIdProperty) || !hasIdProperty)
+ FAIL("No id property in first element.");
+
+ JS::RootedValue idProperty(cx);
+ ENSURE(JS_GetProperty(cx, firstObj, "id", &idProperty));
+
+ // Make sure the property is not set to something that could be converted to a bogus value
+ // TODO: We should be able to convert JS::Values to C++ variables in a way that actually
+ // fails when types do not match (see https://trac.wildfiregames.com/ticket/5128).
+ if (!idProperty.isNumber())
+ FAIL("id property not a number.");
+
+ id = -1;
+ if (!ScriptInterface::FromJSVal(cx, idProperty, id) || id <= 0)
+ FAIL("Invalid id.");
+
+ return true;
+#undef CLEANUP
+}
+
+/**
+ * Parses the current content of m_ResponseData into m_ModData.
+ *
+ * The JSON data is expected to look like
+ * { data: [modobj1, modobj2, ...], ... (including result_count) }
+ * where modobjN has the following structure
+ * { homepage: "url", name: "displayname", nameid: "short-non-whitespace-name",
+ * summary: "short desc.", modfile: { version: "1.2.4", filename: "asdf.zip",
+ * filehash: { md5: "deadbeef" }, filesize: 1234, download: { binary_url: "someurl", ... } }, ... }.
+ * Only the listed properties are of interest to consumers, and we flatten
+ * the modfile structure as that simplifies handling and there are no conflicts.
+ */
+bool ModIo::ParseModsResponse(const ScriptInterface& scriptInterface, const std::string& responseData, std::vector& modData, const PKStruct& pk, std::string& err)
+{
+// Make sure we don't end up passing partial results back
+#define CLEANUP() modData.clear();
+
+ JSContext* cx = scriptInterface.GetContext();
+ JSAutoRequest rq(cx);
+
+ JS::RootedValue modResponse(cx);
+
+ if (!scriptInterface.ParseJSON(responseData, &modResponse))
+ FAIL("Failed to parse response as JSON.");
+
+ if (!modResponse.isObject())
+ FAIL("response not an object.");
+
+ JS::RootedObject modResponseObj(cx, modResponse.toObjectOrNull());
+ JS::RootedValue dataVal(cx);
+ if (!JS_GetProperty(cx, modResponseObj, "data", &dataVal))
+ FAIL("data property not in response.");
+
+ // [modobj1, modobj2, ... ]
+ if (!dataVal.isObject())
+ FAIL("data property not an object.");
+
+ JS::RootedObject data(cx, dataVal.toObjectOrNull());
+ u32 length;
+ if (!JS_IsArrayObject(cx, data) || !JS_GetArrayLength(cx, data, &length) || !length)
+ FAIL("data property not an array with at least one element.");
+
+ modData.clear();
+ modData.reserve(length);
+
+ for (u32 i = 0; i < length; ++i)
+ {
+ JS::RootedValue el(cx);
+ if (!JS_GetElement(cx, data, i, &el) || !el.isObject())
+ FAIL("Failed to get array element object.");
+
+ modData.emplace_back();
+
+#define COPY_STRINGS(prefix, obj, ...) \
+ for (const std::string& prop : { __VA_ARGS__ }) \
+ { \
+ std::string val; \
+ if (!ScriptInterface::FromJSProperty(cx, obj, prop.c_str(), val)) \
+ FAIL("Failed to get %s from %s.", prop, #obj);\
+ modData.back().properties.emplace(prefix+prop, val); \
+ }
+
+ // TODO: Currently the homepage field does not contain a non-null value for any entry.
+ COPY_STRINGS("", el, "name", "name_id", "summary");
+
+ // Now copy over the modfile part, but without the pointless substructure
+ JS::RootedObject elObj(cx, el.toObjectOrNull());
+ JS::RootedValue modFile(cx);
+ if (!JS_GetProperty(cx, elObj, "modfile", &modFile))
+ FAIL("Failed to get modfile data.");
+
+ if (!modFile.isObject())
+ FAIL("modfile not an object.");
+
+ COPY_STRINGS("", modFile, "version", "filesize");
+
+ JS::RootedObject modFileObj(cx, modFile.toObjectOrNull());
+ JS::RootedValue filehash(cx);
+ if (!JS_GetProperty(cx, modFileObj, "filehash", &filehash))
+ FAIL("Failed to get filehash data.");
+
+ COPY_STRINGS("filehash_", filehash, "md5");
+
+ JS::RootedValue download(cx);
+ if (!JS_GetProperty(cx, modFileObj, "download", &download))
+ FAIL("Failed to get download data.");
+
+ COPY_STRINGS("", download, "binary_url");
+
+ // Parse metadata_blob (sig+deps)
+ std::string metadata_blob;
+ if (!ScriptInterface::FromJSProperty(cx, modFile, "metadata_blob", metadata_blob))
+ FAIL("Failed to get metadata_blob from modFile.");
+
+ JS::RootedValue metadata(cx);
+ if (!scriptInterface.ParseJSON(metadata_blob, &metadata))
+ FAIL("Failed to parse metadata_blob as JSON.");
+
+ if (!metadata.isObject())
+ FAIL("metadata_blob not decoded as an object.");
+
+ if (!ScriptInterface::FromJSProperty(cx, metadata, "dependencies", modData.back().dependencies))
+ FAIL("Failed to get dependencies from metadata_blob.");
+
+ std::vector minisigs;
+ if (!ScriptInterface::FromJSProperty(cx, metadata, "minisigs", minisigs))
+ FAIL("Failed to get minisigs from metadata_blob.");
+
+ // Remove this entry if we did not find a valid matching signature.
+ std::string signatureParsingErr;
+ if (!ParseSignature(minisigs, modData.back().sig, pk, signatureParsingErr))
+ modData.pop_back();
+
+#undef COPY_STRINGS
+ }
+
+ return true;
+#undef CLEANUP
+}
+
+/**
+ * Parse signatures to find one that matches the public key, and has a valid global signature.
+ * Returns true and sets @param sig to the valid matching signature.
+ */
+bool ModIo::ParseSignature(const std::vector& minisigs, SigStruct& sig, const PKStruct& pk, std::string& err)
+{
+#define CLEANUP() sig = {};
+ for (const std::string& file_sig : minisigs)
+ {
+ // Format of a .minisig file (created using minisign(1) with -SHm file.zip)
+ // untrusted comment: .*\nb64sign_of_file\ntrusted comment: .*\nb64sign_of_sign_of_file_and_trusted_comment
+
+ std::vector sig_lines;
+ boost::split(sig_lines, file_sig, boost::is_any_of("\n"));
+ if (sig_lines.size() < 4)
+ FAIL("Invalid (too short) sig.");
+
+ // Verify that both the untrusted comment and the trusted comment start with the correct prefix
+ // because that is easy.
+ const std::string untrusted_comment_prefix = "untrusted comment: ";
+ const std::string trusted_comment_prefix = "trusted comment: ";
+ if (!boost::algorithm::starts_with(sig_lines[0], untrusted_comment_prefix))
+ FAIL("Malformed untrusted comment.");
+ if (!boost::algorithm::starts_with(sig_lines[2], trusted_comment_prefix))
+ FAIL("Malformed trusted comment.");
+
+ // We only _really_ care about the second line which is the signature of the file (b64-encoded)
+ // Also handling the other signature is nice, but not really required.
+ const std::string& msg_sig = sig_lines[1];
+
+ size_t bin_len = 0;
+ if (sodium_base642bin((unsigned char*)&sig, sizeof sig, msg_sig.c_str(), msg_sig.size(), NULL, &bin_len, NULL, sodium_base64_VARIANT_ORIGINAL) != 0 || bin_len != sizeof sig)
+ FAIL("Failed to decode base64 sig.");
+
+ cassert(sizeof pk.keynum == sizeof sig.keynum);
+
+ if (memcmp(&pk.keynum, &sig.keynum, sizeof sig.keynum) != 0)
+ continue; // mismatched key, try another one
+
+ if (memcmp(&sig.sig_alg, "ED", 2) != 0)
+ FAIL("Only hashed minisign signatures are supported.");
+
+ // Signature matches our public key
+
+ // Now verify the global signature (sig || trusted_comment)
+
+ unsigned char global_sig[crypto_sign_BYTES];
+ if (sodium_base642bin(global_sig, sizeof global_sig, sig_lines[3].c_str(), sig_lines[3].size(), NULL, &bin_len, NULL, sodium_base64_VARIANT_ORIGINAL) != 0 || bin_len != sizeof global_sig)
+ FAIL("Failed to decode base64 global_sig.");
+
+ const std::string trusted_comment = sig_lines[2].substr(trusted_comment_prefix.size());
+
+ unsigned char* sig_and_trusted_comment = (unsigned char*)sodium_malloc((sizeof sig.sig) + trusted_comment.size());
+ if (!sig_and_trusted_comment)
+ FAIL("sodium_malloc failed.");
+
+ memcpy(sig_and_trusted_comment, sig.sig, sizeof sig.sig);
+ memcpy(sig_and_trusted_comment + sizeof sig.sig, trusted_comment.data(), trusted_comment.size());
+
+ if (crypto_sign_verify_detached(global_sig, sig_and_trusted_comment, (sizeof sig.sig) + trusted_comment.size(), pk.pk) != 0)
+ {
+ err = "Failed to verify global signature.";
+ sodium_free(sig_and_trusted_comment);
+ return false;
+ }
+
+ sodium_free(sig_and_trusted_comment);
+
+ // Valid global sig, and the keynum matches the real one
+ return true;
+ }
+
+ return false;
+#undef CLEANUP
+}
+
+#undef FAIL
Property changes on: ps/trunk/source/ps/ModIo.cpp
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/source/ps/ModIo.h
===================================================================
--- ps/trunk/source/ps/ModIo.h (nonexistent)
+++ ps/trunk/source/ps/ModIo.h (revision 21759)
@@ -0,0 +1,208 @@
+/* Copyright (C) 2018 Wildfire Games.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef INCLUDED_MODIO
+#define INCLUDED_MODIO
+
+#include "lib/external_libraries/curl.h"
+#include "scriptinterface/ScriptInterface.h"
+
+#include
+#include
+
+// TODO: Allocate instance of the below two using sodium_malloc?
+struct PKStruct
+{
+ unsigned char sig_alg[2] = {}; // == "Ed"
+ unsigned char keynum[8] = {}; // should match the keynum in the sigstruct, else this is the wrong key
+ unsigned char pk[crypto_sign_PUBLICKEYBYTES] = {};
+};
+
+struct SigStruct
+{
+ unsigned char sig_alg[2] = {}; // "ED" (since we only support the hashed mode)
+ unsigned char keynum[8] = {}; // should match the keynum in the PKStruct
+ unsigned char sig[crypto_sign_BYTES] = {};
+};
+
+struct ModIoModData
+{
+ std::map properties;
+ std::vector dependencies;
+ SigStruct sig;
+};
+
+enum class DownloadProgressStatus {
+ NONE, // Default state
+ GAMEID, // The game ID is being downloaded
+ READY, // The game ID has been downloaded
+ LISTING, // The mod list is being downloaded
+ LISTED, // The mod list has been downloaded
+ DOWNLOADING, // A mod file is being downloaded
+ SUCCESS, // A mod file has been downloaded
+
+ FAILED_GAMEID, // Game ID couldn't be retrieved
+ FAILED_LISTING, // Mod list couldn't be retrieved
+ FAILED_DOWNLOADING, // File couldn't be retrieved
+ FAILED_FILECHECK // The file is corrupted
+};
+
+struct DownloadProgressData
+{
+ DownloadProgressStatus status;
+ double progress;
+ std::string error;
+};
+
+struct DownloadCallbackData;
+
+/**
+ * mod.io API interfacing code.
+ *
+ * Overview
+ *
+ * This class interfaces with a remote API provider that returns a list of mod files.
+ * These can then be downloaded after some cursory checking of well-formedness of the returned
+ * metadata.
+ * Downloaded files are checked for well formedness by validating that they fit the size and hash
+ * indicated by the API, then we check if the file is actually signed by a trusted key, and only
+ * if all of that is success the file is actually possible to be loaded as a mod.
+ *
+ * Security considerations
+ *
+ * This both distrusts the loaded JS mods, and the API as much as possible.
+ * We do not want a malicious mod to use this to download arbitrary files, nor do we want the API
+ * to make us download something we have not verified.
+ * Therefore we only allow mods to download one of the mods returned by this class (using indices).
+ *
+ * This (mostly) necessitates parsing the API responses here, as opposed to in JS.
+ * One could alternatively parse the responses in a locked down JS context, but that would require
+ * storing that code in here, or making sure nobody can overwrite it. Also this would possibly make
+ * some of the needed accesses for downloading and verifying files a bit more complicated.
+ *
+ * Everything downloaded from the API has its signature verified against our public key.
+ * This is a requirement, as otherwise a compromise of the API would result in users installing
+ * possibly malicious files.
+ * So a compromised API can just serve old files that we signed, so in that case there would need
+ * to be an issue in that old file that was missed.
+ *
+ * To limit the extend to how old those files could be the signing key should be rotated
+ * regularly (e.g. every release). To allow old versions of the engine to still use the API
+ * files can be signed by both the old and the new key for some amount of time, that however
+ * only makes sense in case a mod is compatible with both engine versions.
+ *
+ * Note that this does not prevent all possible attacks a package manager/update system should
+ * defend against. This is intentionally not an update system since proper package managers already
+ * exist. However there is some possible overlap in attack vectors and these should be evalutated
+ * whether they apply and to what extend we can fix that on our side (or how to get the API provider
+ * to help us do so). For a list of some possible issues see:
+ * https://github.com/theupdateframework/specification/blob/master/tuf-spec.md
+ *
+ * The mod.io settings are also locked down such that only mods that have been authorized by us
+ * show up in API queries. This is both done so that all required information (dependencies)
+ * are stored for the files, and that only mods that have been checked for being ok are actually
+ * shown to users.
+ */
+class ModIo
+{
+ NONCOPYABLE(ModIo);
+public:
+ ModIo();
+ ~ModIo();
+
+ // Async requests
+ void StartGetGameId();
+ void StartListMods();
+ void StartDownloadMod(size_t idx);
+
+ /**
+ * Advance the current async request and perform final steps if the download is complete.
+ *
+ * @param scriptInterface used for parsing the data and possibly install the mod.
+ * @return true if the download is complete (successful or not), false otherwise.
+ */
+ bool AdvanceRequest(const ScriptInterface& scriptInterface);
+
+ /**
+ * Cancel the current async request and clean things up
+ */
+ void CancelRequest();
+
+ const std::vector& GetMods() const
+ {
+ return m_ModData;
+ }
+ const DownloadProgressData& GetDownloadProgress() const
+ {
+ return m_DownloadProgressData;
+ }
+
+private:
+ static size_t ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp);
+ static size_t DownloadCallback(void* buffer, size_t size, size_t nmemb, void* userp);
+ static int DownloadProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
+
+ CURLMcode SetupRequest(const std::string& url, bool fileDownload);
+ void TearDownRequest();
+
+ bool ParseGameId(const ScriptInterface& scriptInterface, std::string& err);
+ bool ParseMods(const ScriptInterface& scriptInterface, std::string& err);
+
+ void DeleteDownloadedFile();
+ bool VerifyDownloadedFile(std::string& err);
+
+ // Utility methods for parsing mod.io responses and metadata
+ static bool ParseGameIdResponse(const ScriptInterface& scriptInterface, const std::string& responseData, int& id, std::string& err);
+ static bool ParseModsResponse(const ScriptInterface& scriptInterface, const std::string& responseData, std::vector& modData, const PKStruct& pk, std::string& err);
+ static bool ParseSignature(const std::vector& minisigs, SigStruct& sig, const PKStruct& pk, std::string& err);
+
+ // Url parts
+ std::string m_BaseUrl;
+ std::string m_GamesRequest;
+ std::string m_GameId;
+
+ // Query parameters
+ std::string m_ApiKey;
+ std::string m_IdQuery;
+
+ CURL* m_Curl;
+ CURLM* m_CurlMulti;
+ curl_slist* m_Headers;
+ char m_ErrorBuffer[CURL_ERROR_SIZE];
+ std::string m_ResponseData;
+
+ // Current mod download
+ int m_DownloadModID;
+ OsPath m_DownloadFilePath;
+ DownloadCallbackData* m_CallbackData;
+ DownloadProgressData m_DownloadProgressData;
+
+ PKStruct m_pk;
+
+ std::vector m_ModData;
+
+ friend class TestModIo;
+};
+
+extern ModIo* g_ModIo;
+
+#endif // INCLUDED_MODIO
Property changes on: ps/trunk/source/ps/ModIo.h
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/source/ps/UserReport.cpp
===================================================================
--- ps/trunk/source/ps/UserReport.cpp (revision 21758)
+++ ps/trunk/source/ps/UserReport.cpp (revision 21759)
@@ -1,629 +1,623 @@
/* Copyright (C) 2016 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.
*/
// Wait until the main thread wakes us up
while (true)
{
g_Profiler2.RecordRegionEnter("semaphore wait");
ENSURE(SDL_SemWait(m_WorkerSem) == 0);
g_Profiler2.RecordRegionLeave();
// 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(">>> CUserReporterWorker status: %s\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", 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.SetValueString(CFG_USER, "userreport.id", userID);
g_ConfigDB.WriteValueToFile(CFG_USER, "userreport.id", userID);
}
return userID;
}
bool CUserReporter::IsReportingEnabled()
{
int version = -1;
CFG_GET_VAL("userreport.enabledversion", version);
return (version >= REPORTER_VERSION);
}
void CUserReporter::SetReportingEnabled(bool enabled)
{
CStr val = CStr::FromInt(enabled ? REPORTER_VERSION : 0);
g_ConfigDB.SetValueString(CFG_USER, "userreport.enabledversion", val);
g_ConfigDB.WriteValueToFile(CFG_USER, "userreport.enabledversion", val);
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", 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_Mod.cpp
===================================================================
--- ps/trunk/source/ps/scripting/JSInterface_Mod.cpp (revision 21758)
+++ ps/trunk/source/ps/scripting/JSInterface_Mod.cpp (revision 21759)
@@ -1,62 +1,196 @@
/* Copyright (C) 2018 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 "ps/scripting/JSInterface_Mod.h"
+#include "JSInterface_Mod.h"
+#include "ps/CLogger.h"
#include "ps/Mod.h"
+#include "ps/ModIo.h"
extern void restart_engine();
JS::Value JSI_Mod::GetEngineInfo(ScriptInterface::CxPrivate* pCxPrivate)
{
return Mod::GetEngineInfo(*(pCxPrivate->pScriptInterface));
}
/**
* Returns a JS object containing a listing of available mods that
* have a modname.json file in their modname folder. The returned
* object looks like { modname1: json1, modname2: json2, ... } where
* jsonN is the content of the modnameN/modnameN.json file as a JS
* object.
*
* @return JS object with available mods as the keys of the modname.json
* properties.
*/
JS::Value JSI_Mod::GetAvailableMods(ScriptInterface::CxPrivate* pCxPrivate)
{
return Mod::GetAvailableMods(*(pCxPrivate->pScriptInterface));
}
void JSI_Mod::RestartEngine(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
restart_engine();
}
void JSI_Mod::SetMods(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::vector& mods)
{
g_modsLoaded = mods;
}
+void JSI_Mod::ModIoStartGetGameId(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
+{
+ if (!g_ModIo)
+ g_ModIo = new ModIo();
+
+ ENSURE(g_ModIo);
+
+ g_ModIo->StartGetGameId();
+}
+
+void JSI_Mod::ModIoStartListMods(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
+{
+ if (!g_ModIo)
+ {
+ LOGERROR("ModIoStartListMods called before ModIoStartGetGameId");
+ return;
+ }
+
+ g_ModIo->StartListMods();
+}
+
+void JSI_Mod::ModIoStartDownloadMod(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), uint32_t idx)
+{
+ if (!g_ModIo)
+ {
+ LOGERROR("ModIoStartDownloadMod called before ModIoStartGetGameId");
+ return;
+ }
+
+ g_ModIo->StartDownloadMod(idx);
+}
+
+bool JSI_Mod::ModIoAdvanceRequest(ScriptInterface::CxPrivate* pCxPrivate)
+{
+ if (!g_ModIo)
+ {
+ LOGERROR("ModIoAdvanceRequest called before ModIoGetMods");
+ return false;
+ }
+
+ ScriptInterface* scriptInterface = pCxPrivate->pScriptInterface;
+ return g_ModIo->AdvanceRequest(*scriptInterface);
+}
+
+void JSI_Mod::ModIoCancelRequest(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
+{
+ if (!g_ModIo)
+ {
+ LOGERROR("ModIoCancelRequest called before ModIoGetMods");
+ return;
+ }
+
+ g_ModIo->CancelRequest();
+}
+
+JS::Value JSI_Mod::ModIoGetMods(ScriptInterface::CxPrivate* pCxPrivate)
+{
+ if (!g_ModIo)
+ {
+ LOGERROR("ModIoGetMods called before ModIoStartGetGameId");
+ return JS::NullValue();
+ }
+
+ ScriptInterface* scriptInterface = pCxPrivate->pScriptInterface;
+ JSContext* cx = scriptInterface->GetContext();
+ JSAutoRequest rq(cx);
+
+ const std::vector& availableMods = g_ModIo->GetMods();
+
+ JS::RootedObject mods(cx, JS_NewArrayObject(cx, availableMods.size()));
+ if (!mods)
+ return JS::NullValue();
+
+ u32 i = 0;
+ for (const ModIoModData& mod : availableMods)
+ {
+ JS::RootedValue m(cx, JS::ObjectValue(*JS_NewPlainObject(cx)));
+ for (const std::pair& prop : mod.properties)
+ scriptInterface->SetProperty(m, prop.first.c_str(), prop.second, true);
+
+ scriptInterface->SetProperty(m, "dependencies", mod.dependencies, true);
+
+ JS_SetElement(cx, mods, i++, m);
+ }
+
+ return JS::ObjectValue(*mods);
+}
+
+const std::map statusStrings = {
+ { DownloadProgressStatus::NONE, "none" },
+ { DownloadProgressStatus::GAMEID, "gameid" },
+ { DownloadProgressStatus::READY, "ready" },
+ { DownloadProgressStatus::LISTING, "listing" },
+ { DownloadProgressStatus::LISTED, "listed" },
+ { DownloadProgressStatus::DOWNLOADING, "downloading" },
+ { DownloadProgressStatus::SUCCESS, "success" },
+ { DownloadProgressStatus::FAILED_GAMEID, "failed_gameid" },
+ { DownloadProgressStatus::FAILED_LISTING, "failed_listing" },
+ { DownloadProgressStatus::FAILED_DOWNLOADING, "failed_downloading" },
+ { DownloadProgressStatus::FAILED_FILECHECK, "failed_filecheck" }
+};
+
+JS::Value JSI_Mod::ModIoGetDownloadProgress(ScriptInterface::CxPrivate* pCxPrivate)
+{
+ if (!g_ModIo)
+ {
+ LOGERROR("ModIoGetDownloadProgress called before ModIoGetMods");
+ return JS::NullValue();
+ }
+
+ ScriptInterface* scriptInterface = pCxPrivate->pScriptInterface;
+ JSContext* cx = scriptInterface->GetContext();
+ JSAutoRequest rq(cx);
+
+ JS::RootedValue progressData(cx, JS::ObjectValue(*JS_NewPlainObject(cx)));
+ const DownloadProgressData& progress = g_ModIo->GetDownloadProgress();
+
+ scriptInterface->SetProperty(progressData, "status", statusStrings.at(progress.status), true);
+ scriptInterface->SetProperty(progressData, "progress", progress.progress, true);
+ scriptInterface->SetProperty(progressData, "error", progress.error, true);
+
+ return progressData;
+}
+
void JSI_Mod::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("GetEngineInfo");
scriptInterface.RegisterFunction("GetAvailableMods");
scriptInterface.RegisterFunction("RestartEngine");
scriptInterface.RegisterFunction, &JSI_Mod::SetMods>("SetMods");
+
+ scriptInterface.RegisterFunction("ModIoStartGetGameId");
+ scriptInterface.RegisterFunction("ModIoStartListMods");
+ scriptInterface.RegisterFunction("ModIoStartDownloadMod");
+ scriptInterface.RegisterFunction("ModIoAdvanceRequest");
+ scriptInterface.RegisterFunction("ModIoCancelRequest");
+ scriptInterface.RegisterFunction("ModIoGetMods");
+ scriptInterface.RegisterFunction("ModIoGetDownloadProgress");
}
Index: ps/trunk/source/ps/scripting/JSInterface_Mod.h
===================================================================
--- ps/trunk/source/ps/scripting/JSInterface_Mod.h (revision 21758)
+++ ps/trunk/source/ps/scripting/JSInterface_Mod.h (revision 21759)
@@ -1,35 +1,43 @@
/* Copyright (C) 2018 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 .
*/
#ifndef INCLUDED_JSI_MOD
#define INCLUDED_JSI_MOD
#include "ps/CStr.h"
#include "scriptinterface/ScriptInterface.h"
class ScriptInterface;
namespace JSI_Mod
{
void RegisterScriptFunctions(const ScriptInterface& scriptInterface);
JS::Value GetEngineInfo(ScriptInterface::CxPrivate* pCxPrivate);
JS::Value GetAvailableMods(ScriptInterface::CxPrivate* pCxPrivate);
void RestartEngine(ScriptInterface::CxPrivate* pCxPrivate);
void SetMods(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& mods);
+
+ void ModIoStartGetGameId(ScriptInterface::CxPrivate* pCxPrivate);
+ void ModIoStartListMods(ScriptInterface::CxPrivate* pCxPrivate);
+ void ModIoStartDownloadMod(ScriptInterface::CxPrivate* pCxPrivate, uint32_t idx);
+ bool ModIoAdvanceRequest(ScriptInterface::CxPrivate* pCxPrivate);
+ void ModIoCancelRequest(ScriptInterface::CxPrivate* pCxPrivate);
+ JS::Value ModIoGetMods(ScriptInterface::CxPrivate* pCxPrivate);
+ JS::Value ModIoGetDownloadProgress(ScriptInterface::CxPrivate* pCxPrivate);
}
#endif
Index: ps/trunk/source/ps/tests/test_ModIo.h
===================================================================
--- ps/trunk/source/ps/tests/test_ModIo.h (nonexistent)
+++ ps/trunk/source/ps/tests/test_ModIo.h (revision 21759)
@@ -0,0 +1,246 @@
+/* Copyright (C) 2018 Wildfire Games.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "lib/self_test.h"
+
+#include "ps/CLogger.h"
+#include "ps/ModIo.h"
+#include "scriptinterface/ScriptInterface.h"
+
+#include
+
+class TestModIo : public CxxTest::TestSuite
+{
+public:
+ void setUp()
+ {
+ if (sodium_init() < 0)
+ LOGERROR("failed to initialize libsodium");
+ }
+
+ // TODO: One could probably fuzz these parsing functions to
+ // make sure they handle malformed input nicely.
+
+ void test_id_parsing()
+ {
+ ScriptInterface script("Test", "Test", g_ScriptRuntime);
+
+#define TS_ASSERT_PARSE(input, expected_error, expected_id) \
+ { \
+ TestLogger logger; \
+ int id = -1; \
+ std::string err; \
+ TS_ASSERT(!ModIo::ParseGameIdResponse(script, input, id, err)); \
+ TS_ASSERT_STR_EQUALS(err, expected_error); \
+ TS_ASSERT_EQUALS(id, expected_id); \
+ }
+
+ // Various malformed inputs
+ TS_ASSERT_PARSE("", "Failed to parse response as JSON.", -1);
+ TS_ASSERT_PARSE("()", "Failed to parse response as JSON.", -1);
+ TS_ASSERT_PARSE("[]", "data property not an object.", -1);
+ TS_ASSERT_PARSE("null", "response not an object.", -1);
+ TS_ASSERT_PARSE("{}", "data property not an object.", -1);
+ TS_ASSERT_PARSE("{\"data\": null}", "data property not an object.", -1);
+ TS_ASSERT_PARSE("{\"data\": {}}", "data property not an array with at least one element.", -1);
+ TS_ASSERT_PARSE("{\"data\": []}", "data property not an array with at least one element.", -1);
+ TS_ASSERT_PARSE("{\"data\": [null]}", "First element not an object.", -1);
+ TS_ASSERT_PARSE("{\"data\": [false]}", "First element not an object.", -1);
+ TS_ASSERT_PARSE("{\"data\": [{}]}", "No id property in first element.", -1);
+ TS_ASSERT_PARSE("{\"data\": [[]]}", "No id property in first element.", -1);
+
+ // Various invalid IDs
+ TS_ASSERT_PARSE("{\"data\": [{\"id\": null}]}", "id property not a number.", -1);
+ TS_ASSERT_PARSE("{\"data\": [{\"id\": {}}]}", "id property not a number.", -1);
+ TS_ASSERT_PARSE("{\"data\": [{\"id\": true}]}", "id property not a number.", -1);
+ TS_ASSERT_PARSE("{\"data\": [{\"id\": -12}]}", "Invalid id.", -1);
+ TS_ASSERT_PARSE("{\"data\": [{\"id\": 0}]}", "Invalid id.", -1);
+
+#undef TS_ASSERT_PARSE
+
+ // Correctly formed input
+ {
+ TestLogger logger;
+ int id = -1;
+ std::string err;
+ TS_ASSERT(ModIo::ParseGameIdResponse(script, "{\"data\": [{\"id\": 42}]}", id, err));
+ TS_ASSERT(err.empty());
+ TS_ASSERT_EQUALS(id, 42);
+ }
+ }
+
+ void test_mods_parsing()
+ {
+ ScriptInterface script("Test", "Test", g_ScriptRuntime);
+
+ PKStruct pk;
+
+ const std::string pk_str = "RWTA6VIoth2Q1PFLsRILr3G7NB+mwwO8BSGoXs63X6TQgNGM4cE8Pvd6";
+
+ size_t bin_len = 0;
+ if (sodium_base642bin((unsigned char*)&pk, sizeof pk, pk_str.c_str(), pk_str.size(), NULL, &bin_len, NULL, sodium_base64_VARIANT_ORIGINAL) != 0 || bin_len != sizeof pk)
+ LOGERROR("failed to decode base64 public key");
+
+#define TS_ASSERT_PARSE(input, expected_error) \
+ { \
+ TestLogger logger; \
+ std::vector mods; \
+ std::string err; \
+ TS_ASSERT(!ModIo::ParseModsResponse(script, input, mods, pk, err)); \
+ TS_ASSERT_STR_EQUALS(err, expected_error); \
+ TS_ASSERT_EQUALS(mods.size(), 0); \
+ }
+
+ TS_ASSERT_PARSE("", "Failed to parse response as JSON.");
+ TS_ASSERT_PARSE("()", "Failed to parse response as JSON.");
+ TS_ASSERT_PARSE("null", "response not an object.");
+ TS_ASSERT_PARSE("[]", "data property not an object.");
+ TS_ASSERT_PARSE("{}", "data property not an object.");
+ TS_ASSERT_PARSE("{\"data\": null}", "data property not an object.");
+ TS_ASSERT_PARSE("{\"data\": {}}", "data property not an array with at least one element.");
+ TS_ASSERT_PARSE("{\"data\": []}", "data property not an array with at least one element.");
+ TS_ASSERT_PARSE("{\"data\": [null]}", "Failed to get array element object.");
+ TS_ASSERT_PARSE("{\"data\": [false]}", "Failed to get array element object.");
+ TS_ASSERT_PARSE("{\"data\": [true]}", "Failed to get array element object.");
+ TS_ASSERT_PARSE("{\"data\": [{}]}", "Failed to get name from el.");
+ TS_ASSERT_PARSE("{\"data\": [[]]}", "Failed to get name from el.");
+ TS_ASSERT_PARSE("{\"data\": [{\"foo\":\"bar\"}]}", "Failed to get name from el.");
+
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":null}]}", "Failed to get name_id from el."); // also some script value conversion check warning
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":42}]}", "Failed to get name_id from el."); // no conversion warning, but converting numbers to strings and vice-versa seems ok
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":false}]}", "Failed to get name_id from el."); // also some script value conversion check warning
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":{}}]}", "Failed to get name_id from el."); // also some script value conversion check warning
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":[]}]}", "Failed to get name_id from el."); // also some script value conversion check warning
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"foobar\"}]}", "Failed to get name_id from el.");
+
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\"}]}", "modfile not an object.");
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":null}]}", "modfile not an object.");
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":[]}]}", "Failed to get version from modFile.");
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{}}]}", "Failed to get version from modFile.");
+
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":null}}]}", "Failed to get filesize from modFile."); // also some script value conversion check warning
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234}}]}", "Failed to get md5 from filehash.");
+
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234, \"filehash\":null}}]}", "Failed to get md5 from filehash.");
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234, \"filehash\":{}}}]}", "Failed to get md5 from filehash.");
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234, \"filehash\":{\"md5\":null}}}]}", "Failed to get binary_url from download."); // also some script value conversion check warning
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234, \"filehash\":{\"md5\":\"abc\"}}}]}", "Failed to get binary_url from download.");
+
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234, \"filehash\":{\"md5\":\"abc\"}, \"download\":null}}]}", "Failed to get binary_url from download."); // also some script value conversion check warning
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234, \"filehash\":{\"md5\":\"abc\"}, \"download\":{\"binary_url\":null}}}]}", "Failed to get metadata_blob from modFile."); // also some script value conversion check warning
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234, \"filehash\":{\"md5\":\"abc\"}, \"download\":{\"binary_url\":\"\"}}}]}", "Failed to get metadata_blob from modFile.");
+
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234, \"filehash\":{\"md5\":\"abc\"}, \"download\":{\"binary_url\":\"\"},\"metadata_blob\":null}}]}", "metadata_blob not decoded as an object.");
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234, \"filehash\":{\"md5\":\"abc\"}, \"download\":{\"binary_url\":\"\"},\"metadata_blob\":\"\"}}]}", "Failed to parse metadata_blob as JSON.");
+
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234, \"filehash\":{\"md5\":\"abc\"}, \"download\":{\"binary_url\":\"\"},\"metadata_blob\":\"{}\"}}]}", "Failed to get dependencies from metadata_blob.");
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234, \"filehash\":{\"md5\":\"abc\"}, \"download\":{\"binary_url\":\"\"},\"metadata_blob\":\"{\\\"dependencies\\\":null}\"}}]}", "Failed to get dependencies from metadata_blob.");
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234, \"filehash\":{\"md5\":\"abc\"}, \"download\":{\"binary_url\":\"\"},\"metadata_blob\":\"{\\\"dependencies\\\":[]}\"}}]}", "Failed to get minisigs from metadata_blob.");
+ TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234, \"filehash\":{\"md5\":\"abc\"}, \"download\":{\"binary_url\":\"\"},\"metadata_blob\":\"{\\\"dependencies\\\":[],\\\"minisigs\\\":null}\"}}]}", "Failed to get minisigs from metadata_blob.");
+
+#undef TS_ASSERT_PARSE
+
+ // Correctly formed input, but no signature matching the public key
+ // Thus all such mods/modfiles are not added, thus we get 0 parsed mods.
+ {
+ TestLogger logger;
+ std::vector mods;
+ std::string err;
+ TS_ASSERT(ModIo::ParseModsResponse(script, "{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234, \"filehash\":{\"md5\":\"abc\"}, \"download\":{\"binary_url\":\"\"},\"metadata_blob\":\"{\\\"dependencies\\\":[],\\\"minisigs\\\":[]}\"}}]}", mods, pk, err));
+ TS_ASSERT(err.empty());
+ TS_ASSERT_EQUALS(mods.size(), 0);
+ }
+
+ // Correctly formed input (with a signature matching the public key above, and a valid global signature)
+ {
+ TestLogger logger;
+ std::vector mods;
+ std::string err;
+ TS_ASSERT(ModIo::ParseModsResponse(script, "{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\",\"modfile\":{\"version\":\"\",\"filesize\":1234, \"filehash\":{\"md5\":\"abc\"}, \"download\":{\"binary_url\":\"\"},\"metadata_blob\":\"{\\\"dependencies\\\":[],\\\"minisigs\\\":[\\\"untrusted comment: signature from minisign secret key\\\\nRUTA6VIoth2Q1HUg5bwwbCUZPcqbQ/reLXqxiaWARH5PNcwxX5vBv/mLPLgdxGsIrOyK90763+rCVTmjeYx5BDz8C0CIbGZTNQs=\\\\ntrusted comment: timestamp:1517285433\\\\tfile:tm.zip\\\\nTHwNMhK4Ogj6XA4305p1K9/ouP/DrxPcDFrPaiu+Ke6/WGlHIzBZHvmHWUedvsK6dzL31Gk8YNzscKWnZqWNCw==\\\"]}\"}}]}", mods, pk, err));
+ TS_ASSERT(err.empty());
+ TS_ASSERT_EQUALS(mods.size(), 1);
+ }
+ }
+
+ void test_signature_parsing()
+ {
+ PKStruct pk;
+
+ const std::string pk_str = "RWTA6VIoth2Q1PFLsRILr3G7NB+mwwO8BSGoXs63X6TQgNGM4cE8Pvd6";
+
+ size_t bin_len = 0;
+ if (sodium_base642bin((unsigned char*)&pk, sizeof pk, pk_str.c_str(), pk_str.size(), NULL, &bin_len, NULL, sodium_base64_VARIANT_ORIGINAL) != 0 || bin_len != sizeof pk)
+ LOGERROR("failed to decode base64 public key");
+
+
+ // No invalid signature at all (silent failure)
+#define TS_ASSERT_PARSE_SILENT_FAILURE(input) \
+ { \
+ TestLogger logger; \
+ SigStruct sig; \
+ std::string err; \
+ TS_ASSERT(!ModIo::ParseSignature(input, sig, pk, err)); \
+ TS_ASSERT(err.empty()); \
+ }
+
+#define TS_ASSERT_PARSE(input, expected_error) \
+ { \
+ TestLogger logger; \
+ SigStruct sig; \
+ std::string err; \
+ TS_ASSERT(!ModIo::ParseSignature({input}, sig, pk, err)); \
+ TS_ASSERT_STR_EQUALS(err, expected_error); \
+ }
+
+ TS_ASSERT_PARSE_SILENT_FAILURE({});
+
+ TS_ASSERT_PARSE("", "Invalid (too short) sig.");
+
+ TS_ASSERT_PARSE("\nRUTA6VIoth2Q1HUg5bwwbCUZPcqbQ/reLXqxiaWARH5PNcwxX5vBv/mLPLgdxGsIrOyK90763+rCVTmjeYx5BDz8C0CIbGZTNQs=\n\nTHwNMhK4Ogj6XA4305p1K9/ouP/DrxPcDFrPaiu+Ke6/WGlHIzBZHvmHWUedvsK6dzL31Gk8YNzscKWnZqWNCw==", "Malformed untrusted comment.");
+ TS_ASSERT_PARSE("unturusted comment: \nRUTA6VIoth2Q1HUg5bwwbCUZPcqbQ/reLXqxiaWARH5PNcwxX5vBv/mLPLgdxGsIrOyK90763+rCVTmjeYx5BDz8C0CIbGZTNQs=\n\nTHwNMhK4Ogj6XA4305p1K9/ouP/DrxPcDFrPaiu+Ke6/WGlHIzBZHvmHWUedvsK6dzL31Gk8YNzscKWnZqWNCw==", "Malformed untrusted comment.");
+ TS_ASSERT_PARSE("untrusted comment: \nRUTA6VIoth2Q1HUg5bwwbCUZPcqbQ/reLXqxiaWARH5PNcwxX5vBv/mLPLgdxGsIrOyK90763+rCVTmjeYx5BDz8C0CIbGZTNQs=\n\nTHwNMhK4Ogj6XA4305p1K9/ouP/DrxPcDFrPaiu+Ke6/WGlHIzBZHvmHWUedvsK6dzL31Gk8YNzscKWnZqWNCw==", "Malformed trusted comment.");
+ TS_ASSERT_PARSE("untrusted comment: \nRUTA6VIoth2Q1HUg5bwwbCUZPcqbQ/reLXqxiaWARH5PNcwxX5vBv/mLPLgdxGsIrOyK90763+rCVTmjeYx5BDz8C0CIbGZTNQs=\ntrusted comment:\nTHwNMhK4Ogj6XA4305p1K9/ouP/DrxPcDFrPaiu+Ke6/WGlHIzBZHvmHWUedvsK6dzL31Gk8YNzscKWnZqWNCw==", "Malformed trusted comment.");
+
+ TS_ASSERT_PARSE("untrusted comment: \n\ntrusted comment: \n", "Failed to decode base64 sig.");
+ TS_ASSERT_PARSE("untrusted comment: \nZm9vYmFyCg==\ntrusted comment: \n", "Failed to decode base64 sig.");
+ TS_ASSERT_PARSE("untrusted comment: \nRWTA6VIoth2Q1HUg5bwwbCUZPcqbQ/reLXqxiaWARH5PNcwxX5vBv/mLPLgdxGsIrOyK90763+rCVTmjeYx5BDz8C0CIbGZTNQs=\ntrusted comment: \n", "Only hashed minisign signatures are supported.");
+
+ // Silent failure again this one has the wrong keynum
+ TS_ASSERT_PARSE_SILENT_FAILURE({"untrusted comment: \nRUTA5VIoth2Q1HUg5bwwbCUZPcqbQ/reLXqxiaWARH5PNcwxX5vBv/mLPLgdxGsIrOyK90763+rCVTmjeYx5BDz8C0CIbGZTNQs=\ntrusted comment: \n"});
+
+ TS_ASSERT_PARSE("untrusted comment: \nRUTA6VIoth2Q1HUg5bwwbCUZPcqbQ/reLXqxiaWARH5PNcwxX5vBv/mLPLgdxGsIrOyK90763+rCVTmjeYx5BDz8C0CIbGZTNQs=\ntrusted comment: \n", "Failed to decode base64 global_sig.");
+
+ TS_ASSERT_PARSE("untrusted comment: \nRUTA6VIoth2Q1HUg5bwwbCUZPcqbQ/reLXqxiaWARH5PNcwxX5vBv/mLPLgdxGsIrOyK90763+rCVTmjeYx5BDz8C0CIbGZTNQs=\ntrusted comment: timestamp:1517285433\tfile:tm.zip\nAHwNMhK4Ogj6XA4305p1K9/ouP/DrxPcDFrPaiu+Ke6/WGlHIzBZHvmHWUedvsK6dzL31Gk8YNzscKWnZqWNCw==", "Failed to verify global signature.");
+
+ // Valid signature
+ {
+ TestLogger logger;
+ SigStruct sig;
+ std::string err;
+ TS_ASSERT(ModIo::ParseSignature({"untrusted comment: \nRUTA6VIoth2Q1HUg5bwwbCUZPcqbQ/reLXqxiaWARH5PNcwxX5vBv/mLPLgdxGsIrOyK90763+rCVTmjeYx5BDz8C0CIbGZTNQs=\ntrusted comment: timestamp:1517285433\tfile:tm.zip\nTHwNMhK4Ogj6XA4305p1K9/ouP/DrxPcDFrPaiu+Ke6/WGlHIzBZHvmHWUedvsK6dzL31Gk8YNzscKWnZqWNCw=="}, sig, pk, err));
+ TS_ASSERT(err.empty());
+ }
+
+#undef TS_ASSERT_PARSE_SILENT_FAILURE
+#undef TS_ASSERT_PARSE
+ }
+};
Property changes on: ps/trunk/source/ps/tests/test_ModIo.h
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property