Index: ps/trunk/binaries/data/config/default.cfg
===================================================================
--- ps/trunk/binaries/data/config/default.cfg (revision 17729)
+++ ps/trunk/binaries/data/config/default.cfg (revision 17730)
@@ -1,402 +1,403 @@
; 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. *
; * *
; * 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
; Enable/disable the splashscreen
splashscreendisable = false
; Splashscreen version (date of last modification). By default, 0 to force splashscreen to appear at first launch.
splashscreenversion = 0
; 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
waterugly=false; 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
shadowpcf = true
vsync = false
particles = 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 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"
[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 = "Alt+F4", "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+W" ; Toggle wireframe mode
silhouettes = "Alt+S" ; Toggle unit silhouettes
showsky = "Alt+Z" ; Toggle sky
; > 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
session.gui.toggle = "Alt+G" ; Toggle visibility of session GUI
menu.toggle = "F10" ; Toggle in-game menu
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
; > HOTKEYS ONLY
chat = Return ; Toggle chat window
teamchat = "T" ; Toggle chat window in team chat mode
; > 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
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
milonly = Alt ; Add only military units to selection
idleonly = "I" ; Select only idle 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
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
attack = Ctrl ; Modifier to attack instead of another action (eg 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
queue = Shift ; Modifier to queue unit orders instead of replacing
batchtrain = Shift ; Modifier to train units in batches
massbarter = Shift ; Modifier to barter bunch of resources
masstribute = Shift ; Modifier to tribute bunch of resources
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.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.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.menu]
limitfps = true ; Limit FPS in the menus and loading screen
[gui.session]
attacknotificationmessage = true ; Show attack notification messages
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
[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
[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
[lobby]
chattimestamp = false ; Show time chat message was posted
history = 0 ; Number of past messages to display on join
room = "arena20" ; Default MUC room to join
server = "lobby.wildfiregames.com" ; Address of lobby server
xpartamupp = "wfgbot20" ; Name of the server-side xmpp client that manage games
[mod]
enabledmods = "mod public"
[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)
script.enable = false ; Enable Javascript profiling. Needs to be set before startup and can't be changed later. (default off for 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
[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/public/gui/common/functions_global_object.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/functions_global_object.js (revision 17729)
+++ ps/trunk/binaries/data/mods/public/gui/common/functions_global_object.js (revision 17730)
@@ -1,142 +1,179 @@
-/*
- DESCRIPTION : Contains global GUI functions, which will later be accessible from every GUI script/file.
- NOTES : So far, only the message box-related functions are implemented.
-*/
+/**
+ * Contains global GUI functions accessible from every GUI script/file.
+ */
// *******************************************
// messageBox
// *******************************************
// @params: int mbWidth, int mbHeight, string mbMessage, string mbTitle, int mbMode, arr mbButtonCaptions, function mbBtnCode, var mbCallbackArgs
// @return: void
// @desc: Displays a new modal message box.
// *******************************************
// We want to pass callback functions for the different buttons in a convenient way.
// Because passing functions accross compartment boundaries is a pain, we just store them here together with some optional arguments.
// The messageBox page will return the code of the pressed button and the according function will be called.
var g_messageBoxBtnFunctions = [];
var g_messageBoxCallbackArgs = [];
var g_messageBoxCallbackFunction = function(btnCode)
{
if (btnCode !== undefined && g_messageBoxBtnFunctions[btnCode])
{
// Cache the variables to make it possible to call a messageBox from a callback function.
var callbackFunction = g_messageBoxBtnFunctions[btnCode];
var callbackArgs = g_messageBoxCallbackArgs[btnCode];
g_messageBoxBtnFunctions = [];
g_messageBoxCallbackArgs = [];
if (callbackArgs !== undefined)
callbackFunction(callbackArgs);
else
callbackFunction();
return;
}
g_messageBoxBtnFunctions = [];
g_messageBoxCallbackArgs = [];
};
function messageBox (mbWidth, mbHeight, mbMessage, mbTitle, mbMode, mbButtonCaptions, mbBtnCode, mbCallbackArgs)
{
if (g_messageBoxBtnFunctions && g_messageBoxBtnFunctions.length != 0)
{
warn("A messagebox was called when a previous callback function is still set, aborting!");
return;
}
g_messageBoxBtnFunctions = mbBtnCode;
if (mbCallbackArgs)
g_messageBoxCallbackArgs = mbCallbackArgs;
var initData = {
width: mbWidth,
height: mbHeight,
message: mbMessage,
title: mbTitle,
mode: mbMode,
buttonCaptions: mbButtonCaptions,
};
if (mbBtnCode)
initData.callback = "g_messageBoxCallbackFunction";
Engine.PushGuiPage("page_msgbox.xml", initData);
}
// ====================================================================
function openURL(url)
{
Engine.OpenURL(url);
messageBox(600, 200, sprintf(translate("Opening %(url)s\n in default web browser. Please wait...."), { url: url }), translate("Opening page"), 2);
}
function updateCounters()
{
var caption = "";
var linesCount = 0;
var researchCount = 0;
if (Engine.ConfigDB_GetValue("user", "overlay.fps") === "true")
{
caption += sprintf(translate("FPS: %(fps)4s"), { fps: Engine.GetFPS() }) + "\n";
++linesCount;
}
if (Engine.ConfigDB_GetValue("user", "overlay.realtime") === "true")
{
caption += (new Date()).toLocaleTimeString() + "\n";
++linesCount;
}
// If game has been started
if (typeof g_SimState != "undefined")
{
if (Engine.ConfigDB_GetValue("user", "gui.session.timeelapsedcounter") === "true")
{
var currentSpeed = Engine.GetSimRate();
if (currentSpeed != 1.0)
// Translation: The "x" means "times", with the mathematical meaning of multiplication.
caption += sprintf(translate("%(time)s (%(speed)sx)"),
{
time: timeToString(g_SimState.timeElapsed),
speed: Engine.FormatDecimalNumberIntoString(currentSpeed)
}
);
else
caption += timeToString(g_SimState.timeElapsed);
caption += "\n";
++linesCount;
}
var diplomacyCeasefireCounter = Engine.GetGUIObjectByName("diplomacyCeasefireCounter");
if (g_SimState.ceasefireActive)
{
// Update ceasefire counter in the diplomacy window
var remainingTimeString = timeToString(g_SimState.ceasefireTimeRemaining);
diplomacyCeasefireCounter.caption = sprintf(translateWithContext("ceasefire", "Time remaining until ceasefire is over: %(time)s."), {"time": remainingTimeString});
// Update ceasefire overlay counter
if (Engine.ConfigDB_GetValue("user", "gui.session.ceasefirecounter") === "true")
{
caption += remainingTimeString + "\n";
++linesCount;
}
}
else if (!diplomacyCeasefireCounter.hidden)
{
diplomacyCeasefireCounter.hidden = true;
updateDiplomacy();
}
g_ResearchListTop = 4;
if (linesCount)
g_ResearchListTop += 14 * linesCount;
}
var dataCounter = Engine.GetGUIObjectByName("dataCounter");
dataCounter.caption = caption;
dataCounter.size = sprintf("100%%-100 40 100%%-5 %(bottom)s", { bottom: 40 + 14 * linesCount });
+ dataCounter.hidden = linesCount == 0;
+}
+
+/**
+ * Update the overlay with the most recent network warning of each client.
+ */
+function displayGamestateNotifications()
+{
+ let messages = [];
+ let maxTextWidth = 0;
+
+ // TODO: Players who paused the game should be added here
+
+ // Add network warnings
+ if (Engine.ConfigDB_GetValue("user", "overlay.netwarnings") == "true")
+ {
+ let netwarnings = getNetworkWarnings();
+ messages = messages.concat(netwarnings.messages);
+ maxTextWidth = Math.max(maxTextWidth, netwarnings.maxTextWidth);
+ }
+
+ // Resize textbox
+ let width = maxTextWidth + 20;
+ let height = 14 * messages.length;
+
+ // Position left of the dataCounter
+ let top = "40";
+ let right = Engine.GetGUIObjectByName("dataCounter").hidden ? "100%-15" : "100%-110";
+
+ let bottom = top + "+" + height;
+ let left = right + "-" + width;
+
+ let gameStateNotifications = Engine.GetGUIObjectByName("gameStateNotifications");
+ gameStateNotifications.caption = messages.join("\n");
+ gameStateNotifications.hidden = !messages.length;
+ gameStateNotifications.size = left + " " + top + " " + right + " " + bottom;
+
+ setTimeout(displayGamestateNotifications, 1000);
}
Index: ps/trunk/binaries/data/mods/public/gui/common/global.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/global.xml (revision 17729)
+++ ps/trunk/binaries/data/mods/public/gui/common/global.xml (revision 17730)
@@ -1,95 +1,113 @@
Index: ps/trunk/binaries/data/mods/public/gui/common/network.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/network.js (revision 17729)
+++ ps/trunk/binaries/data/mods/public/gui/common/network.js (revision 17730)
@@ -1,96 +1,200 @@
+/**
+ * Number of milliseconds to display network warnings.
+ */
+const g_NetworkWarningTimeout = 3000;
+
+/**
+ * Currently displayed network warnings. At most one message per user.
+ */
+var g_NetworkWarnings = {};
+
+/**
+ * Message-types to be displayed.
+ */
+var g_NetworkWarningTexts = {
+
+ "server-timeout": (msg, username) =>
+ sprintf(translate("Losing connection to server (%(seconds)s)"), {
+ "seconds": Math.ceil(msg.lastReceivedTime / 1000)
+ }),
+
+ "client-timeout": (msg, username) =>
+ sprintf(translate("%(player)s losing connection (%(seconds)s)"), {
+ "player": username,
+ "seconds": Math.ceil(msg.lastReceivedTime / 1000)
+ }),
+
+ "server-latency": (msg, username) =>
+ sprintf(translate("Bad connection to server (%(milliseconds)sms)"), {
+ "milliseconds": msg.meanRTT
+ }),
+
+ "client-latency": (msg, username) =>
+ sprintf(translate("Bad connection to %(player)s (%(milliseconds)sms)"), {
+ "player": username,
+ "milliseconds": msg.meanRTT
+ })
+};
+
var g_NetworkCommands = {
"/kick": argument => kickPlayer(argument, false),
"/ban": argument => kickPlayer(argument, true),
"/list": argument => addChatMessage({ "type": "clientlist" }),
"/clear": argument => clearChatMessages()
};
/**
* Must be kept in sync with source/network/NetHost.h
*/
function getDisconnectReason(id)
{
switch (id)
{
case 0: return translate("Unknown reason");
case 1: return translate("The host has ended the game");
case 2: return translate("Incorrect network protocol version");
case 3: return translate("Game is loading, please try later");
case 4: return translate("Game has already started, no observers allowed");
case 5: return translate("You have been kicked");
case 6: return translate("You have been banned");
default:
warn("Unknown disconnect-reason ID received: " + id);
return sprintf(translate("\\[Invalid value %(id)s]"), { "id": id });
}
}
/**
* Show the disconnect reason in a message box.
*
* @param {number} reason
*/
function reportDisconnect(reason)
{
// Translation: States the reason why the client disconnected from the server.
let reasonText = sprintf(translate("Reason: %(reason)s."), { "reason": getDisconnectReason(reason) });
messageBox(400, 200, translate("Lost connection to the server.") + "\n\n" + reasonText, translate("Disconnected"), 2);
}
function kickPlayer(username, ban)
{
if (!Engine.KickPlayer(username, ban))
addChatMessage({
"type": "system",
"text": sprintf(ban ? translate("Could not ban %(name)s.") : translate("Could not kick %(name)s."), {
"name": username
})
});
}
/**
* Get a colorized list of usernames sorted by player slot, observers last.
* Requires g_PlayerAssignments and colorizePlayernameByGUID.
*
* @returns {string}
*/
function getUsernameList()
{
let usernames = Object.keys(g_PlayerAssignments).sort((guidA, guidB) => {
let playerIdA = g_PlayerAssignments[guidA].player;
let playerIdB = g_PlayerAssignments[guidB].player;
// Sort observers last
if (playerIdA == -1) return +1;
if (playerIdB == -1) return -1;
// Sort players
return playerIdA - playerIdB;
}).map(guid => colorizePlayernameByGUID(guid));
return sprintf(translate("Users: %(users)s"),
// Translation: This comma is used for separating first to penultimate elements in an enumeration.
{ "users": usernames.join(translate(", ")) });
}
/**
* Execute a command locally. Requires addChatMessage.
*
* @param {string} input
* @returns {Boolean} whether a command was executed
*/
function executeNetworkCommand(input)
{
if (input.indexOf("/") != 0)
return false;
let command = input.split(" ", 1)[0];
let argument = input.substr(command.length + 1);
if (g_NetworkCommands[command])
g_NetworkCommands[command](argument);
return !!g_NetworkCommands[command];
}
+
+/**
+ * Remember this warning for a few seconds.
+ * Overwrite previous warnings for this user.
+ *
+ * @param msg - GUI message sent by NetServer or NetClient
+ */
+function addNetworkWarning(msg)
+{
+ if (!g_NetworkWarningTexts[msg.warntype])
+ {
+ warn("Unknown network warning type received: " + uneval(msg));
+ return;
+ }
+
+ if (Engine.ConfigDB_GetValue("user", "overlay.netwarnings") != "true")
+ return;
+
+ g_NetworkWarnings[msg.guid || "server"] = {
+ "added": Date.now(),
+ "msg": msg
+ };
+}
+
+/**
+ * Colorizes and concatenates all network warnings.
+ * Returns text and textWidth.
+ */
+function getNetworkWarnings()
+{
+ // Remove outdated messages
+ for (let guid in g_NetworkWarnings)
+ {
+ if (Date.now() > g_NetworkWarnings[guid].added + g_NetworkWarningTimeout)
+ delete g_NetworkWarnings[guid];
+
+ if (guid != "server" && !g_PlayerAssignments[guid])
+ delete g_NetworkWarnings[guid];
+ }
+
+ // Show local messages first
+ let guids = Object.keys(g_NetworkWarnings).sort(guid => guid != "server");
+
+ let font = Engine.GetGUIObjectByName("gameStateNotifications").font;
+
+ let messages = [];
+ let maxTextWidth = 0;
+
+ for (let guid of guids)
+ {
+ let msg = g_NetworkWarnings[guid].msg;
+
+ // Add formatted text
+ messages.push(g_NetworkWarningTexts[msg.warntype](msg, colorizePlayernameByGUID(guid)));
+
+ // Add width of unformatted text
+ let username = guid != "server" && g_PlayerAssignments[guid].name;
+ let textWidth = Engine.GetTextWidth(font, g_NetworkWarningTexts[msg.warntype](msg, username));
+ maxTextWidth = Math.max(textWidth, maxTextWidth);
+ }
+
+ return {
+ "messages": messages,
+ "maxTextWidth": maxTextWidth
+ };
+}
Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 17729)
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 17730)
@@ -1,1779 +1,1784 @@
const g_MatchSettings_SP = "config/matchsettings.json";
const g_MatchSettings_MP = "config/matchsettings.mp.json";
const g_Ceasefire = prepareForDropdown(g_Settings ? g_Settings.Ceasefire : undefined);
const g_GameSpeeds = prepareForDropdown(g_Settings ? g_Settings.GameSpeeds.filter(speed => !speed.ReplayOnly) : undefined);
const g_MapSizes = prepareForDropdown(g_Settings ? g_Settings.MapSizes : undefined);
const g_MapTypes = prepareForDropdown(g_Settings ? g_Settings.MapTypes : undefined);
const g_PopulationCapacities = prepareForDropdown(g_Settings ? g_Settings.PopulationCapacities : undefined);
const g_StartingResources = prepareForDropdown(g_Settings ? g_Settings.StartingResources : undefined);
const g_VictoryConditions = prepareForDropdown(g_Settings ? g_Settings.VictoryConditions : undefined);
/**
* All selectable playercolors except gaia.
*/
const g_PlayerColors = g_Settings ? g_Settings.PlayerDefaults.slice(1).map(pData => pData.Color) : undefined;
/**
* Directory containing all maps of the given type.
*/
const g_MapPath = {
"random": "maps/random/",
"scenario": "maps/scenarios/",
"skirmish": "maps/skirmishes/"
};
/**
* Processes a CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer.
*/
const g_NetMessageTypes = {
"netstatus": msg => handleNetStatusMessage(msg),
+ "netwarn": msg => addNetworkWarning(msg),
"gamesetup": msg => handleGamesetupMessage(msg),
"players": msg => handlePlayerAssignmentMessage(msg),
"ready": msg => handleReadyMessage(msg),
"start": msg => handleGamestartMessage(msg),
"kicked": msg => addChatMessage({ "type": "kicked", "username": msg.username }),
"banned": msg => addChatMessage({ "type": "banned", "username": msg.username }),
"chat": msg => addChatMessage({ "type": "chat", "guid": msg.guid, "text": msg.text })
};
const g_FormatChatMessage = {
"system": (msg, user) => systemMessage(msg.text),
"settings": (msg, user) => systemMessage(translate('Game settings have been changed')),
"connect": (msg, user) => systemMessage(sprintf(translate("%(username)s has joined"), { "username": user })),
"disconnect": (msg, user) => systemMessage(sprintf(translate("%(username)s has left"), { "username": user })),
"kicked": (msg, user) => systemMessage(sprintf(translate("%(username)s has been kicked"), { "username": user })),
"banned": (msg, user) => systemMessage(sprintf(translate("%(username)s has been banned"), { "username": user })),
"chat": (msg, user) => sprintf(translate("%(username)s %(message)s"), {
"username": senderFont(sprintf(translate("<%(username)s>"), { "username": user })),
"message": escapeText(msg.text || "")
}),
"ready": (msg, user) => sprintf(translate("* %(username)s is ready!"), {
"username": user
}),
"not-ready": (msg, user) => sprintf(translate("* %(username)s is not ready."), {
"username": user
}),
"clientlist": (msg, user) => getUsernameList()
};
/**
* The dropdownlist items will appear in the order they are added.
*/
const g_MapFilters = [
{
"id": "default",
"name": translate("Default"),
"filter": mapKeywords => mapKeywords.every(keyword => ["naval", "demo", "hidden"].indexOf(keyword) == -1)
},
{
"id": "naval",
"name": translate("Naval Maps"),
"filter": mapKeywords => mapKeywords.indexOf("naval") != -1
},
{
"id": "demo",
"name": translate("Demo Maps"),
"filter": mapKeywords => mapKeywords.indexOf("demo") != -1
},
{
"id": "all",
"name": translate("All Maps"),
"filter": mapKeywords => true
}
];
/**
* Used for generating the botnames.
*/
const g_RomanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
/**
* Offer users to select playable civs only.
* Load unselectable civs as they could appear in scenario maps.
*/
const g_CivData = loadCivData();
/**
* Used for highlighting the sender of chat messages.
*/
const g_SenderFont = "sans-bold-13";
/**
* Highlight the "random" dropdownlist item.
*/
const g_ColorRandom = "orange";
/**
* Highlight AIs in the player-dropdownlist.
*/
const g_AIColor = "70 150 70";
/**
* Highlight unassigned players in the dropdownlist.
*/
const g_UnassignedColor = "140 140 140";
/**
* Highlight ready players.
*/
const g_ReadyColor = "green";
/**
* Highlights the victory condition in the game-description.
*/
const g_VictoryColor = "orange";
/**
* Placeholder item for the map-dropdownlist.
*/
const g_RandomMap = '[color="' + g_ColorRandom + '"]' + translateWithContext("map", "Random") + "[/color]";
/**
* Placeholder item for the civ-dropdownlists.
*/
const g_RandomCiv = '[color="' + g_ColorRandom + '"]' + translateWithContext("civilization", "Random") + '[/color]';
// Is this is a networked game, or offline
var g_IsNetworked;
// Is this user in control of game settings (i.e. is a network server, or offline player)
var g_IsController;
// Server name, if user is a server, connected to the multiplayer lobby
var g_ServerName;
// Are we currently updating the GUI in response to network messages instead of user input
// (and therefore shouldn't send further messages to the network)
var g_IsInGuiUpdate;
// Is this user ready
var g_IsReady;
// There are some duplicate orders on init, we can ignore these [bool].
var g_ReadyInit = true;
// If no one has changed ready status, we have no need to spam the settings changed message.
// 2 - Host's initial ready, suppressed settings message, 1 - Will show settings message, <=0 - Suppressed settings message
var g_ReadyChanged = 2;
/**
* Used to prevent calling resetReadyData when starting a game.
*/
var g_GameStarted = false;
var g_PlayerAssignments = {};
var g_DefaultPlayerData = [];
var g_GameAttributes = { "settings": {} };
var g_ChatMessages = [];
/**
* Cache containing the mapsettings for scenario/skirmish maps. Just-in-time loading.
*/
var g_MapData = {};
// To prevent the display locking up while we load the map metadata,
// we'll start with a 'loading' message and switch to the main screen in the
// tick handler
var g_LoadingState = 0; // 0 = not started, 1 = loading, 2 = loaded
/**
* Initializes some globals without touching the GUI.
*
* @param {Object} attribs - context data sent by the lobby / mainmenu
*/
function init(attribs)
{
if (!g_Settings)
{
cancelSetup();
return;
}
if (["offline", "server", "client"].indexOf(attribs.type) == -1)
{
error("Unexpected 'type' in gamesetup init: " + attribs.type);
cancelSetup();
return;
}
g_IsNetworked = attribs.type != "offline";
g_IsController = attribs.type != "client";
if (attribs.serverName)
g_ServerName = attribs.serverName;
// Get default player data - remove gaia
g_DefaultPlayerData = g_Settings.PlayerDefaults;
g_DefaultPlayerData.shift();
for (let i in g_DefaultPlayerData)
g_DefaultPlayerData[i].Civ = "random";
+
+ setTimeout(displayGamestateNotifications, 1000);
}
/**
* Called after the first tick.
*/
function initGUIObjects()
{
Engine.GetGUIObjectByName("cancelGame").tooltip = Engine.HasXmppClient() ? translate("Return to the lobby.") : translate("Return to the main menu.");
initCivNameList();
initMapTypes();
initMapFilters();
if (g_IsController)
{
// Unique ID to be used for identifying the same game reports for the lobby
g_GameAttributes.matchID = Engine.GetMatchID();
g_GameAttributes.settings.CheatsEnabled = !g_IsNetworked;
g_GameAttributes.settings.RatingEnabled = Engine.IsRankedGame() || undefined;
initMapNameList();
initNumberOfPlayers();
initGameSpeed();
initPopulationCaps();
initStartingResources();
initCeasefire();
initVictoryConditions();
initMapSizes();
initRadioButtons();
}
else
hideControls();
initMultiplayerSettings();
initPlayerAssignments();
resizeMoreOptionsWindow();
if (g_IsNetworked)
Engine.GetGUIObjectByName("chatInput").focus();
if (g_IsController)
{
loadPersistMatchSettings();
if (g_IsInGuiUpdate)
warn("initGUIObjects() called while in GUI update");
updateGameAttributes();
}
}
function initMapTypes()
{
let mapTypes = Engine.GetGUIObjectByName("mapTypeSelection");
mapTypes.list = g_MapTypes.Title;
mapTypes.list_data = g_MapTypes.Name;
mapTypes.onSelectionChange = function() {
if (this.selected != -1)
selectMapType(this.list_data[this.selected]);
};
if (g_IsController)
mapTypes.selected = g_MapTypes.Default;
}
function initMapFilters()
{
let mapFilters = Engine.GetGUIObjectByName("mapFilterSelection");
mapFilters.list = g_MapFilters.map(mapFilter => mapFilter.name);
mapFilters.list_data = g_MapFilters.map(mapFilter => mapFilter.id);
mapFilters.onSelectionChange = function() {
if (this.selected != -1)
selectMapFilter(this.list_data[this.selected]);
};
if (g_IsController)
mapFilters.selected = 0;
g_GameAttributes.mapFilter = "default";
}
/**
* Sets the size of the more-options dialog.
*/
function resizeMoreOptionsWindow()
{
// For singleplayer reduce the size of more options dialog by three options (cheats, rated game, observer late join = 90px)
if (!g_IsNetworked)
{
Engine.GetGUIObjectByName("moreOptions").size = "50%-200 50%-195 50%+200 50%+160";
Engine.GetGUIObjectByName("hideMoreOptions").size = "50%-70 310 50%+70 336";
}
// For non-lobby multiplayergames reduce the size of the dialog by one option (rated game, 30px)
else if (!Engine.HasXmppClient())
{
Engine.GetGUIObjectByName("moreOptions").size = "50%-200 50%-195 50%+200 50%+220";
Engine.GetGUIObjectByName("hideMoreOptions").size = "50%-70 370 50%+70 396";
Engine.GetGUIObjectByName("optionObserverLateJoin").size = "14 338 94% 366";
}
}
function initNumberOfPlayers()
{
let playersArray = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1); // 1, 2, ..., MaxPlayers
let numPlayersSelection = Engine.GetGUIObjectByName("numPlayersSelection");
numPlayersSelection.list = playersArray;
numPlayersSelection.list_data = playersArray;
numPlayersSelection.onSelectionChange = function() {
if (this.selected != -1)
selectNumPlayers(this.list_data[this.selected]);
};
numPlayersSelection.selected = g_MaxPlayers - 1;
}
function initGameSpeed()
{
let gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
gameSpeed.hidden = false;
Engine.GetGUIObjectByName("gameSpeedText").hidden = true;
gameSpeed.list = g_GameSpeeds.Title;
gameSpeed.list_data = g_GameSpeeds.Speed;
gameSpeed.onSelectionChange = function() {
if (this.selected != -1)
g_GameAttributes.gameSpeed = g_GameSpeeds.Speed[this.selected];
updateGameAttributes();
};
gameSpeed.selected = g_GameSpeeds.Default;
}
function initPopulationCaps()
{
let populationCaps = Engine.GetGUIObjectByName("populationCap");
populationCaps.list = g_PopulationCapacities.Title;
populationCaps.list_data = g_PopulationCapacities.Population;
populationCaps.selected = g_PopulationCapacities.Default;
populationCaps.onSelectionChange = function() {
if (this.selected != -1)
g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[this.selected];
updateGameAttributes();
};
}
function initStartingResources()
{
let startingResourcesL = Engine.GetGUIObjectByName("startingResources");
startingResourcesL.list = g_StartingResources.Title;
startingResourcesL.list_data = g_StartingResources.Resources;
startingResourcesL.selected = g_StartingResources.Default;
startingResourcesL.onSelectionChange = function() {
if (this.selected != -1)
g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[this.selected];
updateGameAttributes();
};
}
function initCeasefire()
{
let ceasefireL = Engine.GetGUIObjectByName("ceasefire");
ceasefireL.list = g_Ceasefire.Title;
ceasefireL.list_data = g_Ceasefire.Duration;
ceasefireL.selected = g_Ceasefire.Default;
ceasefireL.onSelectionChange = function() {
if (this.selected != -1)
g_GameAttributes.settings.Ceasefire = g_Ceasefire.Duration[this.selected];
updateGameAttributes();
};
}
function initVictoryConditions()
{
let victoryConditions = Engine.GetGUIObjectByName("victoryCondition");
victoryConditions.list = g_VictoryConditions.Title;
victoryConditions.list_data = g_VictoryConditions.Name;
victoryConditions.onSelectionChange = function() {
if (this.selected != -1)
{
g_GameAttributes.settings.GameType = g_VictoryConditions.Name[this.selected];
g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[this.selected];
}
updateGameAttributes();
};
victoryConditions.selected = g_VictoryConditions.Default;
}
function initMapSizes()
{
let mapSize = Engine.GetGUIObjectByName("mapSize");
mapSize.list = g_MapSizes.LongName;
mapSize.list_data = g_MapSizes.Tiles;
mapSize.onSelectionChange = function() {
if (this.selected != -1)
g_GameAttributes.settings.Size = g_MapSizes.Tiles[this.selected];
updateGameAttributes();
};
mapSize.selected = 0;
}
/**
* Assign update-functions to all checkboxes.
*/
function initRadioButtons()
{
let options = {
"RevealMap": "revealMap",
"ExploreMap": "exploreMap",
"DisableTreasures": "disableTreasures",
"LockTeams": "lockTeams",
"CheatsEnabled": "enableCheats",
"ObserverLateJoin": "observerLateJoin"
};
Object.keys(options).forEach(attribute => {
Engine.GetGUIObjectByName(options[attribute]).onPress = function() {
g_GameAttributes.settings[attribute] = this.checked;
updateGameAttributes();
};
});
Engine.GetGUIObjectByName("enableRating").onPress = function() {
g_GameAttributes.settings.RatingEnabled = this.checked;
Engine.SetRankedGame(this.checked);
Engine.GetGUIObjectByName("enableCheats").enabled = !this.checked;
Engine.GetGUIObjectByName("lockTeams").enabled = !this.checked;
updateGameAttributes();
};
}
/**
* If we're a network client, hide the controls and show the text instead.
*/
function hideControls()
{
hideControl("mapTypeSelection", "mapTypeText");
hideControl("mapFilterSelection", "mapFilterText");
hideControl("mapSelection", "mapSelectionText");
hideControl("victoryCondition", "victoryConditionText");
hideControl("gameSpeed", "gameSpeedText");
hideControl("numPlayersSelection", "numPlayersText");
// TODO: Shouldn't players be able to choose their own assignment?
for (let i = 0; i < g_MaxPlayers; ++i)
{
Engine.GetGUIObjectByName("playerAssignment["+i+"]").hidden = true;
Engine.GetGUIObjectByName("playerCiv["+i+"]").hidden = true;
Engine.GetGUIObjectByName("playerTeam["+i+"]").hidden = true;
}
Engine.GetGUIObjectByName("startGame").enabled = true;
}
/**
* Hides the GUI controls for clients and shows the read-only label instead.
*
* @param {string} control - name of the GUI object able to change a setting
* @param {string} label - name of the GUI object displaying a setting
* @param {boolean} [allowControl] - Whether the current user is allowed to change the control.
*/
function hideControl(control, label, allowControl = g_IsController)
{
Engine.GetGUIObjectByName(control).hidden = !allowControl;
Engine.GetGUIObjectByName(label).hidden = allowControl;
}
/**
* Checks a boolean checkbox for the host and sets the text of the label for the client.
*
* @param {string} control - name of the GUI object able to change a setting
* @param {string} label - name of the GUI object displaying a setting
* @param {boolean} checked - Whether the setting is active / enabled.
*/
function setGUIBoolean(control, label, checked)
{
Engine.GetGUIObjectByName(control).checked = checked;
Engine.GetGUIObjectByName(label).caption = checked ? translate("Yes") : translate("No");
}
/**
* Hide and set some elements depending on whether we play single- or multiplayer.
*/
function initMultiplayerSettings()
{
Engine.GetGUIObjectByName("chatPanel").hidden = !g_IsNetworked;
Engine.GetGUIObjectByName("optionCheats").hidden = !g_IsNetworked;
Engine.GetGUIObjectByName("optionRating").hidden = !Engine.HasXmppClient();
Engine.GetGUIObjectByName("optionObserverLateJoin").hidden = !g_IsNetworked;
Engine.GetGUIObjectByName("enableCheats").enabled = !Engine.IsRankedGame();
Engine.GetGUIObjectByName("lockTeams").enabled = !Engine.IsRankedGame();
Engine.GetGUIObjectByName("enableCheats").checked = g_GameAttributes.settings.CheatsEnabled;
Engine.GetGUIObjectByName("enableRating").checked = !!g_GameAttributes.settings.RatingEnabled;
hideControl("enableCheats", "enableCheatsText");
hideControl("enableRating", "enableRatingText");
hideControl("observerLateJoin", "observerLateJoinText");
}
/**
* Populate team-, color- and civ-dropdowns.
*/
function initPlayerAssignments()
{
let boxSpacing = 32;
for (let i = 0; i < g_MaxPlayers; ++i)
{
let box = Engine.GetGUIObjectByName("playerBox["+i+"]");
let boxSize = box.size;
let h = boxSize.bottom - boxSize.top;
boxSize.top = i * boxSpacing;
boxSize.bottom = i * boxSpacing + h;
box.size = boxSize;
let team = Engine.GetGUIObjectByName("playerTeam["+i+"]");
let teamsArray = Array(g_MaxTeams).fill(0).map((v, i) => i + 1); // 1, 2, ... MaxTeams
team.list = [translateWithContext("team", "None")].concat(teamsArray); // "None", 1, 2, ..., maxTeams
team.list_data = [-1].concat(teamsArray.map(team => team - 1)); // -1, 0, ..., (maxTeams-1)
team.selected = 0;
let playerSlot = i; // declare for inner function use
team.onSelectionChange = function() {
if (this.selected != -1)
g_GameAttributes.settings.PlayerData[playerSlot].Team = this.selected - 1;
updateGameAttributes();
};
let colorPicker = Engine.GetGUIObjectByName("playerColorPicker["+i+"]");
colorPicker.list = g_PlayerColors.map(color => '[color="' + color.r + ' ' + color.g + ' ' + color.b + '"] ■[/color]');
colorPicker.list_data = g_PlayerColors.map((color, index) => index);
colorPicker.selected = -1;
colorPicker.onSelectionChange = function() { selectPlayerColor(playerSlot, this.selected); };
Engine.GetGUIObjectByName("playerCiv["+i+"]").onSelectionChange = function() {
if ((this.selected != -1)&&(g_GameAttributes.mapType !== "scenario"))
g_GameAttributes.settings.PlayerData[playerSlot].Civ = this.list_data[this.selected];
updateGameAttributes();
};
}
}
/**
* Called when the client disconnects.
* The other cases from NetClient should never occur in the gamesetup.
* @param {Object} message
*/
function handleNetStatusMessage(message)
{
if (message.status != "disconnected")
{
error("Unrecognised netstatus type " + message.status);
return;
}
cancelSetup();
reportDisconnect(message.reason);
}
/**
* Called whenever a client clicks on ready (or not ready).
* @param {Object} message
*/
function handleReadyMessage(message)
{
g_ReadyChanged -= 1;
if (g_ReadyChanged < 1 && g_PlayerAssignments[message.guid].player != -1)
addChatMessage({
"type": message.status == 1 ? "ready" : "not-ready",
"guid": message.guid
});
if (!g_IsController)
return;
g_PlayerAssignments[message.guid].status = +message.status == 1;
Engine.SetNetworkPlayerStatus(message.guid, +message.status);
updateReadyUI();
}
/**
* Called after every player is ready and the host decided to finally start the game.
* @param {Object} message
*/
function handleGamestartMessage(message)
{
if (g_IsController && Engine.HasXmppClient())
{
let playerNames = Object.keys(g_PlayerAssignments).map(guid => g_PlayerAssignments[guid].name);
Engine.SendChangeStateGame(playerNames.length, playerNames.join(", "));
}
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": g_GameAttributes,
"isNetworked" : g_IsNetworked,
"playerAssignments": g_PlayerAssignments,
"isController": g_IsController
});
}
/**
* Called whenever the host changed any setting.
* @param {Object} message
*/
function handleGamesetupMessage(message)
{
if (!message.data)
return;
g_GameAttributes = message.data;
if (!!g_GameAttributes.settings.RatingEnabled)
{
g_GameAttributes.settings.CheatsEnabled = false;
g_GameAttributes.settings.LockTeams = true;
}
Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled);
updateGUIObjects();
}
/**
* Called whenever a client joins/leaves or any gamesetting is changed.
* @param {Object} message
*/
function handlePlayerAssignmentMessage(message)
{
let resetReady = false;
let newGUID = "";
// Report joinings
for (let guid in message.hosts)
{
if (g_PlayerAssignments[guid])
continue;
addChatMessage({ "type": "connect", "guid": guid, "username": message.hosts[guid].name });
newGUID = guid;
// Assign the new player
if (!g_IsController || message.hosts[guid].player != -1)
continue;
let freeSlot = g_GameAttributes.settings.PlayerData.findIndex((v,i) =>
Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player != i+1)
);
if (freeSlot != -1)
Engine.AssignNetworkPlayer(freeSlot+1, guid);
}
// Report leavings
for (let guid in g_PlayerAssignments)
{
if (message.hosts[guid])
continue;
addChatMessage({ "type": "disconnect", "guid": guid });
if (g_PlayerAssignments[guid].player != -1)
resetReady = true; // Observers shouldn't reset ready.
}
g_PlayerAssignments = message.hosts;
updatePlayerList();
if (g_PlayerAssignments[newGUID] && g_PlayerAssignments[newGUID].player != -1)
resetReady = true;
if (resetReady)
resetReadyData();
updateReadyUI();
sendRegisterGameStanza();
}
function getMapDisplayName(map)
{
let mapData = loadMapData(map);
if (!mapData || !mapData.settings || !mapData.settings.Name)
{
log("Map data missing in scenario '" + map + "' - likely unsupported format");
return map;
}
return mapData.settings.Name;
}
function getMapPreview(map)
{
let mapData = loadMapData(map);
if (!mapData || !mapData.settings || !mapData.settings.Preview)
return "nopreview.png";
return mapData.settings.Preview;
}
// Get a setting if it exists or return default
function getSetting(settings, defaults, property)
{
if (settings && (property in settings))
return settings[property];
if (defaults && (property in defaults))
return defaults[property];
return undefined;
}
/**
* Initialize the dropdowns containing all selectable civs (including random).
*/
function initCivNameList()
{
let civList = Object.keys(g_CivData).filter(civ => g_CivData[civ].SelectableInGameSetup).map(civ => ({ "name": g_CivData[civ].Name, "code": civ })).sort(sortNameIgnoreCase);
let civListNames = [g_RandomCiv].concat(civList.map(civ => civ.name));
let civListCodes = ["random"].concat(civList.map(civ => civ.code));
for (let i = 0; i < g_MaxPlayers; ++i)
{
let civ = Engine.GetGUIObjectByName("playerCiv["+i+"]");
civ.list = civListNames;
civ.list_data = civListCodes;
civ.selected = 0;
}
}
/**
* Initialize the dropdown containing all maps for the selected maptype and mapfilter.
*/
function initMapNameList()
{
if (!g_MapPath[g_GameAttributes.mapType])
{
error("initMapNameList: Unexpected map type " + g_GameAttributes.mapType);
return;
}
let mapFiles = g_GameAttributes.mapType == "random" ? getJSONFileList(g_GameAttributes.mapPath) : getXMLFileList(g_GameAttributes.mapPath);
// Apply map filter, if any defined
// TODO: Should verify these are valid maps before adding to list
let mapList = [];
for (let mapFile of mapFiles)
{
let file = g_GameAttributes.mapPath + mapFile;
let mapData = loadMapData(file);
let mapFilter = g_MapFilters.find(mapFilter => mapFilter.id == (g_GameAttributes.mapFilter || "all"));
if (!!mapData.settings && mapFilter && mapFilter.filter(mapData.settings.Keywords || []))
mapList.push({ "name": getMapDisplayName(file), "file": file });
}
translateObjectKeys(mapList, ["name"]);
mapList.sort(sortNameIgnoreCase);
let mapListNames = mapList.map(map => map.name);
let mapListFiles = mapList.map(map => map.file);
// Scenario/skirmish maps have a fixed playercount
if (g_GameAttributes.mapType == "random")
{
mapListNames.unshift(g_RandomMap);
mapListFiles.unshift("random");
}
let mapSelectionBox = Engine.GetGUIObjectByName("mapSelection");
mapSelectionBox.list = mapListNames;
mapSelectionBox.list_data = mapListFiles;
mapSelectionBox.onSelectionChange = function() {
if (this.selected != -1)
selectMap(this.list_data[this.selected]);
};
mapSelectionBox.selected = Math.max(0, mapListFiles.indexOf(g_GameAttributes.map || ""));
}
function loadMapData(name)
{
if (!name || !g_MapPath[g_GameAttributes.mapType])
return undefined;
if (name == "random")
return { "settings": { "Name": "", "Description": "" } };
if (!g_MapData[name])
g_MapData[name] = g_GameAttributes.mapType == "random" ? Engine.ReadJSONFile(name + ".json") : Engine.LoadMapSettings(name);
return g_MapData[name];
}
/**
* Sets the gameattributes the way they were the last time the user left the gamesetup.
*/
function loadPersistMatchSettings()
{
if (Engine.ConfigDB_GetValue("user", "persistmatchsettings") != "true")
return;
let settingsFile = g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP;
if (!Engine.FileExists(settingsFile))
return;
let attrs = Engine.ReadJSONFile(settingsFile);
if (!attrs || !attrs.settings)
return;
g_IsInGuiUpdate = true;
let mapName = attrs.map || "";
let mapSettings = attrs.settings;
g_GameAttributes = attrs;
g_GameAttributes.matchID = Engine.GetMatchID();
mapSettings.Seed = Math.floor(Math.random() * 65536);
mapSettings.AISeed = Math.floor(Math.random() * 65536);
if (!g_IsNetworked)
mapSettings.CheatsEnabled = true;
// Replace unselectable civs with random civ
let playerData = mapSettings.PlayerData;
if (playerData && g_GameAttributes.mapType != "scenario")
for (let i in playerData)
if (!g_CivData[playerData[i].Civ] || !g_CivData[playerData[i].Civ].SelectableInGameSetup)
playerData[i].Civ = "random";
// Apply map settings
let newMapData = loadMapData(mapName);
if (newMapData && newMapData.settings)
{
for (let prop in newMapData.settings)
mapSettings[prop] = newMapData.settings[prop];
if (playerData)
mapSettings.PlayerData = playerData;
}
if (mapSettings.PlayerData)
sanitizePlayerData(mapSettings.PlayerData);
// Reload, as the maptype or mapfilter might have changed
initMapNameList();
g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient();
Engine.SetRankedGame(g_GameAttributes.settings.RatingEnabled);
updateGUIObjects();
}
function savePersistMatchSettings()
{
let attributes = Engine.ConfigDB_GetValue("user", "persistmatchsettings") == "true" ? g_GameAttributes : {};
Engine.WriteJSONFile(g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP, attributes);
}
function sanitizePlayerData(playerData)
{
// Remove gaia
if (playerData.length && !playerData[0])
playerData.shift();
playerData.forEach((pData, index) => {
pData.Color = pData.Color || g_PlayerColors[index];
pData.Civ = pData.Civ || "random";
});
// Replace colors with the best matching color of PlayerDefaults
if (g_GameAttributes.mapType != "scenario")
{
playerData.forEach((pData, index) => {
let colorDistances = g_PlayerColors.map(color => colorDistance(color, pData.Color));
let smallestDistance = colorDistances.find(distance => colorDistances.every(distance2 => (distance2 >= distance)));
pData.Color = g_PlayerColors.find(color => colorDistance(color, pData.Color) == smallestDistance);
});
}
ensureUniquePlayerColors(playerData);
}
function cancelSetup()
{
if (g_IsController)
savePersistMatchSettings();
Engine.DisconnectNetworkGame();
if (Engine.HasXmppClient())
{
Engine.LobbySetPlayerPresence("available");
if (g_IsController)
Engine.SendUnregisterGame();
Engine.SwitchGuiPage("page_lobby.xml");
}
else
Engine.SwitchGuiPage("page_pregame.xml");
}
function onTick()
{
if (!g_Settings)
return;
// First tick happens before first render, so don't load yet
if (g_LoadingState == 0)
{
g_LoadingState++;
}
else if (g_LoadingState == 1)
{
Engine.GetGUIObjectByName("loadingWindow").hidden = true;
Engine.GetGUIObjectByName("setupWindow").hidden = false;
initGUIObjects();
g_LoadingState++;
}
else if (g_LoadingState == 2)
{
while (true)
{
let message = Engine.PollNetworkClient();
if (!message)
break;
log("Net message: " + uneval(message));
if (g_NetMessageTypes[message.type])
g_NetMessageTypes[message.type](message);
else
error("Unrecognised net message type " + message.type);
}
}
+
+ updateTimers();
}
/**
* Called when the host choses the number of players on a random map.
* @param {Number} num
*/
function selectNumPlayers(num)
{
// Avoid recursion
if (g_IsInGuiUpdate || !g_IsController || g_GameAttributes.mapType != "random")
return;
// Unassign players from nonexistent slots
if (g_IsNetworked)
{
for (let i = g_MaxPlayers; i > num; --i)
Engine.AssignNetworkPlayer(i, "");
}
else if (g_PlayerAssignments.local.player > num)
g_PlayerAssignments.local.player = 1;
// Update player data
let pData = g_GameAttributes.settings.PlayerData;
if (num < pData.length)
g_GameAttributes.settings.PlayerData = pData.slice(0, num);
else
for (let i = pData.length; i < num; ++i)
g_GameAttributes.settings.PlayerData.push(g_DefaultPlayerData[i]);
updateGameAttributes();
}
/**
* Assigns the given color to that player.
*/
function selectPlayerColor(playerSlot, colorIndex)
{
if (colorIndex == -1)
return;
let playerData = g_GameAttributes.settings.PlayerData;
// If someone else has that color, give that player the old color
let pData = playerData.find(pData => sameColor(g_PlayerColors[colorIndex], pData.Color));
if (pData)
pData.Color = playerData[playerSlot].Color;
// Assign the new color
playerData[playerSlot].Color = g_PlayerColors[colorIndex];
// Ensure colors are not used twice after increasing the number of players
ensureUniquePlayerColors(playerData);
if (!g_IsInGuiUpdate)
updateGameAttributes();
}
function ensureUniquePlayerColors(playerData)
{
for (let i = playerData.length - 1; i >= 0; --i)
// If someone else has that color, assign an unused color
if (playerData.some((pData, j) => i != j && sameColor(playerData[i].Color, pData.Color)))
playerData[i].Color = g_PlayerColors.find(color => playerData.every(pData => !sameColor(color, pData.Color)));
}
/**
* Called when the user selects a map type from the list.
*
* @param {string} type - scenario, skirmish or random
*/
function selectMapType(type)
{
// Avoid recursion
if (g_IsInGuiUpdate || !g_IsController)
return;
if (!g_MapPath[type])
{
error("selectMapType: Unexpected map type " + type);
return;
}
g_MapData = {};
g_GameAttributes.map = "";
g_GameAttributes.mapType = type;
g_GameAttributes.mapPath = g_MapPath[type];
if (type != "scenario")
g_GameAttributes.settings = {
"PlayerData": g_DefaultPlayerData.slice(0, 4),
"Seed": Math.floor(Math.random() * 65536),
"CheatsEnabled": g_GameAttributes.settings.CheatsEnabled
};
g_GameAttributes.settings.AISeed = Math.floor(Math.random() * 65536);
initMapNameList();
updateGameAttributes();
}
function selectMapFilter(id)
{
// Avoid recursion
if (g_IsInGuiUpdate || !g_IsController)
return;
g_GameAttributes.mapFilter = id;
initMapNameList();
updateGameAttributes();
}
// Called when the user selects a map from the list
function selectMap(name)
{
// Avoid recursion
if (g_IsInGuiUpdate || !g_IsController || !name)
return;
// Reset some map specific properties which are not necessarily redefined on each map
for (let prop of ["TriggerScripts", "CircularMap", "Garrison"])
g_GameAttributes.settings[prop] = undefined;
let mapData = loadMapData(name);
let mapSettings = mapData && mapData.settings ? deepcopy(mapData.settings) : {};
// Reset victory conditions
if (g_GameAttributes.mapType != "random")
{
let victoryIdx = g_VictoryConditions.Name.indexOf(mapSettings.GameType || "") != -1 ? g_VictoryConditions.Name.indexOf(mapSettings.GameType) : g_VictoryConditions.Default;
g_GameAttributes.settings.GameType = g_VictoryConditions.Name[victoryIdx];
g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[victoryIdx];
}
if (mapSettings.PlayerData)
sanitizePlayerData(mapSettings.PlayerData);
// Copy any new settings
g_GameAttributes.map = name;
g_GameAttributes.script = mapSettings.Script;
if (g_GameAttributes.map !== "random")
for (let prop in mapSettings)
g_GameAttributes.settings[prop] = mapSettings[prop];
// Use default AI if the map doesn't specify any explicitly
for (let i in g_GameAttributes.settings.PlayerData)
{
if (!('AI' in g_GameAttributes.settings.PlayerData[i]))
g_GameAttributes.settings.PlayerData[i].AI = g_DefaultPlayerData[i].AI;
if (!('AIDiff' in g_GameAttributes.settings.PlayerData[i]))
g_GameAttributes.settings.PlayerData[i].AIDiff = g_DefaultPlayerData[i].AIDiff;
}
// Reset player assignments on map change
if (!g_IsNetworked)
g_PlayerAssignments = { "local": { "name": translate("You"), "player": 1, "civ": "", "team": -1, "ready": 0 } };
else
{
let numPlayers = mapSettings.PlayerData ? mapSettings.PlayerData.length : g_GameAttributes.settings.PlayerData.length;
for (let guid in g_PlayerAssignments)
{ // Unassign extra players
let player = g_PlayerAssignments[guid].player;
if (player <= g_MaxPlayers && player > numPlayers)
Engine.AssignNetworkPlayer(player, "");
}
}
updateGameAttributes();
}
function launchGame()
{
if (!g_IsController)
{
error("Only host can start game");
return;
}
if (!g_GameAttributes.map)
return;
savePersistMatchSettings();
// Select random map
if (g_GameAttributes.map == "random")
{
let victoryScriptsSelected = g_GameAttributes.settings.VictoryScripts;
let gameTypeSelected = g_GameAttributes.settings.GameType;
selectMap(Engine.GetGUIObjectByName("mapSelection").list_data[Math.floor(Math.random() *
(Engine.GetGUIObjectByName("mapSelection").list.length - 1)) + 1]);
g_GameAttributes.settings.VictoryScripts = victoryScriptsSelected;
g_GameAttributes.settings.GameType = gameTypeSelected;
}
g_GameAttributes.settings.TriggerScripts = g_GameAttributes.settings.VictoryScripts.concat(g_GameAttributes.settings.TriggerScripts || []);
// Prevent reseting the readystate
g_GameStarted = true;
g_GameAttributes.settings.mapType = g_GameAttributes.mapType;
// Get a unique array of selectable cultures
let cultures = Object.keys(g_CivData).filter(civ => g_CivData[civ].SelectableInGameSetup).map(civ => g_CivData[civ].Culture);
cultures = cultures.filter((culture, index) => cultures.indexOf(culture) === index);
// Determine random civs and botnames
for (let i in g_GameAttributes.settings.PlayerData)
{
// Pick a random civ of a random culture
let chosenCiv = g_GameAttributes.settings.PlayerData[i].Civ || "random";
if (chosenCiv == "random")
{
let culture = cultures[Math.floor(Math.random() * cultures.length)];
let civs = Object.keys(g_CivData).filter(civ => g_CivData[civ].Culture == culture);
chosenCiv = civs[Math.floor(Math.random() * civs.length)];
}
g_GameAttributes.settings.PlayerData[i].Civ = chosenCiv;
// Pick one of the available botnames for the chosen civ
if (g_GameAttributes.mapType === "scenario" || !g_GameAttributes.settings.PlayerData[i].AI)
continue;
let chosenName = g_CivData[chosenCiv].AINames[Math.floor(Math.random() * g_CivData[chosenCiv].AINames.length)];
if (!g_IsNetworked)
chosenName = translate(chosenName);
// Count how many players use the chosenName
let usedName = g_GameAttributes.settings.PlayerData.filter(pData => pData.Name && pData.Name.indexOf(chosenName) !== -1).length;
g_GameAttributes.settings.PlayerData[i].Name = !usedName ? chosenName : sprintf(translate("%(playerName)s %(romanNumber)s"), { "playerName": chosenName, "romanNumber": g_RomanNumbers[usedName+1] });
}
// Copy playernames from initial player assignment to the settings
for (let guid in g_PlayerAssignments)
{
let player = g_PlayerAssignments[guid];
if (player.player > 0) // not observer or GAIA
g_GameAttributes.settings.PlayerData[player.player - 1].Name = player.name;
}
if (g_IsNetworked)
{
Engine.SetNetworkGameAttributes(g_GameAttributes);
Engine.StartNetworkGame();
}
else
{
// Find the player ID which the user has been assigned to
let playerID = -1;
for (let i in g_GameAttributes.settings.PlayerData)
{
let assignBox = Engine.GetGUIObjectByName("playerAssignment["+i+"]");
if (assignBox.list_data[assignBox.selected] == "local")
playerID = +i+1;
}
Engine.StartGame(g_GameAttributes, playerID);
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": g_GameAttributes,
"isNetworked" : g_IsNetworked,
"playerAssignments": g_PlayerAssignments
});
}
}
/**
* Don't set any attributes here, just show the changes in the GUI.
*
* Unless the mapsettings don't specify a property and the user didn't set it in g_GameAttributes previously.
*/
function updateGUIObjects()
{
g_IsInGuiUpdate = true;
let mapSettings = g_GameAttributes.settings;
// These dropdowns don't set values while g_IsInGuiUpdate
let mapName = g_GameAttributes.map || "";
let mapFilterIdx = g_MapFilters.findIndex(mapFilter => mapFilter.id == (g_GameAttributes.mapFilter || "default"));
let mapTypeIdx = g_GameAttributes.mapType !== undefined ? g_MapTypes.Name.indexOf(g_GameAttributes.mapType) : g_MapTypes.Default;
let gameSpeedIdx = g_GameAttributes.gameSpeed !== undefined ? g_GameSpeeds.Speed.indexOf(g_GameAttributes.gameSpeed) : g_GameSpeeds.Default;
// These dropdowns might set the default (as they ignore g_IsInGuiUpdate)
let mapSizeIdx = mapSettings.Size !== undefined ? g_MapSizes.Tiles.indexOf(mapSettings.Size) : g_MapSizes.Default;
let victoryIdx = mapSettings.GameType !== undefined ? g_VictoryConditions.Name.indexOf(mapSettings.GameType) : g_VictoryConditions.Default;
let popIdx = mapSettings.PopulationCap !== undefined ? g_PopulationCapacities.Population.indexOf(mapSettings.PopulationCap) : g_PopulationCapacities.Default;
let startingResIdx = mapSettings.StartingResources !== undefined ? g_StartingResources.Resources.indexOf(mapSettings.StartingResources) : g_StartingResources.Default;
let ceasefireIdx = mapSettings.Ceasefire !== undefined ? g_Ceasefire.Duration.indexOf(mapSettings.Ceasefire) : g_Ceasefire.Default;
let numPlayers = mapSettings.PlayerData ? mapSettings.PlayerData.length : g_MaxPlayers;
if (g_IsController)
{
Engine.GetGUIObjectByName("mapTypeSelection").selected = mapTypeIdx;
Engine.GetGUIObjectByName("mapFilterSelection").selected = mapFilterIdx;
Engine.GetGUIObjectByName("mapSelection").selected = Engine.GetGUIObjectByName("mapSelection").list_data.indexOf(mapName);
Engine.GetGUIObjectByName("mapSize").selected = mapSizeIdx;
Engine.GetGUIObjectByName("numPlayersSelection").selected = numPlayers - 1;
Engine.GetGUIObjectByName("victoryCondition").selected = victoryIdx;
Engine.GetGUIObjectByName("populationCap").selected = popIdx;
Engine.GetGUIObjectByName("gameSpeed").selected = gameSpeedIdx;
Engine.GetGUIObjectByName("ceasefire").selected = ceasefireIdx;
Engine.GetGUIObjectByName("startingResources").selected = startingResIdx;
}
else
{
Engine.GetGUIObjectByName("mapTypeText").caption = g_MapTypes.Title[mapTypeIdx];
Engine.GetGUIObjectByName("mapFilterText").caption = g_MapFilters[mapFilterIdx].name;
Engine.GetGUIObjectByName("mapSelectionText").caption = mapName == "random" ? g_RandomMap : translate(getMapDisplayName(mapName));
initMapNameList();
}
// Can be visible to both host and clients
Engine.GetGUIObjectByName("mapSizeText").caption = g_GameAttributes.mapType == "random" ? g_MapSizes.LongName[mapSizeIdx] : translate("Default");
Engine.GetGUIObjectByName("numPlayersText").caption = numPlayers;
Engine.GetGUIObjectByName("victoryConditionText").caption = g_VictoryConditions.Title[victoryIdx];
Engine.GetGUIObjectByName("populationCapText").caption = g_PopulationCapacities.Title[popIdx];
Engine.GetGUIObjectByName("startingResourcesText").caption = g_StartingResources.Title[startingResIdx];
Engine.GetGUIObjectByName("ceasefireText").caption = g_Ceasefire.Title[ceasefireIdx];
Engine.GetGUIObjectByName("gameSpeedText").caption = g_GameSpeeds.Title[gameSpeedIdx];
setGUIBoolean("enableCheats", "enableCheatsText", !!mapSettings.CheatsEnabled);
setGUIBoolean("disableTreasures", "disableTreasuresText", !!mapSettings.DisableTreasures);
setGUIBoolean("exploreMap", "exploreMapText", !!mapSettings.ExploreMap);
setGUIBoolean("revealMap", "revealMapText", !!mapSettings.RevealMap);
setGUIBoolean("lockTeams", "lockTeamsText", !!mapSettings.LockTeams);
setGUIBoolean("observerLateJoin", "observerLateJoinText", !!mapSettings.ObserverLateJoin);
setGUIBoolean("enableRating", "enableRatingText", !!mapSettings.RatingEnabled);
Engine.GetGUIObjectByName("cheatWarningText").hidden = !g_IsNetworked || !mapSettings.CheatsEnabled;
Engine.GetGUIObjectByName("enableCheats").enabled = !mapSettings.RatingEnabled;
Engine.GetGUIObjectByName("lockTeams").enabled = !mapSettings.RatingEnabled;
// Mapsize completely hidden for non-random maps
let isRandom = g_GameAttributes.mapType == "random";
Engine.GetGUIObjectByName("mapSizeDesc").hidden = !isRandom;
Engine.GetGUIObjectByName("mapSize").hidden = !isRandom || !g_IsController;
Engine.GetGUIObjectByName("mapSizeText").hidden = !isRandom || g_IsController;
hideControl("numPlayersSelection", "numPlayersText", isRandom && g_IsController);
let notScenario = g_GameAttributes.mapType != "scenario" && g_IsController ;
hideControl("victoryCondition", "victoryConditionText", notScenario);
hideControl("populationCap", "populationCapText", notScenario);
hideControl("startingResources", "startingResourcesText", notScenario);
hideControl("ceasefire", "ceasefireText", notScenario);
hideControl("revealMap", "revealMapText", notScenario);
hideControl("exploreMap", "exploreMapText", notScenario);
hideControl("disableTreasures", "disableTreasuresText", notScenario);
hideControl("lockTeams", "lockTeamsText", notScenario);
setMapDescription();
for (let i = 0; i < g_MaxPlayers; ++i)
{
Engine.GetGUIObjectByName("playerBox["+i+"]").hidden = (i >= numPlayers);
if (i >= numPlayers)
continue;
let pName = Engine.GetGUIObjectByName("playerName["+i+"]");
let pAssignment = Engine.GetGUIObjectByName("playerAssignment["+i+"]");
let pAssignmentText = Engine.GetGUIObjectByName("playerAssignmentText["+i+"]");
let pCiv = Engine.GetGUIObjectByName("playerCiv["+i+"]");
let pCivText = Engine.GetGUIObjectByName("playerCivText["+i+"]");
let pTeam = Engine.GetGUIObjectByName("playerTeam["+i+"]");
let pTeamText = Engine.GetGUIObjectByName("playerTeamText["+i+"]");
let pColor = Engine.GetGUIObjectByName("playerColor["+i+"]");
let pData = mapSettings.PlayerData ? mapSettings.PlayerData[i] : {};
let pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[i] : {};
let color = getSetting(pData, pDefs, "Color");
pColor.sprite = "color:" + rgbToGuiColor(color) + " 100";
pName.caption = translate(getSetting(pData, pDefs, "Name"));
let team = getSetting(pData, pDefs, "Team");
let civ = getSetting(pData, pDefs, "Civ");
pAssignmentText.caption = pAssignment.list[0] ? pAssignment.list[Math.max(0, pAssignment.selected)] : translate("Loading...");
pCivText.caption = civ == "random" ? g_RandomCiv : (g_CivData[civ] ? g_CivData[civ].Name : "Unknown");
pTeamText.caption = (team !== undefined && team >= 0) ? team+1 : "-";
pCiv.selected = civ ? pCiv.list_data.indexOf(civ) : 0;
pTeam.selected = team !== undefined && team >= 0 ? team+1 : 0;
hideControl("playerAssignment["+i+"]", "playerAssignmentText["+i+"]", g_IsController);
hideControl("playerCiv["+i+"]", "playerCivText["+i+"]", notScenario);
hideControl("playerTeam["+i+"]", "playerTeamText["+i+"]", notScenario);
// Allow host to chose player colors on non-scenario maps
let pColorPicker = Engine.GetGUIObjectByName("playerColorPicker["+i+"]");
let pColorPickerHeading = Engine.GetGUIObjectByName("playerColorHeading");
let canChangeColors = g_IsController && g_GameAttributes.mapType != "scenario";
pColorPicker.hidden = !canChangeColors;
pColorPickerHeading.hidden = !canChangeColors;
if (canChangeColors)
pColorPicker.selected = g_PlayerColors.findIndex(col => sameColor(col, color));
}
g_IsInGuiUpdate = false;
// Game attributes include AI settings, so update the player list
updatePlayerList();
// We should have everyone confirm that the new settings are acceptable.
resetReadyData();
}
/**
* Sets an additional map label, map preview image and mapsettings description.
*/
function setMapDescription()
{
let numPlayers = g_GameAttributes.settings.PlayerData ? g_GameAttributes.settings.PlayerData.length : 0;
let mapName = g_GameAttributes.map || "";
let victoryIdx = Math.max(0, g_VictoryConditions.Name.indexOf(g_GameAttributes.settings.GameType || ""));
let victoryTitle = g_VictoryConditions.Title[victoryIdx];
if (victoryIdx != g_VictoryConditions.Default)
victoryTitle = "[color=\"" + g_VictoryColor + "\"]" + victoryTitle + "[/color]";
let mapDescription = g_GameAttributes.settings.Description ? translate(g_GameAttributes.settings.Description) : translate("Sorry, no description available.");
if (mapName == "random")
mapDescription = translate("Randomly selects a map from the list");
let gameDescription = sprintf(translatePlural("%(number)s player. ", "%(number)s players. ", numPlayers), { "number": numPlayers });
gameDescription += translate("Victory Condition:") + " " + victoryTitle + ".\n\n";
gameDescription += mapDescription;
Engine.GetGUIObjectByName("mapInfoName").caption = mapName == "random" ? translateWithContext("map", "Random") : translate(getMapDisplayName(mapName));
Engine.GetGUIObjectByName("mapInfoDescription").caption = gameDescription;
setMapPreviewImage("mapPreview", getMapPreview(mapName));
}
/**
* Broadcast the changed settings to all clients and the lobbybot.
*/
function updateGameAttributes()
{
if (g_IsInGuiUpdate || !g_IsController)
return;
if (g_IsNetworked)
{
Engine.SetNetworkGameAttributes(g_GameAttributes);
if (g_LoadingState >= 2)
sendRegisterGameStanza();
}
else
updateGUIObjects();
}
function AIConfigCallback(ai)
{
g_GameAttributes.settings.PlayerData[ai.playerSlot].AI = ai.id;
g_GameAttributes.settings.PlayerData[ai.playerSlot].AIDiff = ai.difficulty;
if (g_IsNetworked)
Engine.SetNetworkGameAttributes(g_GameAttributes);
else
updatePlayerList();
}
function updatePlayerList()
{
g_IsInGuiUpdate = true;
let hostNameList = [];
let hostGuidList = [];
let assignments = [];
let aiAssignments = {};
let noAssignment;
let assignedCount = 0;
for (let guid in g_PlayerAssignments)
{
let player = g_PlayerAssignments[guid].player;
hostNameList.push(g_PlayerAssignments[guid].name);
hostGuidList.push(guid);
assignments[player] = hostNameList.length-1;
if (player != -1)
assignedCount++;
}
// Only enable start button if we have enough assigned players
if (g_IsController)
Engine.GetGUIObjectByName("startGame").enabled = assignedCount > 0;
for (let ai of g_Settings.AIDescriptions)
{
// If the map uses a hidden AI then don't hide it
if (ai.data.hidden && g_GameAttributes.settings.PlayerData.every(pData => pData.AI != ai.id))
continue;
aiAssignments[ai.id] = hostNameList.length;
hostNameList.push("[color=\""+ g_AIColor + "\"]" + sprintf(translate("AI: %(ai)s"), { "ai": translate(ai.data.name) }));
hostGuidList.push("ai:" + ai.id);
}
noAssignment = hostNameList.length;
hostNameList.push("[color=\""+ g_UnassignedColor + "\"]" + translate("Unassigned"));
hostGuidList.push("");
for (let i = 0; i < g_MaxPlayers; ++i)
{
let playerSlot = i;
let playerID = i+1; // we don't show Gaia, so first slot is ID 1
let selection = assignments[playerID];
let configButton = Engine.GetGUIObjectByName("playerConfig["+i+"]");
configButton.hidden = true;
// Look for valid player slots
if (playerSlot >= g_GameAttributes.settings.PlayerData.length)
continue;
// If no human is assigned, look for an AI instead
if (selection === undefined)
{
let aiId = g_GameAttributes.settings.PlayerData[playerSlot].AI;
if (aiId)
{
// Check for a valid AI
if (aiId in aiAssignments)
selection = aiAssignments[aiId];
else
{
g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
warn("AI \"" + aiId + "\" not present. Defaulting to unassigned.");
}
}
if (!selection)
selection = noAssignment;
// Since no human is assigned, show the AI config button
if (g_IsController)
{
configButton.hidden = false;
configButton.onpress = function() {
Engine.PushGuiPage("page_aiconfig.xml", {
"id": g_GameAttributes.settings.PlayerData[playerSlot].AI,
"difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff,
"callback": "AIConfigCallback",
"playerSlot": playerSlot // required by the callback function
});
};
}
}
// There was a human, so make sure we don't have any AI left
// over in their slot, if we're in charge of the attributes
else if (g_IsController && g_GameAttributes.settings.PlayerData[playerSlot].AI)
{
g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
if (g_IsNetworked)
Engine.SetNetworkGameAttributes(g_GameAttributes);
}
let assignBox = Engine.GetGUIObjectByName("playerAssignment["+i+"]");
let assignBoxText = Engine.GetGUIObjectByName("playerAssignmentText["+i+"]");
assignBox.list = hostNameList;
assignBox.list_data = hostGuidList;
if (assignBox.selected != selection)
assignBox.selected = selection;
assignBoxText.caption = hostNameList[selection];
if (g_IsController)
assignBox.onselectionchange = function() {
if (g_IsInGuiUpdate)
return;
let guid = hostGuidList[this.selected];
if (!guid)
{
if (g_IsNetworked)
// Unassign any host from this player slot
Engine.AssignNetworkPlayer(playerID, "");
// Remove AI from this player slot
g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
}
else if (guid.substr(0, 3) == "ai:")
{
if (g_IsNetworked)
// Unassign any host from this player slot
Engine.AssignNetworkPlayer(playerID, "");
// Set the AI for this player slot
g_GameAttributes.settings.PlayerData[playerSlot].AI = guid.substr(3);
}
else
swapPlayers(guid, playerSlot);
if (g_IsNetworked)
Engine.SetNetworkGameAttributes(g_GameAttributes);
else
updatePlayerList();
updateReadyUI();
};
}
g_IsInGuiUpdate = false;
}
function swapPlayers(guid, newSlot)
{
// Player slots are indexed from 0 as Gaia is omitted.
let newPlayerID = newSlot + 1;
let playerID = g_PlayerAssignments[guid].player;
// Attempt to swap the player or AI occupying the target slot,
// if any, into the slot this player is currently in.
if (playerID != -1)
{
for (let guid in g_PlayerAssignments)
{
// Move the player in the destination slot into the current slot.
if (g_PlayerAssignments[guid].player != newPlayerID)
continue;
if (g_IsNetworked)
Engine.AssignNetworkPlayer(playerID, guid);
else
g_PlayerAssignments[guid].player = playerID;
break;
}
// Transfer the AI from the target slot to the current slot.
g_GameAttributes.settings.PlayerData[playerID - 1].AI = g_GameAttributes.settings.PlayerData[newSlot].AI;
}
if (g_IsNetworked)
Engine.AssignNetworkPlayer(newPlayerID, guid);
else
g_PlayerAssignments[guid].player = newPlayerID;
// Remove AI from this player slot
g_GameAttributes.settings.PlayerData[newSlot].AI = "";
}
function submitChatInput()
{
let input = Engine.GetGUIObjectByName("chatInput");
let text = input.caption;
if (!text.length)
return;
input.caption = "";
if (executeNetworkCommand(text))
return;
Engine.SendNetworkChat(text);
}
function senderFont(text)
{
return '[font="' + g_SenderFont + '"]' + text + '[/font]';
}
function systemMessage(message)
{
return senderFont(sprintf(translate("== %(message)s"), { "message": message }));
}
function colorizePlayernameByGUID(guid, username = "")
{
// TODO: Maybe the host should have the moderator-prefix?
if (!username)
username = g_PlayerAssignments[guid] ? escapeText(g_PlayerAssignments[guid].name) : translate("Unknown Player");
let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1;
let color = "white";
if (playerID > 0)
{
color = g_GameAttributes.settings.PlayerData[playerID - 1].Color;
// Enlighten playercolor to improve readability
let [h, s, l] = rgbToHsl(color.r, color.g, color.b);
let [r, g, b] = hslToRgb(h, s, Math.max(0.6, l));
color = rgbToGuiColor({ "r": r, "g": g, "b": b });
}
return '[color="'+ color +'"]' + username + '[/color]';
}
function addChatMessage(msg)
{
if (!g_FormatChatMessage[msg.type])
return;
let user = colorizePlayernameByGUID(msg.guid || -1, msg.username || "");
g_ChatMessages.push(g_FormatChatMessage[msg.type](msg, user));
Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
}
function showMoreOptions(show)
{
Engine.GetGUIObjectByName("moreOptionsFade").hidden = !show;
Engine.GetGUIObjectByName("moreOptions").hidden = !show;
}
function toggleReady()
{
g_IsReady = !g_IsReady;
if (g_IsReady)
{
Engine.SendNetworkReady(1);
Engine.GetGUIObjectByName("startGame").caption = translate("I'm not ready");
Engine.GetGUIObjectByName("startGame").tooltip = translate("State that you are not ready to play.");
}
else
{
Engine.SendNetworkReady(0);
Engine.GetGUIObjectByName("startGame").caption = translate("I'm ready!");
Engine.GetGUIObjectByName("startGame").tooltip = translate("State that you are ready to play!");
}
}
function updateReadyUI()
{
if (!g_IsNetworked)
return;
let isAI = new Array(g_MaxPlayers + 1).fill(true);
let allReady = true;
for (let guid in g_PlayerAssignments)
{
// We don't really care whether observers are ready.
if (g_PlayerAssignments[guid].player == -1 || !g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1])
continue;
let pData = g_GameAttributes.settings.PlayerData ? g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1] : {};
let pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[g_PlayerAssignments[guid].player - 1] : {};
isAI[g_PlayerAssignments[guid].player] = false;
if (g_PlayerAssignments[guid].status || !g_IsNetworked)
Engine.GetGUIObjectByName("playerName[" + (g_PlayerAssignments[guid].player - 1) + "]").caption = '[color="' + g_ReadyColor + '"]' + translate(getSetting(pData, pDefs, "Name")) + '[/color]';
else
{
Engine.GetGUIObjectByName("playerName[" + (g_PlayerAssignments[guid].player - 1) + "]").caption = translate(getSetting(pData, pDefs, "Name"));
allReady = false;
}
}
// AIs are always ready.
for (let playerid = 0; playerid < g_MaxPlayers; ++playerid)
{
if (!g_GameAttributes.settings.PlayerData[playerid])
continue;
let pData = g_GameAttributes.settings.PlayerData ? g_GameAttributes.settings.PlayerData[playerid] : {};
let pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[playerid] : {};
if (isAI[playerid + 1])
Engine.GetGUIObjectByName("playerName[" + playerid + "]").caption = '[color="' + g_ReadyColor + '"]' + translate(getSetting(pData, pDefs, "Name")) + '[/color]';
}
// The host is not allowed to start until everyone is ready.
if (g_IsNetworked && g_IsController)
{
let startGameButton = Engine.GetGUIObjectByName("startGame");
startGameButton.enabled = allReady;
// Add a explanation on to the tooltip if disabled.
let disabledIndex = startGameButton.tooltip.indexOf('Disabled');
if (disabledIndex != -1 && allReady)
startGameButton.tooltip = startGameButton.tooltip.substring(0, disabledIndex - 2);
else if (disabledIndex == -1 && !allReady)
startGameButton.tooltip = startGameButton.tooltip + " (Disabled until all players are ready)";
}
}
function resetReadyData()
{
if (g_GameStarted)
return;
if (g_ReadyChanged < 1)
addChatMessage({ "type": "settings" });
else if (g_ReadyChanged == 2 && !g_ReadyInit)
return; // duplicate calls on init
else
g_ReadyInit = false;
g_ReadyChanged = 2;
if (!g_IsNetworked)
g_IsReady = true;
else if (g_IsController)
{
Engine.ClearAllPlayerReady();
g_IsReady = true;
Engine.SendNetworkReady(1);
}
else
{
g_IsReady = false;
Engine.GetGUIObjectByName("startGame").caption = translate("I'm ready!");
Engine.GetGUIObjectByName("startGame").tooltip = translate("State that you accept the current settings and are ready to play!");
}
}
/**
* Send the relevant gamesettings to the lobbybot.
*/
function sendRegisterGameStanza()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
let selectedMapSize = Engine.GetGUIObjectByName("mapSize").selected;
let selectedVictoryCondition = Engine.GetGUIObjectByName("victoryCondition").selected;
let mapSize = g_GameAttributes.mapType == "random" ? Engine.GetGUIObjectByName("mapSize").list_data[selectedMapSize] : "Default";
let victoryCondition = Engine.GetGUIObjectByName("victoryCondition").list[selectedVictoryCondition];
let playerNames = Object.keys(g_PlayerAssignments).map(guid => g_PlayerAssignments[guid].name);
Engine.SendRegisterGame({
"name": g_ServerName,
"mapName": g_GameAttributes.map,
"niceMapName": getMapDisplayName(g_GameAttributes.map),
"mapSize": mapSize,
"mapType": g_GameAttributes.mapType,
"victoryCondition": victoryCondition,
"nbp": Object.keys(g_PlayerAssignments).length || 1,
"tnbp": g_GameAttributes.settings.PlayerData.length,
"players": playerNames.join(", ")
});
}
Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.xml (revision 17729)
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.xml (revision 17730)
@@ -1,415 +1,416 @@
+
Match SetupLoadingLoading map data. Please wait...
onTick();
Player NameColorPlayer PlacementCivilizationView civilization infoTeamPick a color.Select player.Configure AI settings.Select player's civilization.Select player's team.Map Type:Map Filter:Select Map:Number of Players:Map Size:Select a map type.Select a map filter.Select a map to play on.Select number of players.Select map size. (Larger sizes may reduce performance.)submitChatInput();
var players = [];
for (var player in g_PlayerAssignments)
players.push(g_PlayerAssignments[player]);
autoCompleteNick("chatInput", players);
SendsubmitChatInput();Cheats enabled.Start game!Start a new game with the current settings.
if (g_IsController)
launchGame();
else
toggleReady();
BackcancelSetup();More OptionsSee more game optionsshowMoreOptions(true);More OptionsGame Speed:Select game speed.Victory Condition:Select victory condition.Population Cap:Select population cap.Starting Resources:Select the game's starting resources.Ceasefire:Set time where no attacks are possible.Revealed Map:Toggle revealed map (see everything).Explored Map:Toggle explored map (see initial map).Disable Treasures:Disable all treasures on the map.Teams Locked:Toggle locked teams.Cheats:Toggle the usability of cheats.Rated Game:Toggle if this game will be rated for the leaderboard.Late Observer Joins:Allow observers to join after the game started.OKClose more game options windowshowMoreOptions(false);
Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup_mp.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup_mp.js (revision 17729)
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup_mp.js (revision 17730)
@@ -1,262 +1,268 @@
var g_IsConnecting = false;
var g_GameType; // "server" or "client"
var g_ServerName = "";
var g_IsRejoining = false;
var g_GameAttributes; // used when rejoining
var g_PlayerAssignments; // used when rejoining
var g_UserRating; // player rating
function init(attribs)
{
g_UserRating = attribs.rating;
switch (attribs.multiplayerGameType)
{
case "join":
if(Engine.HasXmppClient())
{
if (startJoin(attribs.name, attribs.ip))
switchSetupPage("pageJoin", "pageConnecting");
}
else
{
Engine.GetGUIObjectByName("pageJoin").hidden = false;
Engine.GetGUIObjectByName("pageHost").hidden = true;
}
break;
case "host":
Engine.GetGUIObjectByName("pageJoin").hidden = true;
Engine.GetGUIObjectByName("pageHost").hidden = false;
if(Engine.HasXmppClient())
{
Engine.GetGUIObjectByName("hostServerNameWrapper").hidden = false;
Engine.GetGUIObjectByName("hostPlayerName").caption = attribs.name;
Engine.GetGUIObjectByName("hostServerName").caption = sprintf(translate("%(name)s's game"), { name: attribs.name });
}
else
Engine.GetGUIObjectByName("hostPlayerNameWrapper").hidden = false;
break;
default:
error(sprintf("Unrecognised multiplayer game type: %(gameType)s", { gameType: multiplayerGameType }));
break;
}
}
function cancelSetup()
{
if (g_IsConnecting)
Engine.DisconnectNetworkGame();
// Set player lobby presence
if (Engine.HasXmppClient())
Engine.LobbySetPlayerPresence("available");
Engine.PopGuiPage();
}
function startConnectionStatus(type)
{
g_GameType = type;
g_IsConnecting = true;
g_IsRejoining = false;
Engine.GetGUIObjectByName("connectionStatus").caption = translate("Connecting to server...");
}
function onTick()
{
if (!g_IsConnecting)
return;
pollAndHandleNetworkClient();
}
function pollAndHandleNetworkClient()
{
while (true)
{
var message = Engine.PollNetworkClient();
if (!message)
break;
log(sprintf(translate("Net message: %(message)s"), { message: uneval(message) }));
// If we're rejoining an active game, we don't want to actually display
// the game setup screen, so perform similar processing to gamesetup.js
// in this screen
if (g_IsRejoining)
{
switch (message.type)
{
case "netstatus":
switch (message.status)
{
case "disconnected":
cancelSetup();
reportDisconnect(message.reason);
return;
default:
error(sprintf("Unrecognised netstatus type %(netType)s", { netType: message.status }));
break;
}
break;
case "gamesetup":
g_GameAttributes = message.data;
break;
case "players":
g_PlayerAssignments = message.hosts;
break;
case "start":
// Copy playernames from initial player assignment to the settings
for (let guid in g_PlayerAssignments)
{
let player = g_PlayerAssignments[guid];
if (player.player > 0) // not observer or GAIA
g_GameAttributes.settings.PlayerData[player.player - 1].Name = player.name;
}
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": g_GameAttributes,
"isNetworked" : true,
"isRejoining" : g_IsRejoining,
"playerAssignments": g_PlayerAssignments
});
break;
case "chat":
- // Ignore, since we have nowhere to display chat messages
+ break;
+
+ case "netwarn":
break;
default:
error(sprintf("Unrecognised net message type %(messageType)s", { messageType: message.type }));
}
}
else
{
// Not rejoining - just trying to connect to server
switch (message.type)
{
case "netstatus":
switch (message.status)
{
case "connected":
Engine.GetGUIObjectByName("connectionStatus").caption = translate("Registering with server...");
break;
case "authenticated":
if (message.rejoining)
{
Engine.GetGUIObjectByName("connectionStatus").caption = translate("Game has already started, rejoining...");
g_IsRejoining = true;
return; // we'll process the game setup messages in the next tick
}
else
{
Engine.SwitchGuiPage("page_gamesetup.xml", { "type": g_GameType, "serverName": g_ServerName });
return; // don't process any more messages - leave them for the game GUI loop
}
case "disconnected":
cancelSetup();
reportDisconnect(message.reason);
return;
default:
error(sprintf("Unrecognised netstatus type %(netType)s", { netType: message.status }));
break;
}
break;
+
+ case "netwarn":
+ break;
+
default:
error(sprintf("Unrecognised net message type %(messageType)s", { messageType: message.type }));
break;
}
}
}
}
function switchSetupPage(oldpage, newpage)
{
Engine.GetGUIObjectByName(oldpage).hidden = true;
Engine.GetGUIObjectByName(newpage).hidden = false;
}
function startHost(playername, servername)
{
// Save player name
Engine.ConfigDB_CreateValue("user", "playername", playername);
Engine.ConfigDB_WriteFile("user", "config/user.cfg");
// Disallow identically named games in the multiplayer lobby
if (Engine.HasXmppClient())
{
for each (var g in Engine.GetGameList())
{
if (g.name === servername)
{
Engine.GetGUIObjectByName("hostFeedback").caption = translate("Game name already in use.");
return false;
}
}
}
try
{
if (g_UserRating)
Engine.StartNetworkHost(playername + " (" + g_UserRating + ")");
else
Engine.StartNetworkHost(playername);
}
catch (e)
{
cancelSetup();
messageBox(400, 200,
sprintf("Cannot host game: %(message)s.", { message: e.message }),
"Error", 2);
return false;
}
startConnectionStatus("server");
g_ServerName = servername;
// Set player lobby presence
if (Engine.HasXmppClient())
Engine.LobbySetPlayerPresence("playing");
return true;
}
function startJoin(playername, ip)
{
try
{
if (g_UserRating)
Engine.StartNetworkJoin(playername + " (" + g_UserRating + ")", ip);
else
Engine.StartNetworkJoin(playername, ip);
}
catch (e)
{
cancelSetup();
messageBox(400, 200,
sprintf("Cannot join game: %(message)s.", { message: e.message }),
"Error", 2);
return false;
}
startConnectionStatus("client");
if (Engine.HasXmppClient())
// Set player lobby presence
Engine.LobbySetPlayerPresence("playing");
else {
// Only save the player name and host address if they're valid and we're not in the lobby
Engine.ConfigDB_CreateValue("user", "playername", playername);
Engine.ConfigDB_CreateValue("user", "multiplayerserver", ip);
Engine.ConfigDB_WriteFile("user", "config/user.cfg");
}
return true;
}
function getDefaultGameName()
{
return sprintf(translate("%(playername)s's game"), { playername: Engine.ConfigDB_GetValue("user", "playername")});
}
Index: ps/trunk/binaries/data/mods/public/gui/options/options.json
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 17729)
+++ ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 17730)
@@ -1,214 +1,220 @@
{
"generalSetting":
[
{
"type": "boolean",
"label": "Windowed Mode",
"tooltip": "Start 0 A.D. in a window",
"parameters": { "config": "windowed" }
},
{
"type": "boolean",
"label": "Background Pause",
"tooltip": "Pause single player games when window loses focus",
"parameters": { "config": "pauseonfocusloss" }
},
{
"type": "boolean",
"label": "Disable Welcome Screen",
"tooltip": "If you disable this screen completely, you may miss important announcements.\nYou can still launch it using the main menu.",
"parameters": { "config": "splashscreendisable" }
},
{
"type": "boolean",
"label": "Detailed Tooltips",
"tooltip": "Show detailed tooltips for trainable units in unit-producing buildings.",
"parameters": { "config": "showdetailedtooltips" }
},
{
"type": "boolean",
+ "label": "Network Warnings",
+ "tooltip": "Show which player has a bad connection in multiplayer games.",
+ "parameters": { "config": "overlay.netwarnings" }
+ },
+ {
+ "type": "boolean",
"label": "FPS Overlay",
"tooltip": "Show frames per second in top right corner.",
"parameters": { "config": "overlay.fps" }
},
{
"type": "boolean",
"label": "Realtime Overlay",
"tooltip": "Show current system time in top right corner.",
"parameters": { "config": "overlay.realtime" }
},
{
"type": "boolean",
"label": "Gametime Overlay",
"tooltip": "Show current simulation time in top right corner.",
"parameters": { "config": "gui.session.timeelapsedcounter" }
},
{
"type": "boolean",
"label": "Ceasefire Time Overlay",
"tooltip": "Always show the remaining ceasefire time.",
"parameters": { "config": "gui.session.ceasefirecounter" }
},
{
"type": "boolean",
"label": "Persist Match Settings",
"tooltip": "Save and restore match settings for quick reuse when hosting another game",
"parameters": { "config": "persistmatchsettings" }
}
],
"graphicsSetting":
[
{
"type": "boolean",
"label": "Prefer GLSL",
"tooltip": "Use OpenGL 2.0 shaders (recommended)",
"parameters": { "renderer": "PreferGLSL", "config": "preferglsl" }
},
{
"type": "boolean",
"label": "Post Processing",
"tooltip": "Use screen-space postprocessing filters (HDR, Bloom, DOF, etc)",
"parameters": { "renderer": "Postproc", "config": "postproc" }
},
{
"type": "dropdown",
"label": "Graphics quality",
"tooltip": "Graphics quality. REQUIRES GAME RESTART",
"parameters": { "list": [ "Low", "Medium", "High" ], "config": "materialmgr.quality" }
},
{
"type": "boolean",
"label": "Shadows",
"tooltip": "Enable shadows",
"parameters": { "renderer": "Shadows", "config": "shadows"},
"dependencies": [ "shadowpcf" ]
},
{
"type": "boolean",
"label": "Shadow Filtering",
"tooltip": "Smooth shadows",
"parameters": { "renderer": "ShadowPCF", "config": "shadowpcf" }
},
{
"type": "boolean",
"label": "Unit Silhouettes",
"tooltip": "Show outlines of units behind buildings",
"parameters": { "renderer": "Silhouettes", "config": "silhouettes" }
},
{
"type": "boolean",
"label": "Particles",
"tooltip": "Enable particles",
"parameters": { "renderer": "Particles", "config": "particles" }
},
{
"type": "invertedboolean",
"label": "Activate water effects",
"tooltip": "When OFF, use the lowest settings possible to render water. This makes other settings irrelevant.",
"parameters": { "renderer": "WaterUgly", "config": "waterugly" },
"dependencies": [ "waterfancyeffects", "waterrealdepth", "waterreflection", "waterrefraction", "watershadows" ]
},
{
"type": "boolean",
"label": "HQ Water Effects",
"tooltip": "Use higher-quality effects for water, rendering coastal waves, shore foam, and ships trails.",
"parameters": { "renderer": "WaterFancyEffects", "config": "waterfancyeffects" }
},
{
"type": "boolean",
"label": "Real Water Depth",
"tooltip": "Use actual water depth in rendering calculations",
"parameters": { "renderer": "WaterRealDepth", "config": "waterrealdepth" }
},
{
"type": "boolean",
"label": "Water Reflections",
"tooltip": "Allow water to reflect a mirror image",
"parameters": { "renderer": "WaterReflection", "config": "waterreflection" }
},
{
"type": "boolean",
"label": "Water Refraction",
"tooltip": "Use a real water refraction map and not transparency",
"parameters": { "renderer": "WaterRefraction", "config": "waterrefraction" }
},
{
"type": "boolean",
"label": "Shadows on Water",
"tooltip": "Cast shadows on water",
"parameters": { "renderer": "WaterShadows", "config": "watershadows" }
},
{
"type": "boolean",
"label": "Smooth LOS",
"tooltip": "Lift darkness and fog-of-war smoothly",
"parameters": { "renderer": "SmoothLOS", "config": "smoothlos" }
},
{
"type": "boolean",
"label": "Show Sky",
"tooltip": "Render Sky",
"parameters": { "renderer": "ShowSky", "config": "showsky" }
},
{
"type": "boolean",
"label": "VSync",
"tooltip": "Run vertical sync to fix screen tearing. REQUIRES GAME RESTART",
"parameters": { "config": "vsync" }
},
{
"type": "boolean",
"label": "Limit FPS in Menus",
"tooltip": "Limit FPS to 50 in all menus, to save power.",
"parameters": { "config": "gui.menu.limitfps" }
}
],
"soundSetting":
[
{
"type": "number",
"label": "Master Gain",
"tooltip": "Master audio gain",
"parameters": { "config": "sound.mastergain", "function": "SetMasterGain", "min": "0" }
},
{
"type": "number",
"label": "Music Gain",
"tooltip": "In game music gain",
"parameters": { "config": "sound.musicgain", "function": "SetMusicGain", "min": "0" }
},
{
"type": "number",
"label": "Ambient Gain",
"tooltip": "In game ambient sound gain",
"parameters": { "config": "sound.ambientgain", "function": "SetAmbientGain", "min": "0" }
},
{
"type": "number",
"label": "Action Gain",
"tooltip": "In game unit action sound gain",
"parameters": { "config": "sound.actiongain", "function": "SetActionGain", "min": "0" }
},
{
"type": "number",
"label": "UI Gain",
"tooltip": "UI sound gain",
"parameters": { "config": "sound.uigain", "function": "SetUIGain", "min": "0" }
}
],
"lobbySetting":
[
{
"type": "number",
"label": "Chat Backlog",
"tooltip": "Number of backlogged messages to load when joining the lobby",
"parameters": { "config": "lobby.history", "min": "0" }
},
{
"type": "boolean",
"label": "Chat Timestamp",
"tooltip": "Show time that messages are posted in the lobby chat",
"parameters": { "config": "lobby.chattimestamp" }
}
]
}
Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 17729)
+++ ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 17730)
@@ -1,786 +1,787 @@
/**
* All known cheat commands.
* @type {Object}
*/
const g_Cheats = getCheatsData();
/**
* Number of seconds after which chatmessages will disappear.
*/
const g_ChatTimeout = 30;
/**
* Maximum number of lines to display simultaneously.
*/
const g_ChatLines = 20;
/**
* The strings to be displayed including sender and formating.
*/
var g_ChatMessages = [];
/**
* Holds the timer-IDs used for hiding the chat after g_ChatTimeout seconds.
*/
var g_ChatTimers = [];
/**
* Handle all netmessage types that can occur.
*/
var g_NetMessageTypes = {
"netstatus": msg => handleNetStatusMessage(msg),
+ "netwarn": msg => addNetworkWarning(msg),
"players": msg => handlePlayerAssignmentsMessage(msg),
"rejoined": msg => addChatMessage({ "type": "rejoined", "guid": msg.guid }),
"kicked": msg => addChatMessage({ "type": "system", "text": sprintf(translate("%(username)s has been kicked"), { "username": msg.username }) }),
"banned": msg => addChatMessage({ "type": "system", "text": sprintf(translate("%(username)s has been banned"), { "username": msg.username }) }),
"chat": msg => addChatMessage({ "type": "message", "guid": msg.guid, "text": msg.text }),
"aichat": msg => addChatMessage({ "type": "message", "guid": msg.guid, "text": msg.text, "translate": true }),
"gamesetup": msg => "", // Needed for autostart
"start": msg => ""
};
var g_FormatChatMessage = {
"system": msg => msg.text,
"connect": msg => sprintf(translate("%(player)s is starting to rejoin the game."), { "player": colorizePlayernameByGUID(msg.guid) }),
"disconnect": msg => sprintf(translate("%(player)s has left the game."), { "player": colorizePlayernameByGUID(msg.guid) }),
"rejoined": msg => sprintf(translate("%(player)s has rejoined the game."), { "player": colorizePlayernameByGUID(msg.guid) }),
"clientlist": msg => getUsernameList(),
"message": msg => formatChatCommand(msg),
"defeat": msg => formatDefeatMessage(msg),
"diplomacy": msg => formatDiplomacyMessage(msg),
"tribute": msg => formatTributeMessage(msg),
"attack": msg => formatAttackMessage(msg)
};
/**
* Show a label and grey overlay or hide both on connection change.
*/
var g_StatusMessageTypes = {
"authenticated": msg => translate("Connection to the server has been authenticated."),
"connected": msg => translate("Connected to the server."),
"disconnected": msg => translate("Connection to the server has been lost.") + "\n" +
// Translation: States the reason why the client disconnected from the server.
sprintf(translate("Reason: %(reason)s."), { "reason": getDisconnectReason(msg.reason) }),
"waiting_for_players": msg => translate("Waiting for other players to connect..."),
"join_syncing": msg => translate("Synchronising gameplay with other players..."),
"active": msg => ""
};
/**
* Chatmessage shown after commands like /me or /enemies.
*/
var g_ChatCommands = {
"regular": {
"context": translate("(%(context)s) %(userTag)s %(message)s"),
"no-context": translate("%(userTag)s %(message)s")
},
"me": {
"context": translate("(%(context)s) * %(user)s %(message)s"),
"no-context": translate("* %(user)s %(message)s")
}
};
var g_ChatAddresseeContext = {
"/team": translate("Team"),
"/allies": translate("Ally"),
"/enemies": translate("Enemy"),
"/msg": translate("Private")
};
/**
* Returns true if the current player is an addressee, given the chat message type and sender.
*/
var g_IsChatAddressee = {
"/team": senderID =>
g_Players[senderID] &&
g_Players[Engine.GetPlayerID()] &&
g_Players[Engine.GetPlayerID()].team != -1 &&
g_Players[Engine.GetPlayerID()].team == g_Players[senderID].team,
"/allies": senderID =>
g_Players[senderID] &&
g_Players[senderID].isMutualAlly[Engine.GetPlayerID()],
"/enemies": senderID =>
g_Players[senderID] &&
g_Players[senderID].isEnemy[Engine.GetPlayerID()],
"/msg": (senderID, addresseeGUID) =>
g_Players[Engine.GetPlayerID()] &&
g_PlayerAssignments[addresseeGUID] &&
g_PlayerAssignments[addresseeGUID].name == g_Players[Engine.GetPlayerID()].name
};
/**
* Chatmessage shown on diplomacy change.
*/
var g_DiplomacyMessages = {
"active": {
"ally": translate("You are now allied with %(player)s."),
"enemy": translate("You are now at war with %(player)s."),
"neutral": translate("You are now neutral with %(player)s.")
},
"passive": {
"ally": translate("%(player)s is now allied with you."),
"enemy": translate("%(player)s is now at war with you."),
"neutral": translate("%(player)s is now neutral with you.")
},
"observer": {
"ally": translate("%(player)s is now allied with %(player2)s."),
"enemy": translate("%(player)s is now at war with %(player2)s."),
"neutral": translate("%(player)s is now neutral with %(player2)s.")
}
};
/**
* Defines how the GUI reacts to notifications that are sent by the simulation.
*/
var g_NotificationsTypes =
{
"chat": function(notification, player)
{
let message = {
"type": "message",
"guid": findGuidForPlayerID(player) || -1,
"text": notification.message
};
if (message.guid == -1)
message.player = player;
addChatMessage(message);
},
"aichat": function(notification, player)
{
let message = {
"guid": findGuidForPlayerID(player) || -1,
"type": "message",
"text": notification.message,
"translate": true
};
if (message.guid == -1)
message.player = player;
if (notification.translateParameters)
{
message.translateParameters = notification.translateParameters;
message.parameters = notification.parameters;
// special case for formatting of player names which are transmitted as _player_num
for (let param in message.parameters)
{
if (!param.startsWith("_player_"))
continue;
message.parameters[param] = colorizePlayernameByID(message.parameters[param]);
}
}
addChatMessage(message);
},
"defeat": function(notification, player)
{
addChatMessage({
"type": "defeat",
"guid": findGuidForPlayerID(player),
"player": player
});
updateDiplomacy();
},
"diplomacy": function(notification, player)
{
addChatMessage({
"type": "diplomacy",
"sourcePlayer": player,
"targetPlayer": notification.targetPlayer,
"status": notification.status
});
updateDiplomacy();
},
"quit": function(notification, player)
{
Engine.Exit();
},
"tribute": function(notification, player)
{
addChatMessage({
"type": "tribute",
"sourcePlayer": notification.donator,
"targetPlayer": player,
"amounts": notification.amounts
});
},
"attack": function(notification, player)
{
if (player != Engine.GetPlayerID())
return;
if (Engine.ConfigDB_GetValue("user", "gui.session.attacknotificationmessage") !== "true")
return;
addChatMessage({
"type": "attack",
"player": player,
"attacker": notification.attacker,
"targetIsDomesticAnimal": notification.targetIsDomesticAnimal
});
},
"dialog": function(notification, player)
{
if (player == Engine.GetPlayerID())
openDialog(notification.dialogName, notification.data, player);
},
"resetselectionpannel": function(notification, player)
{
if (player != Engine.GetPlayerID())
return;
g_Selection.rebuildSelection({});
}
};
/**
* Loads all known cheat commands.
*
* @returns {Object}
*/
function getCheatsData()
{
let cheats = {};
for (let fileName of getJSONFileList("simulation/data/cheats/"))
{
let currentCheat = Engine.ReadJSONFile("simulation/data/cheats/"+fileName+".json");
if (!currentCheat)
continue;
if (Object.keys(cheats).indexOf(currentCheat.Name) !== -1)
warn("Cheat name '" + currentCheat.Name + "' is already present");
else
cheats[currentCheat.Name] = currentCheat.Data;
}
return cheats;
}
/**
* Reads userinput from the chat and sends a simulation command in case it is a known cheat.
* Hence cheats won't be sent as chat over network.
*
* @param {string} text
* @returns {boolean} - True if a cheat was executed.
*/
function executeCheat(text)
{
if (Engine.GetPlayerID() == -1 || !g_Players[Engine.GetPlayerID()].cheatsEnabled)
return false;
// Find the cheat code that is a prefix of the user input
let cheatCode = Object.keys(g_Cheats).find(cheatCode => text.indexOf(cheatCode) == 0);
if (!cheatCode)
return false;
let cheat = g_Cheats[cheatCode];
let parameter = text.substr(cheatCode.length);
if (cheat.isNumeric)
parameter = +parameter;
if (cheat.DefaultParameter && (isNaN(parameter) || parameter <= 0))
parameter = cheat.DefaultParameter;
Engine.PostNetworkCommand({
"type": "cheat",
"action": cheat.Action,
"text": cheat.Type,
"player": Engine.GetPlayerID(),
"parameter": parameter,
"templates": cheat.Templates,
"selected": g_Selection.toList()
});
return true;
}
function findGuidForPlayerID(playerID)
{
return Object.keys(g_PlayerAssignments).find(guid => g_PlayerAssignments[guid].player == playerID);
}
/**
* Processes all pending simulation messages.
*/
function handleNotifications()
{
let notifications = Engine.GuiInterfaceCall("GetNotifications");
for (let notification of notifications)
{
if (!notification.type)
{
error("Notification without type found.\n"+uneval(notification));
continue;
}
if (!notification.players)
{
error("Notification without players found.\n"+uneval(notification));
continue;
}
if (!g_NotificationsTypes[notification.type])
{
error("Unknown notification type '" + notification.type + "' found.");
continue;
}
for (let player of notification.players)
g_NotificationsTypes[notification.type](notification, player);
}
}
/**
* Updates playerdata cache and refresh diplomacy panel.
*/
function updateDiplomacy()
{
g_Players = getPlayerData(g_PlayerAssignments);
if (g_IsDiplomacyOpen)
openDiplomacy();
}
/**
* Displays all active counters (messages showing the remaining time) for wonder-victory, ceasefire etc.
*/
function updateTimeNotifications()
{
let notifications = Engine.GuiInterfaceCall("GetTimeNotifications");
let notificationText = "";
for (let n of notifications)
{
let message = n.message;
if (n.translateMessage)
message = translate(message);
let parameters = n.parameters || {};
if (n.translateParameters)
translateObjectKeys(parameters, n.translateParameters);
parameters.time = timeToString(n.endTime - g_SimState.timeElapsed);
notificationText += sprintf(message, parameters) + "\n";
}
Engine.GetGUIObjectByName("notificationText").caption = notificationText;
}
/**
* Processes a CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer.
* Saves the received object to mainlog.html.
*
* @param {Object} msg
*/
function handleNetMessage(msg)
{
log("Net message: " + uneval(msg));
if (g_NetMessageTypes[msg.type])
g_NetMessageTypes[msg.type](msg);
else
error("Unrecognised net message type '" + msg.type + "'");
}
/**
* @param {Object} message
*/
function handleNetStatusMessage(message)
{
if (g_Disconnected)
return;
if (!g_StatusMessageTypes[message.status])
{
error("Unrecognised netstatus type '" + message.status + "'");
return;
}
let label = Engine.GetGUIObjectByName("netStatus");
let statusMessage = g_StatusMessageTypes[message.status](message);
label.caption = statusMessage;
label.hidden = !statusMessage;
if (message.status == "disconnected")
{
g_Disconnected = true;
closeOpenDialogs();
}
}
function handlePlayerAssignmentsMessage(message)
{
// Find and report all leavings
for (let guid in g_PlayerAssignments)
{
if (message.hosts[guid])
continue;
addChatMessage({ "type": "disconnect", "guid": guid });
for (let id in g_Players)
if (g_Players[id].guid == guid)
g_Players[id].offline = true;
}
let joins = Object.keys(message.hosts).filter(guid => !g_PlayerAssignments[guid]);
g_PlayerAssignments = message.hosts;
// Report all joinings
joins.forEach(guid => {
let playerID = g_PlayerAssignments[guid].player;
if (g_Players[playerID])
{
g_Players[playerID].guid = guid;
g_Players[playerID].name = g_PlayerAssignments[guid].name;
g_Players[playerID].offline = false;
}
addChatMessage({ "type": "connect", "guid": guid });
});
// Update lobby gamestatus
if (g_IsController && Engine.HasXmppClient())
{
let players = Object.keys(g_PlayerAssignments).map(guid => g_PlayerAssignments[guid].name);
Engine.SendChangeStateGame(Object.keys(g_PlayerAssignments).length, players.join(", "));
}
}
/**
* Send text as chat. Don't look for commands.
*
* @param {string} text
*/
function submitChatDirectly(text)
{
if (!text.length)
return;
if (g_IsNetworked)
Engine.SendNetworkChat(text);
else
addChatMessage({ "type": "message", "guid": "local", "text": text });
}
/**
* Loads the text from the GUI window, checks if it is a local command
* or cheat and executes it. Otherwise sends it as chat.
*/
function submitChatInput()
{
let teamChat = Engine.GetGUIObjectByName("toggleTeamChat").checked;
let input = Engine.GetGUIObjectByName("chatInput");
let text = input.caption;
input.blur(); // Remove focus
input.caption = ""; // Clear chat input
toggleChatWindow();
if (!text.length)
return;
if (executeNetworkCommand(text))
return;
if (executeCheat(text))
return;
// Observers should only be able to chat with everyone.
if (g_IsObserver && text.indexOf("/") == 0 && text.indexOf("/me ") != 0)
return;
if (teamChat && text.indexOf("/team ") != 0)
text = "/team " + text;
submitChatDirectly(text);
}
/**
* Displays the prepared chatmessage.
*
* @param msg {Object}
*/
function addChatMessage(msg)
{
if (!g_FormatChatMessage[msg.type])
return;
let formatted = g_FormatChatMessage[msg.type](msg);
if (!formatted)
return;
g_ChatMessages.push(formatted);
g_ChatTimers.push(setTimeout(removeOldChatMessage, g_ChatTimeout * 1000));
if (g_ChatMessages.length > g_ChatLines)
removeOldChatMessage();
else
Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
}
/**
* Called when the timer has run out for the oldest chatmessage or when the message limit is reached.
*/
function removeOldChatMessage()
{
clearTimeout(g_ChatTimers[0]);
g_ChatTimers.shift();
g_ChatMessages.shift();
Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
}
/**
* This function is used for AIs, whose names don't exist in g_PlayerAssignments.
*/
function colorizePlayernameByID(playerID)
{
let username = playerID > -1 ? escapeText(g_Players[playerID].name) : translate("Unknown Player");
let playerColor = playerID > -1 ? rgbToGuiColor(g_Players[playerID].color) : "white";
return "[color=\"" + playerColor + "\"]" + username + "[/color]";
}
function colorizePlayernameByGUID(guid)
{
let username = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].name : translate("Unknown Player");
let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1;
let playerColor = playerID > 0 ? rgbToGuiColor(g_Players[playerID].color) : "white";
return "[color=\"" + playerColor + "\"]" + username + "[/color]";
}
function formatDefeatMessage(msg)
{
// In singleplayer, the local player is "You". "You has" is incorrect.
let message = !g_IsNetworked && msg.player == Engine.GetPlayerID() ?
translate("You have been defeated.") :
translate("%(player)s has been defeated.");
return sprintf(message, {
"player": colorizePlayernameByID(msg.player)
});
}
function formatDiplomacyMessage(msg)
{
let messageType;
switch (Engine.GetPlayerID())
{
// Check observer first, since we also want to see if the selected player in the developer-overlay has changed the diplomacy
case -1:
messageType = "observer";
break;
case msg.sourcePlayer:
messageType = "active";
break;
case msg.targetPlayer:
messageType = "passive";
break;
default:
return "";
}
return sprintf(g_DiplomacyMessages[messageType][msg.status], {
"player": colorizePlayernameByID(messageType == "active" ? msg.targetPlayer : msg.sourcePlayer),
"player2": colorizePlayernameByID(messageType == "active" ? msg.sourcePlayer : msg.targetPlayer)
});
}
function formatTributeMessage(msg)
{
// Check observer first, since we also want to see if the selected player in the developer-overlay has sent tributes
let message = "";
if (Engine.GetPlayerID() == -1)
message = translate("%(player)s has sent %(player2)s %(amounts)s.");
else if (msg.targetPlayer == Engine.GetPlayerID())
message = translate("%(player)s has sent you %(amounts)s.");
return sprintf(message, {
"player": colorizePlayernameByID(msg.sourcePlayer),
"player2": colorizePlayernameByID(msg.targetPlayer),
"amounts": getLocalizedResourceAmounts(msg.amounts)
});
}
function formatAttackMessage(msg)
{
if (msg.player != Engine.GetPlayerID())
return "";
let message = msg.targetIsDomesticAnimal ?
translate("Your livestock has been attacked by %(attacker)s!") :
translate("You have been attacked by %(attacker)s!");
return sprintf(message, {
"attacker": colorizePlayernameByID(msg.attacker)
});
}
function formatChatCommand(msg)
{
if (!msg.text)
return "";
let isMe = msg.text.indexOf("/me ") == 0;
if (!isMe && !checkChatAddressee(msg))
return "";
isMe = msg.text.indexOf("/me ") == 0;
if (isMe)
msg.text = msg.text.substr("/me ".length);
// Translate or escape text
if (!msg.text)
return "";
if (msg.translate)
{
msg.text = translate(msg.text);
if (msg.translateParameters)
{
let parameters = msg.parameters || {};
translateObjectKeys(parameters, msg.translateParameters);
msg.text = sprintf(msg.text, parameters);
}
}
else
msg.text = escapeText(msg.text);
// GUID for players, playerID for AIs
let coloredUsername = msg.guid != -1 ? colorizePlayernameByGUID(msg.guid) : colorizePlayernameByID(msg.player);
return sprintf(g_ChatCommands[isMe ? "me" : "regular"][msg.context ? "context" : "no-context"], {
"message": msg.text,
"context": msg.context || undefined,
"user": coloredUsername,
"userTag": sprintf(translate("<%(user)s>"), { "user": coloredUsername })
});
}
/**
* Checks if the current user is an addressee of the chatmessage sent by another player.
*
* @param {Object} msg
*/
function checkChatAddressee(msg)
{
if (msg.text[0] != '/')
return true;
if (Engine.GetPlayerID() == -1)
return false;
let cmd = msg.text.split(/\s/)[0];
msg.text = msg.text.substr(cmd.length + 1);
if (cmd == "/ally")
cmd = "/allies";
if (cmd == "/enemy")
cmd = "/enemies";
// GUID for players, ID for bots
let senderID = (g_PlayerAssignments[msg.guid] || msg).player;
let addresseeGUID;
if (cmd == "/msg")
{
addresseeGUID = matchUsername(msg.text);
let addressee = g_PlayerAssignments[addresseeGUID];
if (!addressee || addressee.player == -1 || senderID == -1)
return false;
msg.text = msg.text.substr(addressee.name.length + 1);
}
let isSender = senderID == Engine.GetPlayerID();
if (!g_ChatAddresseeContext[cmd])
{
if (isSender)
warn("Unknown chat command: " + cmd);
return false;
}
msg.context = g_ChatAddresseeContext[cmd];
return isSender || g_IsChatAddressee[cmd](senderID, addresseeGUID);
}
/**
* Returns the guid of the user with the longest name that is a prefix of the given string.
*/
function matchUsername(text)
{
if (!text)
return "";
let match = "";
let playerGUID = "";
for (let guid in g_PlayerAssignments)
{
let pName = g_PlayerAssignments[guid].name;
if (text.indexOf(pName + " ") == 0 && pName.length > match.length)
{
match = pName;
playerGUID = guid;
}
}
return playerGUID;
}
/**
* Unused multiplayer-dialog.
*/
function sendDialogAnswer(guiObject, dialogName)
{
Engine.GetGUIObjectByName(dialogName+"-dialog").hidden = true;
Engine.PostNetworkCommand({
"type": "dialog-answer",
"dialog": dialogName,
"answer": guiObject.name.split("-").pop(),
});
resumeGame();
}
/**
* Unused multiplayer-dialog.
*/
function openDialog(dialogName, data, player)
{
let dialog = Engine.GetGUIObjectByName(dialogName + "-dialog");
if (!dialog)
{
warn("messages.js: Unknow dialog with name " + dialogName);
return;
}
dialog.hidden = false;
for (let objName in data)
{
let obj = Engine.GetGUIObjectByName(dialogName + "-dialog-" + objName);
if (!obj)
{
warn("messages.js: Key '" + objName + "' not found in '" + dialogName + "' dialog.");
continue;
}
for (let key in data[objName])
{
let n = data[objName][key];
if (typeof n == "object" && n.message)
{
let message = n.message;
if (n.translateMessage)
message = translate(message);
let parameters = n.parameters || {};
if (n.translateParameters)
translateObjectKeys(parameters, n.translateParameters);
obj[key] = sprintf(message, parameters);
}
else
obj[key] = n;
}
}
pauseGame();
}
Index: ps/trunk/binaries/data/mods/public/gui/session/session.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 17729)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 17730)
@@ -1,1216 +1,1218 @@
const g_IsReplay = Engine.IsVisualReplay();
const g_GameSpeeds = prepareForDropdown(g_Settings ? g_Settings.GameSpeeds.filter(speed => !speed.ReplayOnly || g_IsReplay) : undefined);
/**
* Colors to flash when pop limit reached.
*/
const g_DefaultPopulationColor = "white";
const g_PopulationAlertColor = "orange";
/**
* A random file will be played. TODO: more variety
*/
const g_Ambient = [ "audio/ambient/dayscape/day_temperate_gen_03.ogg" ];
/**
* Is this user in control of game settings (i.e. is a network server, or offline player).
*/
var g_IsController;
/**
* True if this is a multiplayer game.
*/
var g_IsNetworked = false;
/**
* True if the connection to the server has been lost.
*/
var g_Disconnected = false;
/**
* True if the current user has observer capabilities.
*/
var g_IsObserver = false;
/**
* The playerID selected in the change perspective tool.
*/
var g_ViewedPlayer = Engine.GetPlayerID();
/**
* Unique ID for lobby reports.
*/
var g_MatchID;
/**
* Cache the basic player data (name, civ, color).
*/
var g_Players = [];
/**
* Last time when onTick was called().
* Used for animating the main menu.
*/
var lastTickTime = new Date();
/**
* Not constant as we add "gaia".
**/
var g_CivData = {};
var g_PlayerAssignments = { "local": { "name": translate("You"), "player": 1 } };
/**
* Cache dev-mode settings that are frequently or widely used.
*/
var g_DevSettings = {
"changePerspective": false,
"controlAll": false
};
/**
* Whether status bars should be shown for all of the player's units.
*/
var g_ShowAllStatusBars = false;
/**
* Blink the population counter if the player can't train more units.
*/
var g_IsTrainingBlocked = false;
/**
* Cache simulation state (updated on every simulation update).
*/
var g_SimState;
var g_EntityStates = {};
var g_TemplateData = {};
var g_TemplateDataWithoutLocalization = {};
var g_TechnologyData = {};
/**
* Cache concatenated list of player states ("active", "defeated" or "won").
*/
var g_CachedLastStates = "";
/**
* Whether the current player has lost/won and reached the end of their game.
* Used for reporting the gamestate and showing the game-end message only once.
*/
var g_GameEnded = false;
/**
* Top coordinate of the research list.
* Changes depending on the number of displayed counters.
*/
var g_ResearchListTop = 4;
/**
* List of additional entities to highlight.
*/
var g_ShowGuarding = false;
var g_ShowGuarded = false;
var g_AdditionalHighlight = [];
/**
* Blink the hero selection if that entity has lost health since the last turn.
*/
var g_PreviousHeroHitPoints;
/**
* Unit classes to be checked for the idle-worker-hotkey.
*/
var g_WorkerTypes = ["Female", "Trader", "FishingBoat", "CitizenSoldier", "Healer"];
/**
* Cache the idle worker status.
*/
var g_HasIdleWorker = false;
function GetSimState()
{
if (!g_SimState)
g_SimState = Engine.GuiInterfaceCall("GetSimulationState");
return g_SimState;
}
function GetEntityState(entId)
{
if (!g_EntityStates[entId])
g_EntityStates[entId] = Engine.GuiInterfaceCall("GetEntityState", entId);
return g_EntityStates[entId];
}
function GetExtendedEntityState(entId)
{
let entState = GetEntityState(entId);
if (!entState || entState.extended)
return entState;
let extension = Engine.GuiInterfaceCall("GetExtendedEntityState", entId);
for (let prop in extension)
entState[prop] = extension[prop];
entState.extended = true;
g_EntityStates[entId] = entState;
return entState;
}
function GetTemplateData(templateName)
{
if (!(templateName in g_TemplateData))
{
let template = Engine.GuiInterfaceCall("GetTemplateData", templateName);
translateObjectKeys(template, ["specific", "generic", "tooltip"]);
g_TemplateData[templateName] = template;
}
return g_TemplateData[templateName];
}
function GetTemplateDataWithoutLocalization(templateName)
{
if (!(templateName in g_TemplateDataWithoutLocalization))
{
let template = Engine.GuiInterfaceCall("GetTemplateData", templateName);
g_TemplateDataWithoutLocalization[templateName] = template;
}
return g_TemplateDataWithoutLocalization[templateName];
}
function GetTechnologyData(technologyName)
{
if (!(technologyName in g_TechnologyData))
{
let template = Engine.GuiInterfaceCall("GetTechnologyData", technologyName);
translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]);
g_TechnologyData[technologyName] = template;
}
return g_TechnologyData[technologyName];
}
function init(initData, hotloadData)
{
if (!g_Settings)
{
Engine.EndGame();
Engine.SwitchGuiPage("page_pregame.xml");
return;
}
if (initData)
{
g_IsNetworked = initData.isNetworked;
g_IsController = initData.isController;
g_PlayerAssignments = initData.playerAssignments;
g_MatchID = initData.attribs.matchID;
// Cache the player data
// (This may be updated at runtime by handleNetMessage)
g_Players = getPlayerData(g_PlayerAssignments);
if (initData.savedGUIData)
restoreSavedGameData(initData.savedGUIData);
Engine.GetGUIObjectByName("gameSpeedButton").hidden = g_IsNetworked;
}
else // Needed for autostart loading option
{
if (g_IsReplay)
g_PlayerAssignments.local.player = -1;
g_Players = getPlayerData(null);
}
g_CivData = loadCivData();
g_CivData.gaia = { "Code": "gaia", "Name": translate("Gaia") };
setObserverMode(Engine.GetPlayerID() == -1);
updateTopPanel();
let gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
gameSpeed.list = g_GameSpeeds.Title;
gameSpeed.list_data = g_GameSpeeds.Speed;
let gameSpeedIdx = g_GameSpeeds.Speed.indexOf(Engine.GetSimRate());
gameSpeed.selected = gameSpeedIdx != -1 ? gameSpeedIdx : g_GameSpeeds.Default;
gameSpeed.onSelectionChange = function() { changeGameSpeed(+this.list_data[this.selected]); };
initMenuPosition();
// Populate player selection dropdown
let playerNames = ["Observer"];
let playerIDs = [-1];
for (let player in g_Players)
{
playerNames.push(g_Players[player].name);
playerIDs.push(player);
}
let viewPlayerDropdown = Engine.GetGUIObjectByName("viewPlayer");
viewPlayerDropdown.list = playerNames;
viewPlayerDropdown.list_data = playerIDs;
viewPlayerDropdown.selected = Engine.GetPlayerID() + 1;
// If in Atlas editor, disable the exit button
if (Engine.IsAtlasRunning())
Engine.GetGUIObjectByName("menuExitButton").enabled = false;
if (hotloadData)
g_Selection.selected = hotloadData.selection;
// Starting for the first time:
initMusic();
if (Engine.GetPlayerID() != -1)
global.music.storeTracks(g_CivData[g_Players[Engine.GetPlayerID()].civ].Music);
global.music.setState(global.music.states.PEACE);
playAmbient();
onSimulationUpdate();
+ setTimeout(displayGamestateNotifications, 1000);
+
// Report the performance after 5 seconds (when we're still near
// the initial camera view) and a minute (when the profiler will
// have settled down if framerates as very low), to give some
// extremely rough indications of performance
//
// DISABLED: this information isn't currently useful for anything much,
// and it generates a massive amount of data to transmit and store
//setTimeout(function() { reportPerformance(5); }, 5000);
//setTimeout(function() { reportPerformance(60); }, 60000);
}
function toggleChangePerspective(enabled)
{
g_DevSettings.changePerspective = enabled;
Engine.GetGUIObjectByName("viewPlayer").hidden = !enabled && !g_IsObserver;
selectViewPlayer(g_ViewedPlayer);
}
/**
* Change perspective tool.
* Shown to observers or when enabling the developers option.
*/
function selectViewPlayer(playerID)
{
if (playerID < -1 || playerID > g_Players.length - 1 ||
!g_DevSettings.changePerspective && !g_IsObserver)
return;
g_ViewedPlayer = playerID;
Engine.SetPlayerID(g_DevSettings.changePerspective ? playerID : -1);
updateTopPanel();
onSimulationUpdate();
let viewPlayer = Engine.GetGUIObjectByName("viewPlayer");
let alphaLabel = Engine.GetGUIObjectByName("alphaLabel");
alphaLabel.hidden = g_ViewedPlayer > 0 && !viewPlayer.hidden;
alphaLabel.size = g_ViewedPlayer > 0 ? "50%+20 0 100%-226 100%" : "200 0 100%-475 100%";
if (g_IsDiplomacyOpen)
openDiplomacy();
if (g_IsTradeOpen)
openTrade();
}
function setObserverMode(enabled)
{
g_IsObserver = enabled;
let viewPlayerDropdown = Engine.GetGUIObjectByName("viewPlayer");
viewPlayerDropdown.hidden = !enabled;
if (enabled)
viewPlayerDropdown.selected = 0;
Engine.GetGUIObjectByName("alphaLabel").hidden = enabled;
}
/**
* Returns true if the current user can issue commands for that player.
*/
function controlsPlayer(playerID)
{
return Engine.GetPlayerID() == playerID || g_DevSettings.controlAll;
}
function updateTopPanel()
{
let isPlayer = g_ViewedPlayer > 0;
if (isPlayer)
{
let civName = g_CivData[g_Players[g_ViewedPlayer].civ].Name;
Engine.GetGUIObjectByName("civIcon").sprite = "stretched:" + g_CivData[g_Players[g_ViewedPlayer].civ].Emblem;
Engine.GetGUIObjectByName("civIconOverlay").tooltip = sprintf(translate("%(civ)s - Structure Tree"), { "civ": civName });
}
// Hide stuff gaia/observers don't use.
Engine.GetGUIObjectByName("food").hidden = !isPlayer;
Engine.GetGUIObjectByName("wood").hidden = !isPlayer;
Engine.GetGUIObjectByName("stone").hidden = !isPlayer;
Engine.GetGUIObjectByName("metal").hidden = !isPlayer;
Engine.GetGUIObjectByName("population").hidden = !isPlayer;
Engine.GetGUIObjectByName("civIcon").hidden = !isPlayer;
Engine.GetGUIObjectByName("diplomacyButton1").hidden = !isPlayer;
Engine.GetGUIObjectByName("tradeButton1").hidden = !isPlayer;
Engine.GetGUIObjectByName("observerText").hidden = isPlayer;
// Disable stuff observers shouldn't use
let isActive = isPlayer && GetSimState().players[g_ViewedPlayer].state == "active" && controlsPlayer(g_ViewedPlayer);
Engine.GetGUIObjectByName("pauseButton").enabled = isActive || !g_IsNetworked;
Engine.GetGUIObjectByName("menuResignButton").enabled = isActive;
// Enable observer-only "summary" button.
Engine.GetGUIObjectByName("summaryButton").enabled = Engine.GetPlayerID() == -1;
}
function reportPerformance(time)
{
let settings = Engine.GetMapSettings();
Engine.SubmitUserReport("profile", 3, JSON.stringify({
"time": time,
"map": settings.Name,
"seed": settings.Seed, // only defined for random maps
"size": settings.Size, // only defined for random maps
"profiler": Engine.GetProfilerState()
}));
}
/**
* Resign a player.
* @param leaveGameAfterResign If player is quitting after resignation.
*/
function resignGame(leaveGameAfterResign)
{
let simState = GetSimState();
// Players can't resign if they've already won or lost.
if (simState.players[Engine.GetPlayerID()].state != "active" || g_Disconnected)
return;
// Tell other players that we have given up and been defeated
Engine.PostNetworkCommand({
"type": "defeat-player",
"playerId": Engine.GetPlayerID()
});
updateTopPanel();
global.music.setState(global.music.states.DEFEAT);
// Resume the game if not resigning.
if (!leaveGameAfterResign)
resumeGame();
}
/**
* Leave the game
* @param willRejoin If player is going to be rejoining a networked game.
*/
function leaveGame(willRejoin)
{
let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
let mapSettings = Engine.GetMapSettings();
let gameResult;
if (Engine.GetPlayerID() == -1)
{
// Observers don't win/lose.
gameResult = translate("You have left the game.");
global.music.setState(global.music.states.VICTORY);
}
else
{
let playerState = extendedSimState.players[Engine.GetPlayerID()];
if (g_Disconnected)
gameResult = translate("You have been disconnected.");
else if (playerState.state == "won")
gameResult = translate("You have won the battle!");
else if (playerState.state == "defeated")
gameResult = translate("You have been defeated...");
else // "active"
{
global.music.setState(global.music.states.DEFEAT);
if (willRejoin)
gameResult = translate("You have left the game.");
else
{
gameResult = translate("You have abandoned the game.");
resignGame(true);
}
}
}
let summary = {
"timeElapsed" : extendedSimState.timeElapsed,
"playerStates": extendedSimState.players,
"players": g_Players,
"mapSettings": Engine.GetMapSettings(),
};
if (!g_IsReplay)
Engine.SaveReplayMetadata(JSON.stringify(summary));
Engine.EndGame();
if (g_IsController && Engine.HasXmppClient())
Engine.SendUnregisterGame();
summary.gameResult = gameResult;
summary.isReplay = g_IsReplay;
Engine.SwitchGuiPage("page_summary.xml", summary);
}
// Return some data that we'll use when hotloading this file after changes
function getHotloadData()
{
return { "selection": g_Selection.selected };
}
// Return some data that will be stored in saved game files
function getSavedGameData()
{
// TODO: any other gui state?
return {
"playerAssignments": g_PlayerAssignments,
"groups": g_Groups.groups
};
}
function restoreSavedGameData(data)
{
// Restore camera if any
if (data.camera)
Engine.SetCameraData(data.camera.PosX, data.camera.PosY, data.camera.PosZ,
data.camera.RotX, data.camera.RotY, data.camera.Zoom);
// Clear selection when loading a game
g_Selection.reset();
// Restore control groups
for (let groupNumber in data.groups)
{
g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups;
g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents;
}
updateGroups();
}
/**
* Called every frame.
*/
function onTick()
{
if (!g_Settings)
return;
let now = new Date();
let tickLength = new Date() - lastTickTime;
lastTickTime = now;
checkPlayerState();
while (true)
{
let message = Engine.PollNetworkClient();
if (!message)
break;
handleNetMessage(message);
}
updateCursorAndTooltip();
// If the selection changed, we need to regenerate the sim display (the display depends on both the
// simulation state and the current selection).
if (g_Selection.dirty)
{
g_Selection.dirty = false;
onSimulationUpdate();
// Display rally points for selected buildings
if (Engine.GetPlayerID() != -1)
Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() });
}
updateTimers();
updateMenuPosition(tickLength);
// When training is blocked, flash population (alternates color every 500msec)
Engine.GetGUIObjectByName("resourcePop").textcolor = g_IsTrainingBlocked && Date.now() % 1000 < 500 ? g_PopulationAlertColor : g_DefaultPopulationColor;
Engine.GuiInterfaceCall("ClearRenamedEntities");
}
function checkPlayerState()
{
if (g_GameEnded || Engine.GetPlayerID() < 1)
return;
// Send a game report for each player in this game.
let m_simState = GetSimState();
let playerState = m_simState.players[Engine.GetPlayerID()];
let tempStates = "";
for (let player of m_simState.players)
tempStates += player.state + ",";
if (g_CachedLastStates != tempStates)
{
g_CachedLastStates = tempStates;
reportGame();
}
if (playerState.state == "active")
return;
// Disable the resign- and pausebutton
updateTopPanel();
// Make sure nothing is open to avoid stacking.
closeOpenDialogs();
// Make sure this doesn't run again.
g_GameEnded = true;
let btCaptions;
let btCode;
let message;
let title;
if (Engine.IsAtlasRunning())
{
// If we're in Atlas, we can't leave the game
btCaptions = [translate("OK")];
btCode = [null];
message = translate("Press OK to continue");
}
else
{
btCaptions = [translate("No"), translate("Yes")];
btCode = [null, leaveGame];
message = translate("Do you want to quit?");
}
if (playerState.state == "defeated")
{
title = translate("DEFEATED!");
global.music.setState(global.music.states.DEFEAT);
setObserverMode(true);
}
else if (playerState.state == "won")
{
title = translate("VICTORIOUS!");
global.music.setState(global.music.states.VICTORY);
// TODO: Reveal map directly instead of this silly proxy.
if (!Engine.GetGUIObjectByName("devCommandsRevealMap").checked)
Engine.GetGUIObjectByName("devCommandsRevealMap").checked = true;
}
messageBox(400, 200, message, title, 0, btCaptions, btCode);
}
function changeGameSpeed(speed)
{
if (!g_IsNetworked)
Engine.SetSimRate(speed);
}
function hasIdleWorker()
{
for (let workerType of g_WorkerTypes)
{
let idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", {
"viewedPlayer": g_ViewedPlayer,
"idleClass": workerType,
"prevUnit": undefined,
"limit": 1,
"excludeUnits": []
});
if (idleUnits.length > 0)
return true;
}
return false;
}
function updateIdleWorkerButton()
{
g_HasIdleWorker = hasIdleWorker();
let idleWorkerButton = Engine.GetGUIObjectByName("idleOverlay");
let prefix = "stretched:session/";
if (!g_HasIdleWorker)
idleWorkerButton.sprite = prefix + "minimap-idle-disabled.png";
else if (idleWorkerButton.sprite != prefix + "minimap-idle-highlight.png")
idleWorkerButton.sprite = prefix + "minimap-idle.png";
}
/**
* Recomputes GUI state that depends on simulation state or selection state. Called directly every simulation
* update (see session.xml), or from onTick when the selection has changed.
*/
function onSimulationUpdate()
{
g_EntityStates = {};
g_TemplateData = {};
g_TechnologyData = {};
g_SimState = Engine.GuiInterfaceCall("GetSimulationState");
// If we're called during init when the game is first loading, there will be no simulation yet, so do nothing
if (!g_SimState)
return;
handleNotifications();
g_Selection.update();
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay();
if (g_ShowGuarding || g_ShowGuarded)
updateAdditionalHighlight();
updateHero();
updateGroups();
updateDebug();
updatePlayerDisplay();
updateResearchDisplay();
updateSelectionDetails();
updateBuildingPlacementPreview();
updateTimeNotifications();
updateIdleWorkerButton();
if (g_ViewedPlayer > 0)
{
let playerState = GetSimState().players[g_ViewedPlayer];
g_DevSettings.controlAll = playerState && playerState.controlsAll;
Engine.GetGUIObjectByName("devControlAll").checked = g_DevSettings.controlAll;
}
if (g_ViewedPlayer != -1 && !g_GameEnded)
{
// Update music state on basis of battle state.
let battleState = Engine.GuiInterfaceCall("GetBattleState", g_ViewedPlayer);
if (battleState)
global.music.setState(global.music.states[battleState]);
}
}
function onReplayFinished()
{
closeOpenDialogs();
pauseGame();
messageBox(400, 200,
translateWithContext("replayFinished", "The replay has finished. Do you want to quit?"),
translateWithContext("replayFinished", "Confirmation"),
0,
[translateWithContext("replayFinished", "No"), translateWithContext("replayFinished", "Yes")],
[resumeGame, leaveGame]);
}
/**
* updates a status bar on the GUI
* nameOfBar: name of the bar
* points: points to show
* maxPoints: max points
* direction: gets less from (right to left) 0; (top to bottom) 1; (left to right) 2; (bottom to top) 3;
*/
function updateGUIStatusBar(nameOfBar, points, maxPoints, direction)
{
// check, if optional direction parameter is valid.
if (!direction || !(direction >= 0 && direction < 4))
direction = 0;
// get the bar and update it
let statusBar = Engine.GetGUIObjectByName(nameOfBar);
if (!statusBar)
return;
let healthSize = statusBar.size;
let value = 100*Math.max(0, Math.min(1, points / maxPoints));
// inverse bar
if (direction == 2 || direction == 3)
value = 100 - value;
if (direction == 0)
healthSize.rright = value;
else if (direction == 1)
healthSize.rbottom = value;
else if (direction == 2)
healthSize.rleft = value;
else if (direction == 3)
healthSize.rtop = value;
statusBar.size = healthSize;
}
function updateHero()
{
let unitHeroPanel = Engine.GetGUIObjectByName("unitHeroPanel");
let heroButton = Engine.GetGUIObjectByName("unitHeroButton");
let playerState = GetSimState().players[g_ViewedPlayer];
if (!playerState || playerState.heroes.length <= 0)
{
g_PreviousHeroHitPoints = undefined;
unitHeroPanel.hidden = true;
return;
}
let heroImage = Engine.GetGUIObjectByName("unitHeroImage");
let heroState = GetExtendedEntityState(playerState.heroes[0]);
let template = GetTemplateData(heroState.template);
heroImage.sprite = "stretched:session/portraits/" + template.icon;
let hero = playerState.heroes[0];
heroButton.onpress = function()
{
if (!Engine.HotkeyIsPressed("selection.add"))
g_Selection.reset();
g_Selection.addList([hero]);
};
heroButton.ondoublepress = function() { selectAndMoveTo(getEntityOrHolder(hero)); };
unitHeroPanel.hidden = false;
// Setup tooltip
let tooltip = "[font=\"sans-bold-16\"]" + template.name.specific + "[/font]";
let healthLabel = "[font=\"sans-bold-13\"]" + translate("Health:") + "[/font]";
tooltip += "\n" + sprintf(translate("%(label)s %(current)s / %(max)s"), { label: healthLabel, current: heroState.hitpoints, max: heroState.maxHitpoints });
if (heroState.attack)
tooltip += "\n" + getAttackTooltip(heroState);
tooltip += "\n" + getArmorTooltip(heroState.armour);
if (template.tooltip)
tooltip += "\n" + template.tooltip;
heroButton.tooltip = tooltip;
// update heros health bar
updateGUIStatusBar("heroHealthBar", heroState.hitpoints, heroState.maxHitpoints);
let heroHP = {
"hitpoints": heroState.hitpoints,
"player": g_ViewedPlayer
};
if (!g_PreviousHeroHitPoints)
g_PreviousHeroHitPoints = heroHP;
// if the health of the hero changed since the last update, trigger the animation
if (g_PreviousHeroHitPoints.player == heroHP.player && g_PreviousHeroHitPoints.hitpoints > heroHP.hitpoints)
startColorFade("heroHitOverlay", 100, 0, colorFade_attackUnit, true, smoothColorFadeRestart_attackUnit);
g_PreviousHeroHitPoints = heroHP;
}
function updateGroups()
{
let guiName = "Group";
g_Groups.update();
for (let i = 0; i < 10; ++i)
{
let button = Engine.GetGUIObjectByName("unit"+guiName+"Button["+i+"]");
let label = Engine.GetGUIObjectByName("unit"+guiName+"Label["+i+"]").caption = i;
button.hidden = g_Groups.groups[i].getTotalCount() == 0;
button.onpress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); }; })(i);
button.ondoublepress = (function(i) { return function() { performGroup("snap", i); }; })(i);
button.onpressright = (function(i) { return function() { performGroup("breakUp", i); }; })(i);
setPanelObjectPosition(button, i, 1);
}
}
function updateDebug()
{
let debug = Engine.GetGUIObjectByName("debug");
if (!Engine.GetGUIObjectByName("devDisplayState").checked)
{
debug.hidden = true;
return;
}
debug.hidden = false;
let conciseSimState = deepcopy(GetSimState());
conciseSimState.players = "<<>>";
let text = "simulation: " + uneval(conciseSimState);
let selection = g_Selection.toList();
if (selection.length)
{
let entState = GetExtendedEntityState(selection[0]);
if (entState)
{
let template = GetTemplateData(entState.template);
text += "\n\nentity: {\n";
for (let k in entState)
text += " "+k+":"+uneval(entState[k])+"\n";
text += "}\n\ntemplate: " + uneval(template);
}
}
debug.caption = text.replace(/\[/g, "\\[");
}
function updatePlayerDisplay()
{
let playerState = GetSimState().players[g_ViewedPlayer];
if (!playerState)
return;
Engine.GetGUIObjectByName("resourceFood").caption = Math.floor(playerState.resourceCounts.food);
Engine.GetGUIObjectByName("resourceWood").caption = Math.floor(playerState.resourceCounts.wood);
Engine.GetGUIObjectByName("resourceStone").caption = Math.floor(playerState.resourceCounts.stone);
Engine.GetGUIObjectByName("resourceMetal").caption = Math.floor(playerState.resourceCounts.metal);
Engine.GetGUIObjectByName("resourcePop").caption = playerState.popCount + "/" + playerState.popLimit;
Engine.GetGUIObjectByName("population").tooltip = translate("Population (current / limit)") + "\n" +
sprintf(translate("Maximum population: %(popCap)s"), { "popCap": playerState.popMax });
g_IsTrainingBlocked = playerState.trainingBlocked;
}
function selectAndMoveTo(ent)
{
let entState = GetEntityState(ent);
if (!entState || !entState.position)
return;
g_Selection.reset();
g_Selection.addList([ent]);
let position = entState.position;
Engine.CameraMoveTo(position.x, position.z);
}
function updateResearchDisplay()
{
let researchStarted = Engine.GuiInterfaceCall("GetStartedResearch", g_ViewedPlayer);
// Set up initial positioning.
let buttonSideLength = Engine.GetGUIObjectByName("researchStartedButton[0]").size.right;
for (let i = 0; i < 10; ++i)
{
let button = Engine.GetGUIObjectByName("researchStartedButton[" + i + "]");
let size = button.size;
size.top = g_ResearchListTop + (4 + buttonSideLength) * i;
size.bottom = size.top + buttonSideLength;
button.size = size;
}
let numButtons = 0;
for (let tech in researchStarted)
{
// Show at most 10 in-progress techs.
if (numButtons >= 10)
break;
let template = GetTechnologyData(tech);
let button = Engine.GetGUIObjectByName("researchStartedButton[" + numButtons + "]");
button.hidden = false;
button.tooltip = getEntityNames(template);
button.onpress = (function(e) { return function() { selectAndMoveTo(e); }; })(researchStarted[tech].researcher);
let icon = "stretched:session/portraits/" + template.icon;
Engine.GetGUIObjectByName("researchStartedIcon[" + numButtons + "]").sprite = icon;
// Scale the progress indicator.
let size = Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size;
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
size.top = size.left + Math.round(researchStarted[tech].progress * (size.right - size.left));
Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size = size;
++numButtons;
}
// Hide unused buttons.
for (let i = numButtons; i < 10; ++i)
Engine.GetGUIObjectByName("researchStartedButton[" + i + "]").hidden = true;
}
// Toggles the display of status bars for all of the player's entities.
function recalculateStatusBarDisplay()
{
let entities;
if (g_ShowAllStatusBars)
entities = Engine.GetPlayerID() == -1 ? Engine.PickNonGaiaEntitiesOnScreen() : Engine.PickPlayerEntitiesOnScreen(Engine.GetPlayerID());
else
{
let selected = g_Selection.toList();
for (let ent in g_Selection.highlighted)
selected.push(g_Selection.highlighted[ent]);
// Remove selected entities from the 'all entities' array, to avoid disabling their status bars.
entities = Engine.GuiInterfaceCall(Engine.GetPlayerID() == -1 ? "GetNonGaiaEntities" : "GetPlayerEntities").filter(idx => selected.indexOf(idx) == -1);
}
Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars });
}
// Update the additional list of entities to be highlighted.
function updateAdditionalHighlight()
{
let entsAdd = []; // list of entities units to be highlighted
let entsRemove = [];
let highlighted = g_Selection.toList();
for (let ent in g_Selection.highlighted)
highlighted.push(g_Selection.highlighted[ent]);
if (g_ShowGuarding)
{
// flag the guarding entities to add in this additional highlight
for (let sel in g_Selection.selected)
{
let state = GetEntityState(g_Selection.selected[sel]);
if (!state.guard || !state.guard.entities.length)
continue;
for (let ent of state.guard.entities)
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
entsAdd.push(ent);
}
}
if (g_ShowGuarded)
{
// flag the guarded entities to add in this additional highlight
for (let sel in g_Selection.selected)
{
let state = GetEntityState(g_Selection.selected[sel]);
if (!state.unitAI || !state.unitAI.isGuarding)
continue;
let ent = state.unitAI.isGuarding;
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
entsAdd.push(ent);
}
}
// flag the entities to remove (from the previously added) from this additional highlight
for (let ent of g_AdditionalHighlight)
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1)
entsRemove.push(ent);
_setHighlight(entsAdd, g_HighlightedAlpha, true);
_setHighlight(entsRemove, 0, false);
g_AdditionalHighlight = entsAdd;
}
function playAmbient()
{
Engine.PlayAmbientSound(g_Ambient[Math.floor(Math.random() * g_Ambient.length)], true);
}
function getBuildString()
{
return sprintf(translate("Build: %(buildDate)s (%(revision)s)"), { "buildDate": Engine.GetBuildTimestamp(0), revision: Engine.GetBuildTimestamp(2) });
}
function showTimeWarpMessageBox()
{
messageBox(500, 250,
translate("Note: time warp mode is a developer option, and not intended for use over long periods of time. Using it incorrectly may cause the game to run out of memory or crash."),
translate("Time warp mode"), 2);
}
/**
* Send a report on the gamestatus to the lobby.
*/
function reportGame()
{
if (!Engine.HasXmppClient() || !Engine.IsRankedGame())
return;
let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
let unitsClasses = [
"total",
"Infantry",
"Worker",
"Female",
"Cavalry",
"Champion",
"Hero",
"Ship",
"Trader"
];
let unitsCountersTypes = [
"unitsTrained",
"unitsLost",
"enemyUnitsKilled"
];
let buildingsClasses = [
"total",
"CivCentre",
"House",
"Economic",
"Outpost",
"Military",
"Fortress",
"Wonder"
];
let buildingsCountersTypes = [
"buildingsConstructed",
"buildingsLost",
"enemyBuildingsDestroyed"
];
let resourcesTypes = [
"wood",
"food",
"stone",
"metal"
];
let resourcesCounterTypes = [
"resourcesGathered",
"resourcesUsed",
"resourcesSold",
"resourcesBought"
];
let playerStatistics = {};
// Unit Stats
for (let unitCounterType of unitsCountersTypes)
{
if (!playerStatistics[unitCounterType])
playerStatistics[unitCounterType] = { };
for (let unitsClass of unitsClasses)
playerStatistics[unitCounterType][unitsClass] = "";
}
playerStatistics.unitsLostValue = "";
playerStatistics.unitsKilledValue = "";
// Building stats
for (let buildingCounterType of buildingsCountersTypes)
{
if (!playerStatistics[buildingCounterType])
playerStatistics[buildingCounterType] = { };
for (let buildingsClass of buildingsClasses)
playerStatistics[buildingCounterType][buildingsClass] = "";
}
playerStatistics.buildingsLostValue = "";
playerStatistics.enemyBuildingsDestroyedValue = "";
// Resources
for (let resourcesCounterType of resourcesCounterTypes)
{
if (!playerStatistics[resourcesCounterType])
playerStatistics[resourcesCounterType] = { };
for (let resourcesType of resourcesTypes)
playerStatistics[resourcesCounterType][resourcesType] = "";
}
playerStatistics.resourcesGathered.vegetarianFood = "";
playerStatistics.tradeIncome = "";
// Tribute
playerStatistics.tributesSent = "";
playerStatistics.tributesReceived = "";
// Total
playerStatistics.economyScore = "";
playerStatistics.militaryScore = "";
playerStatistics.totalScore = "";
// Various
playerStatistics.treasuresCollected = "";
playerStatistics.lootCollected = "";
playerStatistics.feminisation = "";
playerStatistics.percentMapExplored = "";
let mapName = Engine.GetMapSettings().Name;
let playerStates = "";
let playerCivs = "";
let teams = "";
let teamsLocked = true;
// Serialize the statistics for each player into a comma-separated list.
// Ignore gaia
for (let i = 1; i < extendedSimState.players.length; ++i)
{
let player = extendedSimState.players[i];
playerStates += player.state + ",";
playerCivs += player.civ + ",";
teams += player.team + ",";
teamsLocked = teamsLocked && player.teamsLocked;
for (let resourcesCounterType of resourcesCounterTypes)
for (let resourcesType of resourcesTypes)
playerStatistics[resourcesCounterType][resourcesType] += player.statistics[resourcesCounterType][resourcesType] + ",";
playerStatistics.resourcesGathered.vegetarianFood += player.statistics.resourcesGathered.vegetarianFood + ",";
for (let unitCounterType of unitsCountersTypes)
for (let unitsClass of unitsClasses)
playerStatistics[unitCounterType][unitsClass] += player.statistics[unitCounterType][unitsClass] + ",";
for (let buildingCounterType of buildingsCountersTypes)
for (let buildingsClass of buildingsClasses)
playerStatistics[buildingCounterType][buildingsClass] += player.statistics[buildingCounterType][buildingsClass] + ",";
let total = 0;
for (let type in player.statistics.resourcesGathered)
total += player.statistics.resourcesGathered[type];
playerStatistics.economyScore += total + ",";
playerStatistics.militaryScore += Math.round((player.statistics.enemyUnitsKilledValue +
player.statistics.enemyBuildingsDestroyedValue) / 10) + ",";
playerStatistics.totalScore += (total + Math.round((player.statistics.enemyUnitsKilledValue +
player.statistics.enemyBuildingsDestroyedValue) / 10)) + ",";
playerStatistics.tradeIncome += player.statistics.tradeIncome + ",";
playerStatistics.tributesSent += player.statistics.tributesSent + ",";
playerStatistics.tributesReceived += player.statistics.tributesReceived + ",";
playerStatistics.percentMapExplored += player.statistics.percentMapExplored + ",";
playerStatistics.treasuresCollected += player.statistics.treasuresCollected + ",";
playerStatistics.lootCollected += player.statistics.lootCollected + ",";
}
// Send the report with serialized data
let reportObject = {};
reportObject.timeElapsed = extendedSimState.timeElapsed;
reportObject.playerStates = playerStates;
reportObject.playerID = Engine.GetPlayerID();
reportObject.matchID = g_MatchID;
reportObject.civs = playerCivs;
reportObject.teams = teams;
reportObject.teamsLocked = String(teamsLocked);
reportObject.ceasefireActive = String(extendedSimState.ceasefireActive);
reportObject.ceasefireTimeRemaining = String(extendedSimState.ceasefireTimeRemaining);
reportObject.mapName = mapName;
reportObject.economyScore = playerStatistics.economyScore;
reportObject.militaryScore = playerStatistics.militaryScore;
reportObject.totalScore = playerStatistics.totalScore;
for (let rct of resourcesCounterTypes)
{
for (let rt of resourcesTypes)
reportObject[rt+rct.substr(9)] = playerStatistics[rct][rt];
// eg. rt = food rct.substr = Gathered rct = resourcesGathered
}
reportObject.vegetarianFoodGathered = playerStatistics.resourcesGathered.vegetarianFood;
for (let type of unitsClasses)
{
// eg. type = Infantry (type.substr(0,1)).toLowerCase()+type.substr(1) = infantry
reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"UnitsTrained"] = playerStatistics.unitsTrained[type];
reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"UnitsLost"] = playerStatistics.unitsLost[type];
reportObject["enemy"+type+"UnitsKilled"] = playerStatistics.enemyUnitsKilled[type];
}
for (let type of buildingsClasses)
{
reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"BuildingsConstructed"] = playerStatistics.buildingsConstructed[type];
reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"BuildingsLost"] = playerStatistics.buildingsLost[type];
reportObject["enemy"+type+"BuildingsDestroyed"] = playerStatistics.enemyBuildingsDestroyed[type];
}
reportObject.tributesSent = playerStatistics.tributesSent;
reportObject.tributesReceived = playerStatistics.tributesReceived;
reportObject.percentMapExplored = playerStatistics.percentMapExplored;
reportObject.treasuresCollected = playerStatistics.treasuresCollected;
reportObject.lootCollected = playerStatistics.lootCollected;
reportObject.tradeIncome = playerStatistics.tradeIncome;
Engine.SendGameReport(reportObject);
}
Index: ps/trunk/source/gui/scripting/ScriptFunctions.cpp
===================================================================
--- ps/trunk/source/gui/scripting/ScriptFunctions.cpp (revision 17729)
+++ ps/trunk/source/gui/scripting/ScriptFunctions.cpp (revision 17730)
@@ -1,1074 +1,1086 @@
/* 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 "scriptinterface/ScriptInterface.h"
#include "graphics/Camera.h"
+#include "graphics/FontMetrics.h"
#include "graphics/GameView.h"
#include "graphics/MapReader.h"
#include "graphics/scripting/JSInterface_GameView.h"
#include "gui/GUI.h"
#include "gui/GUIManager.h"
#include "gui/IGUIObject.h"
#include "gui/scripting/JSInterface_GUITypes.h"
#include "i18n/L10n.h"
#include "i18n/scripting/JSInterface_L10n.h"
#include "lib/svn_revision.h"
#include "lib/sysdep/sysdep.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include "lobby/scripting/JSInterface_Lobby.h"
#include "maths/FixedVector3D.h"
#include "network/NetClient.h"
#include "network/NetServer.h"
#include "network/NetTurnManager.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/Errors.h"
#include "ps/GUID.h"
#include "ps/Game.h"
#include "ps/GameSetup/Atlas.h"
#include "ps/GameSetup/Config.h"
#include "ps/Globals.h" // g_frequencyFilter
#include "ps/Hotkey.h"
#include "ps/ProfileViewer.h"
#include "ps/Pyrogenesis.h"
#include "ps/SavedGame.h"
#include "ps/UserReport.h"
#include "ps/World.h"
#include "ps/scripting/JSInterface_ConfigDB.h"
#include "ps/scripting/JSInterface_Console.h"
#include "ps/scripting/JSInterface_Mod.h"
#include "ps/scripting/JSInterface_VFS.h"
#include "ps/scripting/JSInterface_VisualReplay.h"
#include "renderer/scripting/JSInterface_Renderer.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpAIManager.h"
#include "simulation2/components/ICmpCommandQueue.h"
#include "simulation2/components/ICmpGuiInterface.h"
#include "simulation2/components/ICmpPlayerManager.h"
#include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/components/ICmpSelectable.h"
#include "simulation2/components/ICmpTemplateManager.h"
#include "simulation2/helpers/Selection.h"
#include "soundmanager/SoundManager.h"
#include "soundmanager/scripting/JSInterface_Sound.h"
#include "tools/atlas/GameInterface/GameLoop.h"
/*
* This file defines a set of functions that are available to GUI scripts, to allow
* interaction with the rest of the engine.
* Functions are exposed to scripts within the global object 'Engine', so
* scripts should call "Engine.FunctionName(...)" etc.
*/
extern void restart_mainloop_in_atlas(); // from main.cpp
extern void EndGame();
extern void kill_mainloop();
namespace {
// Note that the initData argument may only contain clonable data.
// Functions aren't supported for example!
// TODO: Use LOGERROR to print a friendly error message when the requirements aren't met instead of failing with debug_warn when cloning.
void PushGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData)
{
g_GUI->PushPage(name, pCxPrivate->pScriptInterface->WriteStructuredClone(initData));
}
void SwitchGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData)
{
g_GUI->SwitchPage(name, pCxPrivate->pScriptInterface, initData);
}
void PopGuiPage(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
g_GUI->PopPage();
}
// Note that the args argument may only contain clonable data.
// Functions aren't supported for example!
// TODO: Use LOGERROR to print a friendly error message when the requirements aren't met instead of failing with debug_warn when cloning.
void PopGuiPageCB(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue args)
{
g_GUI->PopPageCB(pCxPrivate->pScriptInterface->WriteStructuredClone(args));
}
JS::Value GuiInterfaceCall(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue data)
{
if (!g_Game)
return JS::UndefinedValue();
CSimulation2* sim = g_Game->GetSimulation2();
ENSURE(sim);
CmpPtr cmpGuiInterface(*sim, SYSTEM_ENTITY);
if (!cmpGuiInterface)
return JS::UndefinedValue();
int player = g_Game->GetPlayerID();
JSContext* cxSim = sim->GetScriptInterface().GetContext();
JSAutoRequest rqSim(cxSim);
JS::RootedValue arg(cxSim, sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), data));
JS::RootedValue ret(cxSim);
cmpGuiInterface->ScriptCall(player, name, arg, &ret);
return pCxPrivate->pScriptInterface->CloneValueFromOtherContext(sim->GetScriptInterface(), ret);
}
void PostNetworkCommand(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue cmd)
{
if (!g_Game)
return;
CSimulation2* sim = g_Game->GetSimulation2();
ENSURE(sim);
CmpPtr cmpCommandQueue(*sim, SYSTEM_ENTITY);
if (!cmpCommandQueue)
return;
JSContext* cxSim = sim->GetScriptInterface().GetContext();
JSAutoRequest rqSim(cxSim);
JS::RootedValue cmd2(cxSim, sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), cmd));
cmpCommandQueue->PostNetworkCommand(cmd2);
}
entity_id_t PickEntityAtPoint(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x, int y)
{
return EntitySelection::PickEntityAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, g_Game->GetPlayerID(), false);
}
std::vector PickPlayerEntitiesInRect(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x0, int y0, int x1, int y1, int player)
{
return EntitySelection::PickEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x0, y0, x1, y1, player, false);
}
std::vector PickPlayerEntitiesOnScreen(ScriptInterface::CxPrivate* pCxPrivate, int player)
{
return PickPlayerEntitiesInRect(pCxPrivate, 0, 0, g_xres, g_yres, player);
}
std::vector PickNonGaiaEntitiesOnScreen(ScriptInterface::CxPrivate* pCxPrivate)
{
std::vector entities;
CmpPtr cmpPlayerManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY);
if (!cmpPlayerManager)
return entities;
i32 numPlayers = cmpPlayerManager->GetNumPlayers();
for (i32 player = 1; player < numPlayers; ++player)
{
std::vector ents = PickPlayerEntitiesOnScreen(pCxPrivate, player);
entities.insert(entities.end(), ents.begin(), ents.end());
}
return entities;
}
std::vector PickSimilarPlayerEntities(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& templateName, bool includeOffScreen, bool matchRank, bool allowFoundations)
{
return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetPlayerID(), includeOffScreen, matchRank, false, allowFoundations);
}
CFixedVector3D GetTerrainAtScreenPoint(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x, int y)
{
CVector3D pos = g_Game->GetView()->GetCamera()->GetWorldCoordinates(x, y, true);
return CFixedVector3D(fixed::FromFloat(pos.X), fixed::FromFloat(pos.Y), fixed::FromFloat(pos.Z));
}
std::wstring SetCursor(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name)
{
std::wstring old = g_CursorName;
g_CursorName = name;
return old;
}
bool IsVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_Game ? g_Game->IsVisualReplay() : false;
}
int GetPlayerID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (g_Game)
return g_Game->GetPlayerID();
return -1;
}
void SetPlayerID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int id)
{
if (g_Game)
g_Game->SetPlayerID(id);
}
JS::Value GetEngineInfo(ScriptInterface::CxPrivate* pCxPrivate)
{
return SavedGames::GetEngineInfo(*(pCxPrivate->pScriptInterface));
}
void StartNetworkGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
ENSURE(g_NetServer);
g_NetServer->StartGame();
}
void StartGame(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs, int playerID)
{
ENSURE(!g_NetServer);
ENSURE(!g_NetClient);
ENSURE(!g_Game);
g_Game = new CGame();
// Convert from GUI script context to sim script context
CSimulation2* sim = g_Game->GetSimulation2();
JSContext* cxSim = sim->GetScriptInterface().GetContext();
JSAutoRequest rqSim(cxSim);
JS::RootedValue gameAttribs(cxSim,
sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), attribs));
g_Game->SetPlayerID(playerID);
g_Game->StartGame(&gameAttribs, "");
}
JS::Value StartSavedGame(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name)
{
// We need to be careful with different compartments and contexts.
// The GUI calls this function from the GUI context and expects the return value in the same context.
// The game we start from here creates another context and expects data in this context.
JSContext* cxGui = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cxGui);
ENSURE(!g_NetServer);
ENSURE(!g_NetClient);
ENSURE(!g_Game);
// Load the saved game data from disk
JS::RootedValue guiContextMetadata(cxGui);
std::string savedState;
Status err = SavedGames::Load(name, *(pCxPrivate->pScriptInterface), &guiContextMetadata, savedState);
if (err < 0)
return JS::UndefinedValue();
g_Game = new CGame();
{
CSimulation2* sim = g_Game->GetSimulation2();
JSContext* cxGame = sim->GetScriptInterface().GetContext();
JSAutoRequest rq(cxGame);
JS::RootedValue gameContextMetadata(cxGame,
sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), guiContextMetadata));
JS::RootedValue gameInitAttributes(cxGame);
sim->GetScriptInterface().GetProperty(gameContextMetadata, "initAttributes", &gameInitAttributes);
int playerID;
sim->GetScriptInterface().GetProperty(gameContextMetadata, "player", playerID);
// Start the game
g_Game->SetPlayerID(playerID);
g_Game->StartGame(&gameInitAttributes, savedState);
}
return guiContextMetadata;
}
void SaveGame(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filename, const std::wstring& description, JS::HandleValue GUIMetadata)
{
shared_ptr GUIMetadataClone = pCxPrivate->pScriptInterface->WriteStructuredClone(GUIMetadata);
if (SavedGames::Save(filename, description, *g_Game->GetSimulation2(), GUIMetadataClone, g_Game->GetPlayerID()) < 0)
LOGERROR("Failed to save game");
}
void SaveGamePrefix(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& prefix, const std::wstring& description, JS::HandleValue GUIMetadata)
{
shared_ptr GUIMetadataClone = pCxPrivate->pScriptInterface->WriteStructuredClone(GUIMetadata);
if (SavedGames::SavePrefix(prefix, description, *g_Game->GetSimulation2(), GUIMetadataClone, g_Game->GetPlayerID()) < 0)
LOGERROR("Failed to save game");
}
void SetNetworkGameAttributes(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs1)
{
ENSURE(g_NetServer);
//TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere
// (with no obvious reason).
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue attribs(cx, attribs1);
g_NetServer->UpdateGameAttributes(&attribs, *(pCxPrivate->pScriptInterface));
}
void StartNetworkHost(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& playerName)
{
ENSURE(!g_NetClient);
ENSURE(!g_NetServer);
ENSURE(!g_Game);
g_NetServer = new CNetServer();
if (!g_NetServer->SetupConnection())
{
pCxPrivate->pScriptInterface->ReportError("Failed to start server");
SAFE_DELETE(g_NetServer);
return;
}
g_Game = new CGame();
g_NetClient = new CNetClient(g_Game);
g_NetClient->SetUserName(playerName);
if (!g_NetClient->SetupConnection("127.0.0.1"))
{
pCxPrivate->pScriptInterface->ReportError("Failed to connect to server");
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
}
void StartNetworkJoin(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& playerName, const std::string& serverAddress)
{
ENSURE(!g_NetClient);
ENSURE(!g_NetServer);
ENSURE(!g_Game);
g_Game = new CGame();
g_NetClient = new CNetClient(g_Game);
g_NetClient->SetUserName(playerName);
if (!g_NetClient->SetupConnection(serverAddress))
{
pCxPrivate->pScriptInterface->ReportError("Failed to connect to server");
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
}
void DisconnectNetworkGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
// TODO: we ought to do async reliable disconnections
SAFE_DELETE(g_NetServer);
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
bool KickPlayer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& playerName, bool ban)
{
if (!g_NetServer)
return false;
return g_NetServer->KickPlayer(playerName, ban);
}
JS::Value PollNetworkClient(ScriptInterface::CxPrivate* pCxPrivate)
{
if (!g_NetClient)
return JS::UndefinedValue();
// Convert from net client context to GUI script context
JSContext* cxNet = g_NetClient->GetScriptInterface().GetContext();
JSAutoRequest rqNet(cxNet);
JS::RootedValue pollNet(cxNet);
g_NetClient->GuiPoll(&pollNet);
return pCxPrivate->pScriptInterface->CloneValueFromOtherContext(g_NetClient->GetScriptInterface(), pollNet);
}
void AssignNetworkPlayer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int playerID, const std::string& guid)
{
ENSURE(g_NetServer);
g_NetServer->AssignPlayer(playerID, guid);
}
void SetNetworkPlayerStatus(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& guid, int ready)
{
ENSURE(g_NetServer);
g_NetServer->SetPlayerReady(guid, ready);
}
void ClearAllPlayerReady (ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
ENSURE(g_NetServer);
g_NetServer->ClearAllPlayerReady();
}
void SendNetworkChat(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& message)
{
ENSURE(g_NetClient);
g_NetClient->SendChatMessage(message);
}
void SendNetworkReady(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int message)
{
ENSURE(g_NetClient);
g_NetClient->SendReadyMessage(message);
}
void SendNetworkRejoined(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
ENSURE(g_NetClient);
g_NetClient->SendRejoinedMessage();
}
JS::Value GetAIs(ScriptInterface::CxPrivate* pCxPrivate)
{
return ICmpAIManager::GetAIs(*(pCxPrivate->pScriptInterface));
}
JS::Value GetSavedGames(ScriptInterface::CxPrivate* pCxPrivate)
{
return SavedGames::GetSavedGames(*(pCxPrivate->pScriptInterface));
}
bool DeleteSavedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name)
{
return SavedGames::DeleteSavedGame(name);
}
void OpenURL(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& url)
{
sys_open_url(url);
}
std::wstring GetMatchID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return ps_generate_guid().FromUTF8();
}
void RestartInAtlas(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
restart_mainloop_in_atlas();
}
bool AtlasIsAvailable(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return ATLAS_IsAvailable();
}
bool IsAtlasRunning(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return (g_AtlasGameLoop && g_AtlasGameLoop->running);
}
JS::Value LoadMapSettings(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& pathname)
{
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cx);
CMapSummaryReader reader;
if (reader.LoadMap(pathname) != PSRETURN_OK)
return JS::UndefinedValue();
JS::RootedValue settings(cx);
reader.GetMapSettings(*(pCxPrivate->pScriptInterface), &settings);
return settings;
}
JS::Value GetMapSettings(ScriptInterface::CxPrivate* pCxPrivate)
{
if (!g_Game)
return JS::UndefinedValue();
JSContext* cx = g_Game->GetSimulation2()->GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
JS::RootedValue mapSettings(cx);
g_Game->GetSimulation2()->GetMapSettings(&mapSettings);
return pCxPrivate->pScriptInterface->CloneValueFromOtherContext(
g_Game->GetSimulation2()->GetScriptInterface(),
mapSettings);
}
/**
* Get the current X coordinate of the camera.
*/
float CameraGetX(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (g_Game && g_Game->GetView())
return g_Game->GetView()->GetCameraX();
return -1;
}
/**
* Get the current Z coordinate of the camera.
*/
float CameraGetZ(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (g_Game && g_Game->GetView())
return g_Game->GetView()->GetCameraZ();
return -1;
}
/**
* Start / stop camera following mode
* @param entityid unit id to follow. If zero, stop following mode
*/
void CameraFollow(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), entity_id_t entityid)
{
if (g_Game && g_Game->GetView())
g_Game->GetView()->CameraFollow(entityid, false);
}
/**
* Start / stop first-person camera following mode
* @param entityid unit id to follow. If zero, stop following mode
*/
void CameraFollowFPS(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), entity_id_t entityid)
{
if (g_Game && g_Game->GetView())
g_Game->GetView()->CameraFollow(entityid, true);
}
/**
* Set the data (position, orientation and zoom) of the camera
*/
void SetCameraData(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), entity_pos_t x, entity_pos_t y, entity_pos_t z, entity_pos_t rotx, entity_pos_t roty, entity_pos_t zoom)
{
// called from JS; must not fail
if(!(g_Game && g_Game->GetWorld() && g_Game->GetView() && g_Game->GetWorld()->GetTerrain()))
return;
CVector3D Pos = CVector3D(x.ToFloat(), y.ToFloat(), z.ToFloat());
float RotX = rotx.ToFloat();
float RotY = roty.ToFloat();
float Zoom = zoom.ToFloat();
g_Game->GetView()->SetCamera(Pos, RotX, RotY, Zoom);
}
/// Move camera to a 2D location
void CameraMoveTo(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), entity_pos_t x, entity_pos_t z)
{
// called from JS; must not fail
if(!(g_Game && g_Game->GetWorld() && g_Game->GetView() && g_Game->GetWorld()->GetTerrain()))
return;
CTerrain* terrain = g_Game->GetWorld()->GetTerrain();
CVector3D target;
target.X = x.ToFloat();
target.Z = z.ToFloat();
target.Y = terrain->GetExactGroundLevel(target.X, target.Z);
g_Game->GetView()->MoveCameraTarget(target);
}
entity_id_t GetFollowedEntity(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (g_Game && g_Game->GetView())
return g_Game->GetView()->GetFollowedEntity();
return INVALID_ENTITY;
}
bool HotkeyIsPressed_(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& hotkeyName)
{
return HotkeyIsPressed(hotkeyName);
}
void DisplayErrorDialog(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& msg)
{
debug_DisplayError(msg.c_str(), DE_NO_DEBUG_INFO, NULL, NULL, NULL, 0, NULL, NULL);
}
JS::Value GetProfilerState(ScriptInterface::CxPrivate* pCxPrivate)
{
return g_ProfileViewer.SaveToJS(*(pCxPrivate->pScriptInterface));
}
bool IsUserReportEnabled(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_UserReporter.IsReportingEnabled();
}
void SetUserReportEnabled(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), bool enabled)
{
g_UserReporter.SetReportingEnabled(enabled);
}
std::string GetUserReportStatus(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_UserReporter.GetStatus();
}
void SubmitUserReport(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& type, int version, const std::wstring& data)
{
g_UserReporter.SubmitReport(type.c_str(), version, utf8_from_wstring(data));
}
void SetSimRate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), float rate)
{
g_Game->SetSimRate(rate);
}
float GetSimRate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_Game->GetSimRate();
}
void SetTurnLength(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int length)
{
if (g_NetServer)
g_NetServer->SetTurnLength(length);
else
LOGERROR("Only network host can change turn length");
}
// Focus the game camera on a given position.
void SetCameraTarget(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), float x, float y, float z)
{
g_Game->GetView()->ResetCameraTarget(CVector3D(x, y, z));
}
// Deliberately cause the game to crash.
// Currently implemented via access violation (read of address 0).
// Useful for testing the crashlog/stack trace code.
int Crash(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
debug_printf("Crashing at user's request.\n");
return *(volatile int*)0;
}
void DebugWarn(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
debug_warn(L"Warning at user's request.");
}
// Force a JS garbage collection cycle to take place immediately.
// Writes an indication of how long this took to the console.
void ForceGC(ScriptInterface::CxPrivate* pCxPrivate)
{
double time = timer_Time();
JS_GC(pCxPrivate->pScriptInterface->GetJSRuntime());
time = timer_Time() - time;
g_Console->InsertMessage(fmt::sprintf("Garbage collection completed in: %f", time));
}
void DumpSimState(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
OsPath path = psLogDir()/"sim_dump.txt";
std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
g_Game->GetSimulation2()->DumpDebugState(file);
}
void DumpTerrainMipmap(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
VfsPath filename(L"screenshots/terrainmipmap.png");
g_Game->GetWorld()->GetTerrain()->GetHeightMipmap().DumpToDisk(filename);
OsPath realPath;
g_VFS->GetRealPath(filename, realPath);
LOGMESSAGERENDER("Terrain mipmap written to '%s'", realPath.string8());
}
void EnableTimeWarpRecording(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), unsigned int numTurns)
{
g_Game->GetTurnManager()->EnableTimeWarpRecording(numTurns);
}
void RewindTimeWarp(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
g_Game->GetTurnManager()->RewindTimeWarp();
}
void QuickSave(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
g_Game->GetTurnManager()->QuickSave();
}
void QuickLoad(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
g_Game->GetTurnManager()->QuickLoad();
}
void SetBoundingBoxDebugOverlay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), bool enabled)
{
ICmpSelectable::ms_EnableDebugOverlays = enabled;
}
void Script_EndGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
EndGame();
}
// Cause the game to exit gracefully.
// params:
// returns:
// notes:
// - Exit happens after the current main loop iteration ends
// (since this only sets a flag telling it to end)
void ExitProgram(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
kill_mainloop();
}
// Is the game paused?
bool IsPaused(ScriptInterface::CxPrivate* pCxPrivate)
{
if (!g_Game)
{
JS_ReportError(pCxPrivate->pScriptInterface->GetContext(), "Game is not started");
return false;
}
return g_Game->m_Paused;
}
// Pause/unpause the game
void SetPaused(ScriptInterface::CxPrivate* pCxPrivate, bool pause)
{
if (!g_Game)
{
JS_ReportError(pCxPrivate->pScriptInterface->GetContext(), "Game is not started");
return;
}
g_Game->m_Paused = pause;
#if CONFIG2_AUDIO
if (g_SoundManager)
g_SoundManager->Pause(pause);
#endif
}
// Return the global frames-per-second value.
// params:
// returns: FPS [int]
// notes:
// - This value is recalculated once a frame. We take special care to
// filter it, so it is both accurate and free of jitter.
int GetFps(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
int freq = 0;
if (g_frequencyFilter)
freq = g_frequencyFilter->StableFrequency();
return freq;
}
JS::Value GetGUIObjectByName(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStr& name)
{
IGUIObject* guiObj = g_GUI->FindObjectByName(name);
if (guiObj)
return JS::ObjectValue(*guiObj->GetJSObject());
else
return JS::UndefinedValue();
}
// Return the date/time at which the current executable was compiled.
// params: mode OR an integer specifying
// what to display: -1 for "date time (svn revision)", 0 for date, 1 for time, 2 for svn revision
// returns: string with the requested timestamp info
// notes:
// - Displayed on main menu screen; tells non-programmers which auto-build
// they are running. Could also be determined via .EXE file properties,
// but that's a bit more trouble.
// - To be exact, the date/time returned is when scriptglue.cpp was
// last compiled, but the auto-build does full rebuilds.
// - svn revision is generated by calling svnversion and cached in
// lib/svn_revision.cpp. it is useful to know when attempting to
// reproduce bugs (the main EXE and PDB should be temporarily reverted to
// that revision so that they match user-submitted crashdumps).
std::wstring GetBuildTimestamp(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int mode)
{
char buf[200];
if (mode == -1) // Date, time and revision.
{
UDate dateTime = g_L10n.ParseDateTime(__DATE__ " " __TIME__, "MMM d yyyy HH:mm:ss", Locale::getUS());
std::string dateTimeString = g_L10n.LocalizeDateTime(dateTime, L10n::DateTime, SimpleDateFormat::DATE_TIME);
char svnRevision[32];
sprintf_s(svnRevision, ARRAY_SIZE(svnRevision), "%ls", svn_revision);
if (strcmp(svnRevision, "custom build") == 0)
{
// Translation: First item is a date and time, item between parenthesis is the Subversion revision number of the current build.
sprintf_s(buf, ARRAY_SIZE(buf), g_L10n.Translate("%s (custom build)").c_str(), dateTimeString.c_str());
}
else
{
// Translation: First item is a date and time, item between parenthesis is the Subversion revision number of the current build.
sprintf_s(buf, ARRAY_SIZE(buf), g_L10n.Translate("%s (%ls)").c_str(), dateTimeString.c_str(), svn_revision);
}
}
else if (mode == 0) // Date.
{
UDate dateTime = g_L10n.ParseDateTime(__DATE__, "MMM d yyyy", Locale::getUS());
std::string dateTimeString = g_L10n.LocalizeDateTime(dateTime, L10n::Date, SimpleDateFormat::MEDIUM);
sprintf_s(buf, ARRAY_SIZE(buf), "%s", dateTimeString.c_str());
}
else if (mode == 1) // Time.
{
UDate dateTime = g_L10n.ParseDateTime(__TIME__, "HH:mm:ss", Locale::getUS());
std::string dateTimeString = g_L10n.LocalizeDateTime(dateTime, L10n::Time, SimpleDateFormat::MEDIUM);
sprintf_s(buf, ARRAY_SIZE(buf), "%s", dateTimeString.c_str());
}
else if (mode == 2) // Revision.
{
char svnRevision[32];
sprintf_s(svnRevision, ARRAY_SIZE(svnRevision), "%ls", svn_revision);
if (strcmp(svnRevision, "custom build") == 0)
{
sprintf_s(buf, ARRAY_SIZE(buf), "%s", g_L10n.Translate("custom build").c_str());
}
else
{
sprintf_s(buf, ARRAY_SIZE(buf), "%ls", svn_revision);
}
}
return wstring_from_utf8(buf);
}
JS::Value ReadJSONFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath)
{
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue out(cx);
pCxPrivate->pScriptInterface->ReadJSONFile(filePath, &out);
return out;
}
void WriteJSONFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath, JS::HandleValue val1)
{
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cx);
// TODO: This is a workaround because we need to pass a MutableHandle to StringifyJSON.
JS::RootedValue val(cx, val1);
std::string str(pCxPrivate->pScriptInterface->StringifyJSON(&val, false));
VfsPath path(filePath);
WriteBuffer buf;
buf.Append(str.c_str(), str.length());
g_VFS->CreateFile(path, buf.Data(), buf.Size());
}
bool TemplateExists(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& templateName)
{
return g_GUI->TemplateExists(templateName);
}
CParamNode GetTemplate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& templateName)
{
return g_GUI->GetTemplate(templateName);
}
+int GetTextWidth(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStr& fontName, const CStrW& text)
+{
+ int width = 0;
+ int height = 0;
+ CStrIntern _fontName(fontName);
+ CFontMetrics fontMetrics(_fontName);
+ fontMetrics.CalculateStringSize(text.c_str(), width, height);
+ return width;
+}
+
//-----------------------------------------------------------------------------
// Timer
//-----------------------------------------------------------------------------
// Script profiling functions: Begin timing a piece of code with StartJsTimer(num)
// and stop timing with StopJsTimer(num). The results will be printed to stdout
// when the game exits.
static const size_t MAX_JS_TIMERS = 20;
static TimerUnit js_start_times[MAX_JS_TIMERS];
static TimerUnit js_timer_overhead;
static TimerClient js_timer_clients[MAX_JS_TIMERS];
static wchar_t js_timer_descriptions_buf[MAX_JS_TIMERS * 12]; // depends on MAX_JS_TIMERS and format string below
static void InitJsTimers(ScriptInterface& scriptInterface)
{
wchar_t* pos = js_timer_descriptions_buf;
for(size_t i = 0; i < MAX_JS_TIMERS; i++)
{
const wchar_t* description = pos;
pos += swprintf_s(pos, 12, L"js_timer %d", (int)i)+1;
timer_AddClient(&js_timer_clients[i], description);
}
// call several times to get a good approximation of 'hot' performance.
// note: don't use a separate timer slot to warm up and then judge
// overhead from another: that causes worse results (probably some
// caching effects inside JS, but I don't entirely understand why).
std::wstring calibration_script =
L"Engine.StartXTimer(0);\n" \
L"Engine.StopXTimer (0);\n" \
L"\n";
scriptInterface.LoadGlobalScript("timer_calibration_script", calibration_script);
// slight hack: call LoadGlobalScript twice because we can't average several
// TimerUnit values because there's no operator/. this way is better anyway
// because it hopefully avoids the one-time JS init overhead.
js_timer_clients[0].sum.SetToZero();
scriptInterface.LoadGlobalScript("timer_calibration_script", calibration_script);
js_timer_clients[0].sum.SetToZero();
js_timer_clients[0].num_calls = 0;
}
void StartJsTimer(ScriptInterface::CxPrivate* pCxPrivate, unsigned int slot)
{
ONCE(InitJsTimers(*(pCxPrivate->pScriptInterface)));
if (slot >= MAX_JS_TIMERS)
{
LOGERROR("Exceeded the maximum number of timer slots for scripts!");
return;
}
js_start_times[slot].SetFromTimer();
}
void StopJsTimer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), unsigned int slot)
{
if (slot >= MAX_JS_TIMERS)
{
LOGERROR("Exceeded the maximum number of timer slots for scripts!");
return;
}
TimerUnit now;
now.SetFromTimer();
now.Subtract(js_timer_overhead);
BillingPolicy_Default()(&js_timer_clients[slot], js_start_times[slot], now);
js_start_times[slot].SetToZero();
}
} // namespace
void GuiScriptingInit(ScriptInterface& scriptInterface)
{
JSI_IGUIObject::init(scriptInterface);
JSI_GUITypes::init(scriptInterface);
JSI_GameView::RegisterScriptFunctions(scriptInterface);
JSI_Renderer::RegisterScriptFunctions(scriptInterface);
JSI_Console::RegisterScriptFunctions(scriptInterface);
JSI_ConfigDB::RegisterScriptFunctions(scriptInterface);
JSI_Mod::RegisterScriptFunctions(scriptInterface);
JSI_Sound::RegisterScriptFunctions(scriptInterface);
JSI_L10n::RegisterScriptFunctions(scriptInterface);
JSI_Lobby::RegisterScriptFunctions(scriptInterface);
JSI_VisualReplay::RegisterScriptFunctions(scriptInterface);
// VFS (external)
scriptInterface.RegisterFunction("BuildDirEntList");
scriptInterface.RegisterFunction("FileExists");
scriptInterface.RegisterFunction("GetFileMTime");
scriptInterface.RegisterFunction("GetFileSize");
scriptInterface.RegisterFunction("ReadFile");
scriptInterface.RegisterFunction("ReadFileLines");
// GUI manager functions:
scriptInterface.RegisterFunction("PushGuiPage");
scriptInterface.RegisterFunction("SwitchGuiPage");
scriptInterface.RegisterFunction("PopGuiPage");
scriptInterface.RegisterFunction("PopGuiPageCB");
scriptInterface.RegisterFunction("GetGUIObjectByName");
// Simulation<->GUI interface functions:
scriptInterface.RegisterFunction("GuiInterfaceCall");
scriptInterface.RegisterFunction("PostNetworkCommand");
// Entity picking
scriptInterface.RegisterFunction("PickEntityAtPoint");
scriptInterface.RegisterFunction, int, int, int, int, int, &PickPlayerEntitiesInRect>("PickPlayerEntitiesInRect");
scriptInterface.RegisterFunction, int, &PickPlayerEntitiesOnScreen>("PickPlayerEntitiesOnScreen");
scriptInterface.RegisterFunction, &PickNonGaiaEntitiesOnScreen>("PickNonGaiaEntitiesOnScreen");
scriptInterface.RegisterFunction, std::string, bool, bool, bool, &PickSimilarPlayerEntities>("PickSimilarPlayerEntities");
scriptInterface.RegisterFunction("GetTerrainAtScreenPoint");
// Network / game setup functions
scriptInterface.RegisterFunction("StartNetworkGame");
scriptInterface.RegisterFunction("StartGame");
scriptInterface.RegisterFunction("EndGame");
scriptInterface.RegisterFunction("StartNetworkHost");
scriptInterface.RegisterFunction("StartNetworkJoin");
scriptInterface.RegisterFunction("DisconnectNetworkGame");
scriptInterface.RegisterFunction("KickPlayer");
scriptInterface.RegisterFunction("PollNetworkClient");
scriptInterface.RegisterFunction("SetNetworkGameAttributes");
scriptInterface.RegisterFunction("AssignNetworkPlayer");
scriptInterface.RegisterFunction("SetNetworkPlayerStatus");
scriptInterface.RegisterFunction("ClearAllPlayerReady");
scriptInterface.RegisterFunction("SendNetworkChat");
scriptInterface.RegisterFunction("SendNetworkReady");
scriptInterface.RegisterFunction("SendNetworkRejoined");
scriptInterface.RegisterFunction("GetAIs");
scriptInterface.RegisterFunction("GetEngineInfo");
// Saved games
scriptInterface.RegisterFunction("StartSavedGame");
scriptInterface.RegisterFunction("GetSavedGames");
scriptInterface.RegisterFunction("DeleteSavedGame");
scriptInterface.RegisterFunction("SaveGame");
scriptInterface.RegisterFunction("SaveGamePrefix");
scriptInterface.RegisterFunction("QuickSave");
scriptInterface.RegisterFunction("QuickLoad");
// Misc functions
scriptInterface.RegisterFunction("SetCursor");
scriptInterface.RegisterFunction("IsVisualReplay");
scriptInterface.RegisterFunction("GetPlayerID");
scriptInterface.RegisterFunction("SetPlayerID");
scriptInterface.RegisterFunction("OpenURL");
scriptInterface.RegisterFunction("GetMatchID");
scriptInterface.RegisterFunction("RestartInAtlas");
scriptInterface.RegisterFunction("AtlasIsAvailable");
scriptInterface.RegisterFunction("IsAtlasRunning");
scriptInterface.RegisterFunction("LoadMapSettings");
scriptInterface.RegisterFunction("GetMapSettings");
scriptInterface.RegisterFunction("CameraGetX");
scriptInterface.RegisterFunction("CameraGetZ");
scriptInterface.RegisterFunction("CameraFollow");
scriptInterface.RegisterFunction("CameraFollowFPS");
scriptInterface.RegisterFunction("SetCameraData");
scriptInterface.RegisterFunction("CameraMoveTo");
scriptInterface.RegisterFunction("GetFollowedEntity");
scriptInterface.RegisterFunction("HotkeyIsPressed");
scriptInterface.RegisterFunction("DisplayErrorDialog");
scriptInterface.RegisterFunction("GetProfilerState");
scriptInterface.RegisterFunction("Exit");
scriptInterface.RegisterFunction("IsPaused");
scriptInterface.RegisterFunction("SetPaused");
scriptInterface.RegisterFunction("GetFPS");
scriptInterface.RegisterFunction("GetBuildTimestamp");
scriptInterface.RegisterFunction("ReadJSONFile");
scriptInterface.RegisterFunction("WriteJSONFile");
scriptInterface.RegisterFunction("TemplateExists");
scriptInterface.RegisterFunction("GetTemplate");
+ scriptInterface.RegisterFunction("GetTextWidth");
// User report functions
scriptInterface.RegisterFunction("IsUserReportEnabled");
scriptInterface.RegisterFunction("SetUserReportEnabled");
scriptInterface.RegisterFunction("GetUserReportStatus");
scriptInterface.RegisterFunction("SubmitUserReport");
// Development/debugging functions
scriptInterface.RegisterFunction("StartXTimer");
scriptInterface.RegisterFunction("StopXTimer");
scriptInterface.RegisterFunction("SetSimRate");
scriptInterface.RegisterFunction("GetSimRate");
scriptInterface.RegisterFunction("SetTurnLength");
scriptInterface.RegisterFunction("SetCameraTarget");
scriptInterface.RegisterFunction("Crash");
scriptInterface.RegisterFunction("DebugWarn");
scriptInterface.RegisterFunction("ForceGC");
scriptInterface.RegisterFunction("DumpSimState");
scriptInterface.RegisterFunction("DumpTerrainMipmap");
scriptInterface.RegisterFunction("EnableTimeWarpRecording");
scriptInterface.RegisterFunction("RewindTimeWarp");
scriptInterface.RegisterFunction("SetBoundingBoxDebugOverlay");
}
Index: ps/trunk/source/network/NetClient.cpp
===================================================================
--- ps/trunk/source/network/NetClient.cpp (revision 17729)
+++ ps/trunk/source/network/NetClient.cpp (revision 17730)
@@ -1,676 +1,776 @@
-/* Copyright (C) 2015 Wildfire Games.
+/* 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 "NetClient.h"
#include "NetMessage.h"
#include "NetSession.h"
#include "NetTurnManager.h"
#include "lib/byte_order.h"
#include "lib/sysdep/sysdep.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/Compress.h"
#include "ps/CStr.h"
#include "ps/Game.h"
#include "ps/GUID.h"
#include "ps/Loader.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
CNetClient *g_NetClient = NULL;
/**
* Async task for receiving the initial game state when rejoining an
* in-progress network game.
*/
class CNetFileReceiveTask_ClientRejoin : public CNetFileReceiveTask
{
NONCOPYABLE(CNetFileReceiveTask_ClientRejoin);
public:
CNetFileReceiveTask_ClientRejoin(CNetClient& client)
: m_Client(client)
{
}
virtual void OnComplete()
{
// We've received the game state from the server
// Save it so we can use it after the map has finished loading
m_Client.m_JoinSyncBuffer = m_Buffer;
// Pretend the server told us to start the game
CGameStartMessage start;
m_Client.HandleMessage(&start);
}
private:
CNetClient& m_Client;
};
CNetClient::CNetClient(CGame* game) :
m_Session(NULL),
m_UserName(L"anonymous"),
m_GUID(ps_generate_guid()), m_HostID((u32)-1), m_ClientTurnManager(NULL), m_Game(game),
- m_GameAttributes(game->GetSimulation2()->GetScriptInterface().GetContext())
+ m_GameAttributes(game->GetSimulation2()->GetScriptInterface().GetContext()),
+ m_LastConnectionCheck(0)
{
m_Game->SetTurnManager(NULL); // delete the old local turn manager so we don't accidentally use it
void* context = this;
JS_AddExtraGCRootsTracer(GetScriptInterface().GetJSRuntime(), CNetClient::Trace, this);
// Set up transitions for session
AddTransition(NCS_UNCONNECTED, (uint)NMT_CONNECT_COMPLETE, NCS_CONNECT, (void*)&OnConnect, context);
AddTransition(NCS_CONNECT, (uint)NMT_SERVER_HANDSHAKE, NCS_HANDSHAKE, (void*)&OnHandshake, context);
AddTransition(NCS_HANDSHAKE, (uint)NMT_SERVER_HANDSHAKE_RESPONSE, NCS_AUTHENTICATE, (void*)&OnHandshakeResponse, context);
AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE_RESULT, NCS_INITIAL_GAMESETUP, (void*)&OnAuthenticate, context);
AddTransition(NCS_INITIAL_GAMESETUP, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context);
AddTransition(NCS_PREGAME, (uint)NMT_CHAT, NCS_PREGAME, (void*)&OnChat, context);
AddTransition(NCS_PREGAME, (uint)NMT_READY, NCS_PREGAME, (void*)&OnReady, context);
AddTransition(NCS_PREGAME, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context);
AddTransition(NCS_PREGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_PREGAME, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_PREGAME, (uint)NMT_KICKED, NCS_PREGAME, (void*)&OnKicked, context);
+ AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_PREGAME, (void*)&OnClientTimeout, context);
+ AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_PREGAME, (void*)&OnClientPerformance, context);
AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, (void*)&OnGameStart, context);
AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, (void*)&OnJoinSyncStart, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, (void*)&OnChat, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_SETUP, NCS_JOIN_SYNCING, (void*)&OnGameSetup, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_JOIN_SYNCING, (void*)&OnPlayerAssignment, context);
+ AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_TIMEOUT, NCS_JOIN_SYNCING, (void*)&OnClientTimeout, context);
+ AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_PERFORMANCE, NCS_JOIN_SYNCING, (void*)&OnClientPerformance, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_START, NCS_JOIN_SYNCING, (void*)&OnGameStart, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SIMULATION_COMMAND, NCS_JOIN_SYNCING, (void*)&OnInGame, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_END_COMMAND_BATCH, NCS_JOIN_SYNCING, (void*)&OnJoinSyncEndCommandBatch, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context);
AddTransition(NCS_LOADING, (uint)NMT_CHAT, NCS_LOADING, (void*)&OnChat, context);
AddTransition(NCS_LOADING, (uint)NMT_GAME_SETUP, NCS_LOADING, (void*)&OnGameSetup, context);
AddTransition(NCS_LOADING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_LOADING, (void*)&OnPlayerAssignment, context);
+ AddTransition(NCS_LOADING, (uint)NMT_CLIENT_TIMEOUT, NCS_LOADING, (void*)&OnClientTimeout, context);
+ AddTransition(NCS_LOADING, (uint)NMT_CLIENT_PERFORMANCE, NCS_LOADING, (void*)&OnClientPerformance, context);
AddTransition(NCS_LOADING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context);
AddTransition(NCS_INGAME, (uint)NMT_REJOINED, NCS_INGAME, (void*)&OnRejoined, context);
AddTransition(NCS_INGAME, (uint)NMT_KICKED, NCS_INGAME, (void*)&OnKicked, context);
+ AddTransition(NCS_INGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_INGAME, (void*)&OnClientTimeout, context);
+ AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_INGAME, (void*)&OnClientPerformance, context);
AddTransition(NCS_INGAME, (uint)NMT_CHAT, NCS_INGAME, (void*)&OnChat, context);
AddTransition(NCS_INGAME, (uint)NMT_GAME_SETUP, NCS_INGAME, (void*)&OnGameSetup, context);
AddTransition(NCS_INGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_INGAME, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_INGAME, (uint)NMT_SIMULATION_COMMAND, NCS_INGAME, (void*)&OnInGame, context);
AddTransition(NCS_INGAME, (uint)NMT_SYNC_ERROR, NCS_INGAME, (void*)&OnInGame, context);
AddTransition(NCS_INGAME, (uint)NMT_END_COMMAND_BATCH, NCS_INGAME, (void*)&OnInGame, context);
// Set first state
SetFirstState(NCS_UNCONNECTED);
}
CNetClient::~CNetClient()
{
DestroyConnection();
JS_RemoveExtraGCRootsTracer(GetScriptInterface().GetJSRuntime(), CNetClient::Trace, this);
}
void CNetClient::TraceMember(JSTracer *trc)
{
std::deque >::iterator itr;
for (itr=m_GuiMessageQueue.begin(); itr != m_GuiMessageQueue.end(); ++itr)
JS_CallHeapValueTracer(trc, &*itr, "m_GuiMessageQueue");
}
void CNetClient::SetUserName(const CStrW& username)
{
ENSURE(!m_Session); // must be called before we start the connection
m_UserName = username;
}
bool CNetClient::SetupConnection(const CStr& server)
{
CNetClientSession* session = new CNetClientSession(*this);
bool ok = session->Connect(PS_DEFAULT_PORT, server);
SetAndOwnSession(session);
return ok;
}
void CNetClient::SetAndOwnSession(CNetClientSession* session)
{
delete m_Session;
m_Session = session;
}
void CNetClient::DestroyConnection()
{
// Send network messages from the current frame before connection is destroyed.
if (m_ClientTurnManager)
{
m_ClientTurnManager->OnDestroyConnection(); // End sending of commands for scheduled turn.
Flush(); // Make sure the messages are sent.
}
SAFE_DELETE(m_Session);
}
void CNetClient::Poll()
{
- if (m_Session)
- m_Session->Poll();
+ if (!m_Session)
+ return;
+
+ CheckServerConnection();
+ m_Session->Poll();
+}
+
+void CNetClient::CheckServerConnection()
+{
+ // Trigger local warnings if the connection to the server is bad.
+ // At most once per second.
+ std::time_t now = std::time(nullptr);
+ if (now <= m_LastConnectionCheck)
+ return;
+
+ m_LastConnectionCheck = now;
+
+ JSContext* cx = GetScriptInterface().GetContext();
+
+ // Report if we are losing the connection to the server
+ u32 lastReceived = m_Session->GetLastReceivedTime();
+ if (lastReceived > NETWORK_WARNING_TIMEOUT)
+ {
+ JS::RootedValue msg(cx);
+ GetScriptInterface().Eval("({ 'type':'netwarn', 'warntype': 'server-timeout' })", &msg);
+ GetScriptInterface().SetProperty(msg, "lastReceivedTime", lastReceived);
+ PushGuiMessage(msg);
+ return;
+ }
+
+ // Report if we have a bad ping to the server
+ u32 meanRTT = m_Session->GetMeanRTT();
+ if (meanRTT > DEFAULT_TURN_LENGTH_MP)
+ {
+ JS::RootedValue msg(cx);
+ GetScriptInterface().Eval("({ 'type':'netwarn', 'warntype': 'server-latency' })", &msg);
+ GetScriptInterface().SetProperty(msg, "meanRTT", meanRTT);
+ PushGuiMessage(msg);
+ }
}
void CNetClient::Flush()
{
if (m_Session)
m_Session->Flush();
}
void CNetClient::GuiPoll(JS::MutableHandleValue ret)
{
if (m_GuiMessageQueue.empty())
{
ret.setUndefined();
return;
}
ret.set(m_GuiMessageQueue.front());
m_GuiMessageQueue.pop_front();
}
void CNetClient::PushGuiMessage(const JS::HandleValue message)
{
ENSURE(!message.isUndefined());
m_GuiMessageQueue.push_back(JS::Heap(message));
}
std::string CNetClient::TestReadGuiMessages()
{
JSContext* cx = GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
std::string r;
JS::RootedValue msg(cx);
while (true)
{
GuiPoll(&msg);
if (msg.isUndefined())
break;
r += GetScriptInterface().ToString(&msg) + "\n";
}
return r;
}
ScriptInterface& CNetClient::GetScriptInterface()
{
return m_Game->GetSimulation2()->GetScriptInterface();
}
void CNetClient::PostPlayerAssignmentsToScript()
{
JSContext* cx = GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
JS::RootedValue msg(cx);
GetScriptInterface().Eval("({'type':'players', 'hosts':{}})", &msg);
JS::RootedValue hosts(cx);
GetScriptInterface().GetProperty(msg, "hosts", &hosts);
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
JS::RootedValue host(cx);
GetScriptInterface().Eval("({})", &host);
GetScriptInterface().SetProperty(host, "name", std::wstring(it->second.m_Name), false);
GetScriptInterface().SetProperty(host, "player", it->second.m_PlayerID, false);
GetScriptInterface().SetProperty(host, "status", it->second.m_Status, false);
GetScriptInterface().SetProperty(hosts, it->first.c_str(), host, false);
}
PushGuiMessage(msg);
}
bool CNetClient::SendMessage(const CNetMessage* message)
{
if (!m_Session)
return false;
return m_Session->SendMessage(message);
}
void CNetClient::HandleConnect()
{
Update((uint)NMT_CONNECT_COMPLETE, NULL);
}
void CNetClient::HandleDisconnect(u32 reason)
{
JSContext* cx = GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
JS::RootedValue msg(cx);
GetScriptInterface().Eval("({'type':'netstatus','status':'disconnected'})", &msg);
GetScriptInterface().SetProperty(msg, "reason", (int)reason, false);
PushGuiMessage(msg);
SAFE_DELETE(m_Session);
// Update the state immediately to UNCONNECTED (don't bother with FSM transitions since
// we'd need one for every single state, and we don't need to use per-state actions)
SetCurrState(NCS_UNCONNECTED);
}
void CNetClient::SendChatMessage(const std::wstring& text)
{
CChatMessage chat;
chat.m_Message = text;
SendMessage(&chat);
}
void CNetClient::SendReadyMessage(const int status)
{
CReadyMessage readyStatus;
readyStatus.m_Status = status;
SendMessage(&readyStatus);
}
void CNetClient::SendRejoinedMessage()
{
CRejoinedMessage rejoinedMessage;
SendMessage(&rejoinedMessage);
}
bool CNetClient::HandleMessage(CNetMessage* message)
{
// Handle non-FSM messages first
Status status = m_Session->GetFileTransferer().HandleMessageReceive(message);
if (status == INFO::OK)
return true;
if (status != INFO::SKIPPED)
return false;
if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
{
CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message;
// TODO: we should support different transfer request types, instead of assuming
// it's always requesting the simulation state
std::stringstream stream;
LOGMESSAGERENDER("Serializing game at turn %u for rejoining player", m_ClientTurnManager->GetCurrentTurn());
u32 turn = to_le32(m_ClientTurnManager->GetCurrentTurn());
stream.write((char*)&turn, sizeof(turn));
bool ok = m_Game->GetSimulation2()->SerializeState(stream);
ENSURE(ok);
// Compress the content with zlib to save bandwidth
// (TODO: if this is still too large, compressing with e.g. LZMA works much better)
std::string compressed;
CompressZLib(stream.str(), compressed, true);
m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, compressed);
return true;
}
// Update FSM
bool ok = Update(message->GetType(), message);
if (!ok)
LOGERROR("Net client: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)GetCurrState());
return ok;
}
void CNetClient::LoadFinished()
{
JSContext* cx = GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
if (!m_JoinSyncBuffer.empty())
{
// We're rejoining a game, and just finished loading the initial map,
// so deserialize the saved game state now
std::string state;
DecompressZLib(m_JoinSyncBuffer, state, true);
std::stringstream stream(state);
u32 turn;
stream.read((char*)&turn, sizeof(turn));
turn = to_le32(turn);
LOGMESSAGE("Rejoining client deserializing state at turn %u\n", turn);
bool ok = m_Game->GetSimulation2()->DeserializeState(stream);
ENSURE(ok);
m_ClientTurnManager->ResetState(turn, turn);
JS::RootedValue msg(cx);
GetScriptInterface().Eval("({'type':'netstatus','status':'join_syncing'})", &msg);
PushGuiMessage(msg);
}
else
{
// Connecting at the start of a game, so we'll wait for other players to finish loading
JS::RootedValue msg(cx);
GetScriptInterface().Eval("({'type':'netstatus','status':'waiting_for_players'})", &msg);
PushGuiMessage(msg);
}
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = m_ClientTurnManager->GetCurrentTurn();
SendMessage(&loaded);
}
bool CNetClient::OnConnect(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE);
CNetClient* client = (CNetClient*)context;
JSContext* cx = client->GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
JS::RootedValue msg(cx);
client->GetScriptInterface().Eval("({'type':'netstatus','status':'connected'})", &msg);
client->PushGuiMessage(msg);
return true;
}
bool CNetClient::OnHandshake(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE);
CNetClient* client = (CNetClient*)context;
CCliHandshakeMessage handshake;
handshake.m_MagicResponse = PS_PROTOCOL_MAGIC_RESPONSE;
handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
client->SendMessage(&handshake);
return true;
}
bool CNetClient::OnHandshakeResponse(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE_RESPONSE);
CNetClient* client = (CNetClient*)context;
CAuthenticateMessage authenticate;
authenticate.m_GUID = client->m_GUID;
authenticate.m_Name = client->m_UserName;
authenticate.m_Password = L""; // TODO
client->SendMessage(&authenticate);
return true;
}
bool CNetClient::OnAuthenticate(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE_RESULT);
CNetClient* client = (CNetClient*)context;
JSContext* cx = client->GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
CAuthenticateResultMessage* message = (CAuthenticateResultMessage*)event->GetParamRef();
LOGMESSAGE("Net: Authentication result: host=%u, %s", message->m_HostID, utf8_from_wstring(message->m_Message));
bool isRejoining = (message->m_Code == ARC_OK_REJOINING);
client->m_HostID = message->m_HostID;
JS::RootedValue msg(cx);
client->GetScriptInterface().Eval("({'type':'netstatus','status':'authenticated'})", &msg);
client->GetScriptInterface().SetProperty(msg, "rejoining", isRejoining);
client->PushGuiMessage(msg);
return true;
}
bool CNetClient::OnChat(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CHAT);
CNetClient* client = (CNetClient*)context;
JSContext* cx = client->GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
CChatMessage* message = (CChatMessage*)event->GetParamRef();
JS::RootedValue msg(cx);
client->GetScriptInterface().Eval("({'type':'chat'})", &msg);
client->GetScriptInterface().SetProperty(msg, "guid", std::string(message->m_GUID), false);
client->GetScriptInterface().SetProperty(msg, "text", std::wstring(message->m_Message), false);
client->PushGuiMessage(msg);
return true;
}
bool CNetClient::OnReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_READY);
CNetClient* client = (CNetClient*)context;
JSContext* cx = client->GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
CReadyMessage* message = (CReadyMessage*)event->GetParamRef();
JS::RootedValue msg(cx);
client->GetScriptInterface().Eval("({'type':'ready'})", &msg);
client->GetScriptInterface().SetProperty(msg, "guid", std::string(message->m_GUID), false);
client->GetScriptInterface().SetProperty(msg, "status", int (message->m_Status), false);
client->PushGuiMessage(msg);
return true;
}
bool CNetClient::OnGameSetup(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
CNetClient* client = (CNetClient*)context;
JSContext* cx = client->GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef();
client->m_GameAttributes = message->m_Data;
JS::RootedValue msg(cx);
client->GetScriptInterface().Eval("({'type':'gamesetup'})", &msg);
client->GetScriptInterface().SetProperty(msg, "data", message->m_Data, false);
client->PushGuiMessage(msg);
return true;
}
bool CNetClient::OnPlayerAssignment(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_PLAYER_ASSIGNMENT);
CNetClient* client = (CNetClient*)context;
CPlayerAssignmentMessage* message = (CPlayerAssignmentMessage*)event->GetParamRef();
// Unpack the message
PlayerAssignmentMap newPlayerAssignments;
for (size_t i = 0; i < message->m_Hosts.size(); ++i)
{
PlayerAssignment assignment;
assignment.m_Enabled = true;
assignment.m_Name = message->m_Hosts[i].m_Name;
assignment.m_PlayerID = message->m_Hosts[i].m_PlayerID;
assignment.m_Status = message->m_Hosts[i].m_Status;
newPlayerAssignments[message->m_Hosts[i].m_GUID] = assignment;
}
client->m_PlayerAssignments.swap(newPlayerAssignments);
client->PostPlayerAssignmentsToScript();
return true;
}
bool CNetClient::OnGameStart(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_START);
CNetClient* client = (CNetClient*)context;
JSContext* cx = client->GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
// Find the player assigned to our GUID
int player = -1;
if (client->m_PlayerAssignments.find(client->m_GUID) != client->m_PlayerAssignments.end())
player = client->m_PlayerAssignments[client->m_GUID].m_PlayerID;
client->m_ClientTurnManager = new CNetClientTurnManager(
*client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger());
client->m_Game->SetPlayerID(player);
client->m_Game->StartGame(&client->m_GameAttributes, "");
JS::RootedValue msg(cx);
client->GetScriptInterface().Eval("({'type':'start'})", &msg);
client->PushGuiMessage(msg);
return true;
}
bool CNetClient::OnJoinSyncStart(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START);
CNetClient* client = (CNetClient*)context;
// The server wants us to start downloading the game state from it, so do so
client->m_Session->GetFileTransferer().StartTask(
shared_ptr(new CNetFileReceiveTask_ClientRejoin(*client))
);
return true;
}
bool CNetClient::OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH);
CNetClient* client = (CNetClient*)context;
CEndCommandBatchMessage* endMessage = (CEndCommandBatchMessage*)event->GetParamRef();
client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength);
// Execute all the received commands for the latest turn
client->m_ClientTurnManager->UpdateFastForward();
return true;
}
bool CNetClient::OnRejoined(void *context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_REJOINED);
CNetClient* client = (CNetClient*)context;
JSContext* cx = client->GetScriptInterface().GetContext();
CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef();
JS::RootedValue msg(cx);
client->GetScriptInterface().Eval("({'type':'rejoined'})", &msg);
client->GetScriptInterface().SetProperty(msg, "guid", std::string(message->m_GUID), false);
client->PushGuiMessage(msg);
return true;
}
bool CNetClient::OnKicked(void *context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_KICKED);
CNetClient* client = (CNetClient*)context;
JSContext* cx = client->GetScriptInterface().GetContext();
CKickedMessage* message = (CKickedMessage*)event->GetParamRef();
JS::RootedValue msg(cx);
client->GetScriptInterface().Eval("({})", &msg);
client->GetScriptInterface().SetProperty(msg, "username", message->m_Name);
client->GetScriptInterface().SetProperty(msg, "type", message->m_Ban ? std::string("banned") : std::string("kicked"));
client->PushGuiMessage(msg);
return true;
}
+bool CNetClient::OnClientTimeout(void *context, CFsmEvent* event)
+{
+ // Report the timeout of some other client
+
+ ENSURE(event->GetType() == (uint)NMT_CLIENT_TIMEOUT);
+
+ CNetClient* client = (CNetClient*)context;
+ JSContext* cx = client->GetScriptInterface().GetContext();
+
+ if (client->GetCurrState() == NCS_LOADING)
+ return true;
+
+ CClientTimeoutMessage* message = (CClientTimeoutMessage*)event->GetParamRef();
+ JS::RootedValue msg(cx);
+
+ client->GetScriptInterface().Eval("({ 'type':'netwarn', 'warntype': 'client-timeout' })", &msg);
+ client->GetScriptInterface().SetProperty(msg, "guid", std::string(message->m_GUID));
+ client->GetScriptInterface().SetProperty(msg, "lastReceivedTime", message->m_LastReceivedTime);
+ client->PushGuiMessage(msg);
+
+ return true;
+}
+
+bool CNetClient::OnClientPerformance(void *context, CFsmEvent* event)
+{
+ // Performance statistics for one or multiple clients
+
+ ENSURE(event->GetType() == (uint)NMT_CLIENT_PERFORMANCE);
+
+ CNetClient* client = (CNetClient*)context;
+ JSContext* cx = client->GetScriptInterface().GetContext();
+
+ if (client->GetCurrState() == NCS_LOADING)
+ return true;
+
+ CClientPerformanceMessage* message = (CClientPerformanceMessage*)event->GetParamRef();
+ std::vector &clients = message->m_Clients;
+
+ // Display warnings for other clients with bad ping
+ for (size_t i = 0; i < clients.size(); ++i)
+ {
+ if (clients[i].m_MeanRTT < DEFAULT_TURN_LENGTH_MP || clients[i].m_GUID == client->m_GUID)
+ continue;
+
+ JS::RootedValue msg(cx);
+ client->GetScriptInterface().Eval("({ 'type':'netwarn', 'warntype': 'client-latency' })", &msg);
+ client->GetScriptInterface().SetProperty(msg, "guid", clients[i].m_GUID);
+ client->GetScriptInterface().SetProperty(msg, "meanRTT", clients[i].m_MeanRTT);
+ client->PushGuiMessage(msg);
+ }
+
+ return true;
+}
+
bool CNetClient::OnLoadedGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetClient* client = (CNetClient*)context;
JSContext* cx = client->GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
// All players have loaded the game - start running the turn manager
// so that the game begins
client->m_Game->SetTurnManager(client->m_ClientTurnManager);
JS::RootedValue msg(cx);
client->GetScriptInterface().Eval("({'type':'netstatus','status':'active'})", &msg);
client->PushGuiMessage(msg);
return true;
}
bool CNetClient::OnInGame(void *context, CFsmEvent* event)
{
// TODO: should split each of these cases into a separate method
CNetClient* client = (CNetClient*)context;
CNetMessage* message = (CNetMessage*)event->GetParamRef();
if (message)
{
if (message->GetType() == NMT_SIMULATION_COMMAND)
{
CSimulationMessage* simMessage = static_cast (message);
client->m_ClientTurnManager->OnSimulationMessage(simMessage);
}
else if (message->GetType() == NMT_SYNC_ERROR)
{
CSyncErrorMessage* syncMessage = static_cast (message);
client->m_ClientTurnManager->OnSyncError(syncMessage->m_Turn, syncMessage->m_HashExpected, syncMessage->m_PlayerNames);
}
else if (message->GetType() == NMT_END_COMMAND_BATCH)
{
CEndCommandBatchMessage* endMessage = static_cast (message);
client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength);
}
}
return true;
}
Index: ps/trunk/source/network/NetClient.h
===================================================================
--- ps/trunk/source/network/NetClient.h (revision 17729)
+++ ps/trunk/source/network/NetClient.h (revision 17730)
@@ -1,248 +1,258 @@
-/* Copyright (C) 2015 Wildfire Games.
+/* 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 .
*/
#ifndef NETCLIENT_H
#define NETCLIENT_H
#include "network/fsm.h"
#include "network/NetFileTransfer.h"
#include "network/NetHost.h"
#include "scriptinterface/ScriptVal.h"
#include "ps/CStr.h"
#include
class CGame;
class CNetClientSession;
class CNetClientTurnManager;
class CNetServer;
class ScriptInterface;
// NetClient session FSM states
enum
{
NCS_UNCONNECTED,
NCS_CONNECT,
NCS_HANDSHAKE,
NCS_AUTHENTICATE,
NCS_INITIAL_GAMESETUP,
NCS_PREGAME,
NCS_LOADING,
NCS_JOIN_SYNCING,
NCS_INGAME
};
/**
* Network client.
* This code is run by every player (including the host, if they are not
* a dedicated server).
* It provides an interface between the GUI, the network (via CNetClientSession),
* and the game (via CGame and CNetClientTurnManager).
*/
class CNetClient : public CFsm
{
NONCOPYABLE(CNetClient);
friend class CNetFileReceiveTask_ClientRejoin;
public:
/**
* Construct a client associated with the given game object.
* The game must exist for the lifetime of this object.
*/
CNetClient(CGame* game);
virtual ~CNetClient();
/**
* We assume that adding a tracing function that's only called
* during GC is better for performance than using a
* PersistentRooted where each value needs to be added to
* the root set.
*/
static void Trace(JSTracer *trc, void *data)
{
reinterpret_cast(data)->TraceMember(trc);
}
void TraceMember(JSTracer *trc);
/**
* Set the user's name that will be displayed to all players.
* This must not be called after the connection setup.
*/
void SetUserName(const CStrW& username);
/**
* Set up a connection to the remote networked server.
* @param server IP address or host name to connect to
* @return true on success, false on connection failure
*/
bool SetupConnection(const CStr& server);
/**
* Destroy the connection to the server.
* This client probably cannot be used again.
*/
void DestroyConnection();
/**
* Poll the connection for messages from the server and process them, and send
* any queued messages.
* This must be called frequently (i.e. once per frame).
*/
void Poll();
/**
+ * Locally triggers a GUI message if the connection to the server is being lost or has bad latency.
+ */
+ void CheckServerConnection();
+
+ /**
* Flush any queued outgoing network messages.
* This should be called soon after sending a group of messages that may be batched together.
*/
void Flush();
/**
* Retrieves the next queued GUI message, and removes it from the queue.
* The returned value is in the GetScriptInterface() JS context.
*
* This is the only mechanism for the networking code to send messages to
* the GUI - it is pull-based (instead of push) so the engine code does not
* need to know anything about the code structure of the GUI scripts.
*
* The structure of the messages is { "type": "...", ... }.
* The exact types and associated data are not specified anywhere - the
* implementation and GUI scripts must make the same assumptions.
*
* @return next message, or the value 'undefined' if the queue is empty
*/
void GuiPoll(JS::MutableHandleValue);
/**
* Add a message to the queue, to be read by GuiPoll.
* The script value must be in the GetScriptInterface() JS context.
*/
void PushGuiMessage(const JS::HandleValue message);
/**
* Return a concatenation of all messages in the GUI queue,
* for test cases to easily verify the queue contents.
*/
std::string TestReadGuiMessages();
/**
* Get the script interface associated with this network client,
* which is equivalent to the one used by the CGame in the constructor.
*/
ScriptInterface& GetScriptInterface();
/**
* Send a message to the server.
* @param message message to send
* @return true on success
*/
bool SendMessage(const CNetMessage* message);
/**
* Call when the network connection has been successfully initiated.
*/
void HandleConnect();
/**
* Call when the network connection has been lost.
*/
void HandleDisconnect(u32 reason);
/**
* Call when a message has been received from the network.
*/
bool HandleMessage(CNetMessage* message);
/**
* Call when the game has started and all data files have been loaded,
* to signal to the server that we are ready to begin the game.
*/
void LoadFinished();
void SendChatMessage(const std::wstring& text);
void SendReadyMessage(const int status);
/**
* Call when the client has rejoined a running match and finished
* the loading screen.
*/
void SendRejoinedMessage();
private:
// Net message / FSM transition handlers
static bool OnConnect(void* context, CFsmEvent* event);
static bool OnHandshake(void* context, CFsmEvent* event);
static bool OnHandshakeResponse(void* context, CFsmEvent* event);
static bool OnAuthenticate(void* context, CFsmEvent* event);
static bool OnChat(void* context, CFsmEvent* event);
static bool OnReady(void* context, CFsmEvent* event);
static bool OnGameSetup(void* context, CFsmEvent* event);
static bool OnPlayerAssignment(void* context, CFsmEvent* event);
static bool OnInGame(void* context, CFsmEvent* event);
static bool OnGameStart(void* context, CFsmEvent* event);
static bool OnJoinSyncStart(void* context, CFsmEvent* event);
static bool OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event);
static bool OnRejoined(void* context, CFsmEvent* event);
static bool OnKicked(void* context, CFsmEvent* event);
+ static bool OnClientTimeout(void* context, CFsmEvent* event);
+ static bool OnClientPerformance(void* context, CFsmEvent* event);
static bool OnLoadedGame(void* context, CFsmEvent* event);
/**
* Take ownership of a session object, and use it for all network communication.
*/
void SetAndOwnSession(CNetClientSession* session);
/**
* Push a message onto the GUI queue listing the current player assignments.
*/
void PostPlayerAssignmentsToScript();
CGame *m_Game;
CStrW m_UserName;
/// Current network session (or NULL if not connected)
CNetClientSession* m_Session;
/// Turn manager associated with the current game (or NULL if we haven't started the game yet)
CNetClientTurnManager* m_ClientTurnManager;
/// Unique-per-game identifier of this client, used to identify the sender of simulation commands
u32 m_HostID;
/// Latest copy of game setup attributes heard from the server
JS::PersistentRootedValue m_GameAttributes;
/// Latest copy of player assignments heard from the server
PlayerAssignmentMap m_PlayerAssignments;
/// Globally unique identifier to distinguish users beyond the lifetime of a single network session
CStr m_GUID;
/// Queue of messages for GuiPoll
std::deque > m_GuiMessageQueue;
/// Serialized game state received when joining an in-progress game
std::string m_JoinSyncBuffer;
+
+ /// Time when the server was last checked for timeouts and bad latency
+ std::time_t m_LastConnectionCheck;
};
/// Global network client for the standard game
extern CNetClient *g_NetClient;
#endif // NETCLIENT_H
Index: ps/trunk/source/network/NetMessage.cpp
===================================================================
--- ps/trunk/source/network/NetMessage.cpp (revision 17729)
+++ ps/trunk/source/network/NetMessage.cpp (revision 17730)
@@ -1,203 +1,211 @@
-/* Copyright (C) 2015 Wildfire Games.
+/* 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 "NetMessage.h"
#include "ps/CLogger.h"
#include "ps/Game.h"
#include "simulation2/Simulation2.h"
#undef ALLNETMSGS_DONT_CREATE_NMTS
#define ALLNETMSGS_IMPLEMENT
#include "NetMessages.h"
CNetMessage::CNetMessage()
{
m_Type = NMT_INVALID;
}
CNetMessage::CNetMessage(NetMessageType type)
{
m_Type = type;
}
CNetMessage::~CNetMessage()
{
}
u8* CNetMessage::Serialize(u8* pBuffer) const
{
size_t size = GetSerializedLength();
Serialize_int_1(pBuffer, m_Type);
Serialize_int_2(pBuffer, size);
return pBuffer;
}
const u8* CNetMessage::Deserialize(const u8* pStart, const u8* pEnd)
{
if (pStart + 3 > pEnd)
{
LOGERROR("CNetMessage: Corrupt packet (smaller than header)");
return NULL;
}
const u8* pBuffer = pStart;
int type;
size_t size;
Deserialize_int_1(pBuffer, type);
Deserialize_int_2(pBuffer, size);
m_Type = (NetMessageType)type;
if (pStart + size != pEnd)
{
LOGERROR("CNetMessage: Corrupt packet (incorrect size)");
return NULL;
}
return pBuffer;
}
size_t CNetMessage::GetSerializedLength() const
{
// By default, return header size
return 3;
}
CStr CNetMessage::ToString() const
{
// This is called only when the subclass doesn't override it
if (GetType() == NMT_INVALID)
return "MESSAGE_TYPE_NONE { Undefined Message }";
else
return "Unknown Message " + CStr::FromInt(GetType());
}
CNetMessage* CNetMessageFactory::CreateMessage(const void* pData,
size_t dataSize,
ScriptInterface& scriptInterface)
{
CNetMessage* pNewMessage = NULL;
CNetMessage header;
// Figure out message type
header.Deserialize((const u8*)pData, (const u8*)pData + dataSize);
switch (header.GetType())
{
case NMT_GAME_SETUP:
pNewMessage = new CGameSetupMessage(scriptInterface);
break;
case NMT_PLAYER_ASSIGNMENT:
pNewMessage = new CPlayerAssignmentMessage;
break;
case NMT_FILE_TRANSFER_REQUEST:
pNewMessage = new CFileTransferRequestMessage;
break;
case NMT_FILE_TRANSFER_RESPONSE:
pNewMessage = new CFileTransferResponseMessage;
break;
case NMT_FILE_TRANSFER_DATA:
pNewMessage = new CFileTransferDataMessage;
break;
case NMT_FILE_TRANSFER_ACK:
pNewMessage = new CFileTransferAckMessage;
break;
case NMT_JOIN_SYNC_START:
pNewMessage = new CJoinSyncStartMessage;
break;
case NMT_REJOINED:
pNewMessage = new CRejoinedMessage;
break;
case NMT_KICKED:
pNewMessage = new CKickedMessage;
break;
+ case NMT_CLIENT_TIMEOUT:
+ pNewMessage = new CClientTimeoutMessage;
+ break;
+
+ case NMT_CLIENT_PERFORMANCE:
+ pNewMessage = new CClientPerformanceMessage;
+ break;
+
case NMT_LOADED_GAME:
pNewMessage = new CLoadedGameMessage;
break;
case NMT_SERVER_HANDSHAKE:
pNewMessage = new CSrvHandshakeMessage;
break;
case NMT_SERVER_HANDSHAKE_RESPONSE:
pNewMessage = new CSrvHandshakeResponseMessage;
break;
case NMT_CLIENT_HANDSHAKE:
pNewMessage = new CCliHandshakeMessage;
break;
case NMT_AUTHENTICATE:
pNewMessage = new CAuthenticateMessage;
break;
case NMT_AUTHENTICATE_RESULT:
pNewMessage = new CAuthenticateResultMessage;
break;
case NMT_GAME_START:
pNewMessage = new CGameStartMessage;
break;
case NMT_END_COMMAND_BATCH:
pNewMessage = new CEndCommandBatchMessage;
break;
case NMT_SYNC_CHECK:
pNewMessage = new CSyncCheckMessage;
break;
case NMT_SYNC_ERROR:
pNewMessage = new CSyncErrorMessage;
break;
case NMT_CHAT:
pNewMessage = new CChatMessage;
break;
case NMT_READY:
pNewMessage = new CReadyMessage;
break;
case NMT_SIMULATION_COMMAND:
pNewMessage = new CSimulationMessage(scriptInterface);
break;
default:
LOGERROR("CNetMessageFactory::CreateMessage(): Unknown message type '%d' received", header.GetType());
break;
}
if (pNewMessage)
pNewMessage->Deserialize((const u8*)pData, (const u8*)pData + dataSize);
return pNewMessage;
}
Index: ps/trunk/source/network/NetMessages.h
===================================================================
--- ps/trunk/source/network/NetMessages.h (revision 17729)
+++ ps/trunk/source/network/NetMessages.h (revision 17730)
@@ -1,208 +1,223 @@
-/* Copyright (C) 2015 Wildfire Games.
+/* 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 .
*/
/**
* @file
* The list of messages used by the network subsystem.
*/
#ifndef NETMESSAGES_H
#define NETMESSAGES_H
#include "ps/CStr.h"
#include "scriptinterface/ScriptVal.h"
#define PS_PROTOCOL_MAGIC 0x5073013f // 'P', 's', 0x01, '?'
#define PS_PROTOCOL_MAGIC_RESPONSE 0x50630121 // 'P', 'c', 0x01, '!'
-#define PS_PROTOCOL_VERSION 0x01010008 // Arbitrary protocol
+#define PS_PROTOCOL_VERSION 0x01010009 // Arbitrary protocol
#define PS_DEFAULT_PORT 0x5073 // 'P', 's'
// Defines the list of message types. The order of the list must not change.
// The message types having a negative value are used internally and not sent
// over the network. The message types used for network communication have
// positive values.
enum NetMessageType
{
NMT_CONNECT_COMPLETE = -256, // Connection is complete
NMT_CONNECTION_LOST,
NMT_INVALID = 0, // Invalid message
NMT_SERVER_HANDSHAKE, // Handshake stage
NMT_CLIENT_HANDSHAKE,
NMT_SERVER_HANDSHAKE_RESPONSE,
NMT_AUTHENTICATE, // Authentication stage
NMT_AUTHENTICATE_RESULT,
NMT_CHAT, // Common chat message
NMT_READY,
NMT_GAME_SETUP,
NMT_PLAYER_ASSIGNMENT,
NMT_FILE_TRANSFER_REQUEST,
NMT_FILE_TRANSFER_RESPONSE,
NMT_FILE_TRANSFER_DATA,
NMT_FILE_TRANSFER_ACK,
NMT_JOIN_SYNC_START,
NMT_REJOINED,
NMT_KICKED,
+ NMT_CLIENT_TIMEOUT,
+ NMT_CLIENT_PERFORMANCE,
+
NMT_LOADED_GAME,
NMT_GAME_START,
NMT_END_COMMAND_BATCH,
NMT_SYNC_CHECK, // OOS-detection hash checking
NMT_SYNC_ERROR, // OOS-detection error
NMT_SIMULATION_COMMAND,
NMT_LAST // Last message in the list
};
// Authentication result codes
enum AuthenticateResultCode
{
ARC_OK,
ARC_OK_REJOINING,
ARC_PASSWORD_INVALID,
};
#endif // NETMESSAGES_H
#ifdef CREATING_NMT
#define ALLNETMSGS_DONT_CREATE_NMTS
#define START_NMT_CLASS_(_nm, _message) START_NMT_CLASS(C##_nm##Message, _message)
#define DERIVE_NMT_CLASS_(_base, _nm, _message) START_NMT_CLASS_DERIVED(C ## _base ## Message, C ## _nm ## Message, _message)
START_NMTS()
START_NMT_CLASS_(SrvHandshake, NMT_SERVER_HANDSHAKE)
NMT_FIELD_INT(m_Magic, u32, 4)
NMT_FIELD_INT(m_ProtocolVersion, u32, 4)
NMT_FIELD_INT(m_SoftwareVersion, u32, 4)
END_NMT_CLASS()
START_NMT_CLASS_(CliHandshake, NMT_CLIENT_HANDSHAKE)
NMT_FIELD_INT(m_MagicResponse, u32, 4)
NMT_FIELD_INT(m_ProtocolVersion, u32, 4)
NMT_FIELD_INT(m_SoftwareVersion, u32, 4)
END_NMT_CLASS()
START_NMT_CLASS_(SrvHandshakeResponse, NMT_SERVER_HANDSHAKE_RESPONSE)
NMT_FIELD_INT(m_UseProtocolVersion, u32, 4)
NMT_FIELD_INT(m_Flags, u32, 4)
NMT_FIELD(CStrW, m_Message)
END_NMT_CLASS()
START_NMT_CLASS_(Authenticate, NMT_AUTHENTICATE)
NMT_FIELD(CStr8, m_GUID)
NMT_FIELD(CStrW, m_Name)
NMT_FIELD(CStrW, m_Password)
END_NMT_CLASS()
START_NMT_CLASS_(AuthenticateResult, NMT_AUTHENTICATE_RESULT)
NMT_FIELD_INT(m_Code, u32, 4)
NMT_FIELD_INT(m_HostID, u32, 2)
NMT_FIELD(CStrW, m_Message)
END_NMT_CLASS()
START_NMT_CLASS_(Chat, NMT_CHAT)
NMT_FIELD(CStr8, m_GUID) // ignored when client->server, valid when server->client
NMT_FIELD(CStrW, m_Message)
END_NMT_CLASS()
START_NMT_CLASS_(Ready, NMT_READY)
NMT_FIELD(CStr8, m_GUID)
NMT_FIELD_INT(m_Status, u8, 1)
END_NMT_CLASS()
START_NMT_CLASS_(PlayerAssignment, NMT_PLAYER_ASSIGNMENT)
NMT_START_ARRAY(m_Hosts)
NMT_FIELD(CStr8, m_GUID)
NMT_FIELD(CStrW, m_Name)
NMT_FIELD_INT(m_PlayerID, i8, 1)
NMT_FIELD_INT(m_Status, u8, 1)
NMT_END_ARRAY()
END_NMT_CLASS()
START_NMT_CLASS_(FileTransferRequest, NMT_FILE_TRANSFER_REQUEST)
NMT_FIELD_INT(m_RequestID, u32, 4)
END_NMT_CLASS()
START_NMT_CLASS_(FileTransferResponse, NMT_FILE_TRANSFER_RESPONSE)
NMT_FIELD_INT(m_RequestID, u32, 4)
NMT_FIELD_INT(m_Length, u32, 4)
END_NMT_CLASS()
START_NMT_CLASS_(FileTransferData, NMT_FILE_TRANSFER_DATA)
NMT_FIELD_INT(m_RequestID, u32, 4)
NMT_FIELD(CStr8, m_Data)
END_NMT_CLASS()
START_NMT_CLASS_(FileTransferAck, NMT_FILE_TRANSFER_ACK)
NMT_FIELD_INT(m_RequestID, u32, 4)
NMT_FIELD_INT(m_NumPackets, u32, 4)
END_NMT_CLASS()
START_NMT_CLASS_(JoinSyncStart, NMT_JOIN_SYNC_START)
END_NMT_CLASS()
START_NMT_CLASS_(Rejoined, NMT_REJOINED)
NMT_FIELD(CStr8, m_GUID)
END_NMT_CLASS()
START_NMT_CLASS_(Kicked, NMT_KICKED)
NMT_FIELD(CStrW, m_Name)
NMT_FIELD_INT(m_Ban, u8, 1)
END_NMT_CLASS()
+START_NMT_CLASS_(ClientTimeout, NMT_CLIENT_TIMEOUT)
+ NMT_FIELD(CStr8, m_GUID)
+ NMT_FIELD_INT(m_LastReceivedTime, u32, 4)
+END_NMT_CLASS()
+
+START_NMT_CLASS_(ClientPerformance, NMT_CLIENT_PERFORMANCE)
+ NMT_START_ARRAY(m_Clients)
+ NMT_FIELD(CStr8, m_GUID)
+ NMT_FIELD_INT(m_MeanRTT, u32, 4)
+ NMT_END_ARRAY()
+END_NMT_CLASS()
+
START_NMT_CLASS_(LoadedGame, NMT_LOADED_GAME)
NMT_FIELD_INT(m_CurrentTurn, u32, 4)
END_NMT_CLASS()
START_NMT_CLASS_(GameStart, NMT_GAME_START)
END_NMT_CLASS()
START_NMT_CLASS_(EndCommandBatch, NMT_END_COMMAND_BATCH)
NMT_FIELD_INT(m_Turn, u32, 4)
NMT_FIELD_INT(m_TurnLength, u32, 2)
END_NMT_CLASS()
START_NMT_CLASS_(SyncCheck, NMT_SYNC_CHECK)
NMT_FIELD_INT(m_Turn, u32, 4)
NMT_FIELD(CStr, m_Hash)
END_NMT_CLASS()
START_NMT_CLASS_(SyncError, NMT_SYNC_ERROR)
NMT_FIELD_INT(m_Turn, u32, 4)
NMT_FIELD(CStr, m_HashExpected)
NMT_START_ARRAY(m_PlayerNames)
NMT_FIELD(CStrW, m_Name)
NMT_END_ARRAY()
END_NMT_CLASS()
END_NMTS()
#else
#ifndef ALLNETMSGS_DONT_CREATE_NMTS
# ifdef ALLNETMSGS_IMPLEMENT
# define NMT_CREATOR_IMPLEMENT
# endif
# define NMT_CREATE_HEADER_NAME "NetMessages.h"
# include "NMTCreator.h"
#endif // #ifndef ALLNETMSGS_DONT_CREATE_NMTS
#endif // #ifdef CREATING_NMT
Index: ps/trunk/source/network/NetServer.cpp
===================================================================
--- ps/trunk/source/network/NetServer.cpp (revision 17729)
+++ ps/trunk/source/network/NetServer.cpp (revision 17730)
@@ -1,1266 +1,1318 @@
/* 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 "NetServer.h"
#include "NetClient.h"
#include "NetMessage.h"
#include "NetSession.h"
#include "NetStats.h"
#include "NetTurnManager.h"
#include "lib/external_libraries/enet.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptRuntime.h"
#include "simulation2/Simulation2.h"
#if CONFIG2_MINIUPNPC
#include
#include
#include
#include
#endif
#define DEFAULT_SERVER_NAME L"Unnamed Server"
#define DEFAULT_WELCOME_MESSAGE L"Welcome"
#define MAX_CLIENTS 10
static const int CHANNEL_COUNT = 1;
/**
* enet_host_service timeout (msecs).
* Smaller numbers may hurt performance; larger numbers will
* hurt latency responding to messages from game thread.
*/
static const int HOST_SERVICE_TIMEOUT = 50;
CNetServer* g_NetServer = NULL;
static CStr DebugName(CNetServerSession* session)
{
if (session == NULL)
return "[unknown host]";
if (session->GetGUID().empty())
return "[unauthed host]";
return "[" + session->GetGUID().substr(0, 8) + "...]";
}
/**
* Async task for receiving the initial game state to be forwarded to another
* client that is rejoining an in-progress network game.
*/
class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask
{
NONCOPYABLE(CNetFileReceiveTask_ServerRejoin);
public:
CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID)
: m_Server(server), m_RejoinerHostID(hostID)
{
}
virtual void OnComplete()
{
// We've received the game state from an existing player - now
// we need to send it onwards to the newly rejoining player
// Find the session corresponding to the rejoining host (if any)
CNetServerSession* session = NULL;
for (size_t i = 0; i < m_Server.m_Sessions.size(); ++i)
{
if (m_Server.m_Sessions[i]->GetHostID() == m_RejoinerHostID)
{
session = m_Server.m_Sessions[i];
break;
}
}
if (!session)
{
LOGMESSAGE("Net server: rejoining client disconnected before we sent to it");
return;
}
// Store the received state file, and tell the client to start downloading it from us
// TODO: this will get kind of confused if there's multiple clients downloading in parallel;
// they'll race and get whichever happens to be the latest received by the server,
// which should still work but isn't great
m_Server.m_JoinSyncFile = m_Buffer;
CJoinSyncStartMessage message;
session->SendMessage(&message);
}
private:
CNetServerWorker& m_Server;
u32 m_RejoinerHostID;
};
/*
* XXX: We use some non-threadsafe functions from the worker thread.
* See http://trac.wildfiregames.com/ticket/654
*/
CNetServerWorker::CNetServerWorker(int autostartPlayers) :
m_AutostartPlayers(autostartPlayers),
m_Shutdown(false),
m_ScriptInterface(NULL),
- m_NextHostID(1), m_Host(NULL), m_HostGUID(), m_Stats(NULL)
+ m_NextHostID(1), m_Host(NULL), m_HostGUID(), m_Stats(NULL),
+ m_LastConnectionCheck(0)
{
m_State = SERVER_STATE_UNCONNECTED;
m_ServerTurnManager = NULL;
m_ServerName = DEFAULT_SERVER_NAME;
m_WelcomeMessage = DEFAULT_WELCOME_MESSAGE;
}
CNetServerWorker::~CNetServerWorker()
{
if (m_State != SERVER_STATE_UNCONNECTED)
{
// Tell the thread to shut down
{
CScopeLock lock(m_WorkerMutex);
m_Shutdown = true;
}
// Wait for it to shut down cleanly
pthread_join(m_WorkerThread, NULL);
}
// Clean up resources
delete m_Stats;
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
m_Sessions[i]->DisconnectNow(NDR_SERVER_SHUTDOWN);
delete m_Sessions[i];
}
if (m_Host)
{
enet_host_destroy(m_Host);
}
delete m_ServerTurnManager;
}
bool CNetServerWorker::SetupConnection()
{
ENSURE(m_State == SERVER_STATE_UNCONNECTED);
ENSURE(!m_Host);
// Bind to default host
ENetAddress addr;
addr.host = ENET_HOST_ANY;
addr.port = PS_DEFAULT_PORT;
// Create ENet server
m_Host = enet_host_create(&addr, MAX_CLIENTS, CHANNEL_COUNT, 0, 0);
if (!m_Host)
{
LOGERROR("Net server: enet_host_create failed");
return false;
}
m_Stats = new CNetStatsTable();
if (CProfileViewer::IsInitialised())
g_ProfileViewer.AddRootTable(m_Stats);
m_State = SERVER_STATE_PREGAME;
// Launch the worker thread
int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this);
ENSURE(ret == 0);
#if CONFIG2_MINIUPNPC
// Launch the UPnP thread
ret = pthread_create(&m_UPnPThread, NULL, &SetupUPnP, NULL);
ENSURE(ret == 0);
#endif
return true;
}
#if CONFIG2_MINIUPNPC
void* CNetServerWorker::SetupUPnP(void*)
{
// Values we want to set.
char psPort[6];
sprintf_s(psPort, ARRAY_SIZE(psPort), "%d", PS_DEFAULT_PORT);
const char* leaseDuration = "0"; // Indefinite/permanent lease duration.
const char* description = "0AD Multiplayer";
const char* protocall = "UDP";
char internalIPAddress[64];
char externalIPAddress[40];
// Variables to hold the values that actually get set.
char intClient[40];
char intPort[6];
char duration[16];
// Intermediate variables.
struct UPNPUrls urls;
struct IGDdatas data;
struct UPNPDev* devlist = NULL;
// Cached root descriptor URL.
std::string rootDescURL;
CFG_GET_VAL("network.upnprootdescurl", rootDescURL);
if (!rootDescURL.empty())
LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL.c_str());
int ret = 0;
bool allocatedUrls = false;
// Try a cached URL first
if (!rootDescURL.empty() && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress)))
{
LOGMESSAGE("Net server: using cached IGD = %s", urls.controlURL);
ret = 1;
}
// No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds.
#if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14
else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL)
#else
else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL)
#endif
{
ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress));
allocatedUrls = ret != 0; // urls is allocated on non-zero return values
}
else
{
LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL.");
return NULL;
}
switch (ret)
{
case 0:
LOGMESSAGE("Net server: No IGD found");
break;
case 1:
LOGMESSAGE("Net server: found valid IGD = %s", urls.controlURL);
break;
case 2:
LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls.controlURL);
break;
case 3:
LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls.controlURL);
break;
default:
debug_warn(L"Unrecognized return value from UPNP_GetValidIGD");
}
// Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance.
ret = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIPAddress);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret, strupnperror(ret));
return NULL;
}
LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress);
// Try to setup port forwarding.
ret = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, psPort, psPort,
internalIPAddress, description, protocall, 0, leaseDuration);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)",
psPort, psPort, internalIPAddress, ret, strupnperror(ret));
return NULL;
}
// Check that the port was actually forwarded.
ret = UPNP_GetSpecificPortMappingEntry(urls.controlURL,
data.first.servicetype,
psPort, protocall,
#if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10
NULL/*remoteHost*/,
#endif
intClient, intPort, NULL/*desc*/,
NULL/*enabled*/, duration);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret, strupnperror(ret));
return NULL;
}
LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)",
externalIPAddress, psPort, protocall, intClient, intPort, duration);
// Cache root descriptor URL to try to avoid discovery next time.
g_ConfigDB.SetValueString(CFG_USER, "network.upnprootdescurl", urls.controlURL);
g_ConfigDB.WriteFile(CFG_USER);
LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls.controlURL);
// Make sure everything is properly freed.
if (allocatedUrls)
FreeUPNPUrls(&urls);
freeUPNPDevlist(devlist);
return NULL;
}
#endif // CONFIG2_MINIUPNPC
bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message)
{
ENSURE(m_Host);
CNetServerSession* session = static_cast(peer->data);
return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
}
bool CNetServerWorker::Broadcast(const CNetMessage* message)
{
ENSURE(m_Host);
bool ok = true;
// Send to all sessions that are active and has finished authentication
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
if (m_Sessions[i]->GetCurrState() == NSS_PREGAME || m_Sessions[i]->GetCurrState() == NSS_INGAME)
{
if (!m_Sessions[i]->SendMessage(message))
ok = false;
// TODO: this does lots of repeated message serialisation if we have lots
// of remote peers; could do it more efficiently if that's a real problem
}
}
return ok;
}
void* CNetServerWorker::RunThread(void* data)
{
debug_SetThreadName("NetServer");
static_cast(data)->Run();
return NULL;
}
void CNetServerWorker::Run()
{
// The script runtime uses the profiler and therefore the thread must be registered before the runtime is created
g_Profiler2.RegisterCurrentThread("Net server");
// To avoid the need for JS_SetContextThread, we create and use and destroy
// the script interface entirely within this network thread
m_ScriptInterface = new ScriptInterface("Engine", "Net server", ScriptInterface::CreateRuntime(g_ScriptRuntime));
m_GameAttributes.set(m_ScriptInterface->GetJSRuntime(), JS::UndefinedValue());
while (true)
{
if (!RunStep())
break;
// Implement autostart mode
if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers)
StartGame();
// Update profiler stats
m_Stats->LatchHostState(m_Host);
}
// Clear roots before deleting their context
m_GameAttributes.clear();
m_SavedCommands.clear();
SAFE_DELETE(m_ScriptInterface);
}
bool CNetServerWorker::RunStep()
{
// Check for messages from the game thread.
// (Do as little work as possible while the mutex is held open,
// to avoid performance problems and deadlocks.)
m_ScriptInterface->GetRuntime()->MaybeIncrementalGC(0.5f);
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
std::vector > newAssignPlayer;
std::vector newStartGame;
std::vector > newPlayerReady;
std::vector newPlayerResetReady;
std::vector newGameAttributes;
std::vector newTurnLength;
{
CScopeLock lock(m_WorkerMutex);
if (m_Shutdown)
return false;
newStartGame.swap(m_StartGameQueue);
newPlayerReady.swap(m_PlayerReadyQueue);
newPlayerResetReady.swap(m_PlayerResetReadyQueue);
newAssignPlayer.swap(m_AssignPlayerQueue);
newGameAttributes.swap(m_GameAttributesQueue);
newTurnLength.swap(m_TurnLengthQueue);
}
for (size_t i = 0; i < newAssignPlayer.size(); ++i)
AssignPlayer(newAssignPlayer[i].first, newAssignPlayer[i].second);
for (size_t i = 0; i < newPlayerReady.size(); ++i)
SetPlayerReady(newPlayerReady[i].first, newPlayerReady[i].second);
if (!newPlayerResetReady.empty())
ClearAllPlayerReady();
if (!newGameAttributes.empty())
{
JS::RootedValue gameAttributesVal(cx);
GetScriptInterface().ParseJSON(newGameAttributes.back(), &gameAttributesVal);
UpdateGameAttributes(&gameAttributesVal);
}
if (!newTurnLength.empty())
SetTurnLength(newTurnLength.back());
// Do StartGame last, so we have the most up-to-date game attributes when we start
if (!newStartGame.empty())
StartGame();
// Perform file transfers
for (size_t i = 0; i < m_Sessions.size(); ++i)
m_Sessions[i]->GetFileTransferer().Poll();
+ CheckClientConnections();
+
// Process network events:
ENetEvent event;
int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT);
if (status < 0)
{
LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status);
// TODO: notify game that the server has shut down
return false;
}
if (status == 0)
{
// Reached timeout with no events - try again
return true;
}
// Process the event:
switch (event.type)
{
case ENET_EVENT_TYPE_CONNECT:
{
// Report the client address
char hostname[256] = "(error)";
enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
LOGMESSAGE("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port);
// Set up a session object for this peer
CNetServerSession* session = new CNetServerSession(*this, event.peer);
m_Sessions.push_back(session);
SetupSession(session);
ENSURE(event.peer->data == NULL);
event.peer->data = session;
HandleConnect(session);
break;
}
case ENET_EVENT_TYPE_DISCONNECT:
{
// If there is an active session with this peer, then reset and delete it
CNetServerSession* session = static_cast(event.peer->data);
if (session)
{
LOGMESSAGE("Net server: Disconnected %s", DebugName(session).c_str());
// Remove the session first, so we won't send player-update messages to it
// when updating the FSM
m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end());
session->Update((uint)NMT_CONNECTION_LOST, NULL);
delete session;
event.peer->data = NULL;
}
break;
}
case ENET_EVENT_TYPE_RECEIVE:
{
// If there is an active session with this peer, then process the message
CNetServerSession* session = static_cast(event.peer->data);
if (session)
{
// Create message from raw data
CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
if (msg)
{
LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str());
HandleMessageReceive(msg, session);
delete msg;
}
}
// Done using the packet
enet_packet_destroy(event.packet);
break;
}
case ENET_EVENT_TYPE_NONE:
break;
}
return true;
}
+void CNetServerWorker::CheckClientConnections()
+{
+ if (m_State == SERVER_STATE_LOADING)
+ return;
+
+ // Send messages at most once per second
+ std::time_t now = std::time(nullptr);
+ if (now <= m_LastConnectionCheck)
+ return;
+
+ m_LastConnectionCheck = now;
+
+ for (size_t i = 0; i < m_Sessions.size(); ++i)
+ {
+ u32 lastReceived = m_Sessions[i]->GetLastReceivedTime();
+ u32 meanRTT = m_Sessions[i]->GetMeanRTT();
+
+ CNetMessage* message = nullptr;
+
+ // Report if we didn't hear from the client since few seconds
+ if (lastReceived > NETWORK_WARNING_TIMEOUT)
+ {
+ CClientTimeoutMessage* msg = new CClientTimeoutMessage();
+ msg->m_GUID = m_Sessions[i]->GetGUID();
+ msg->m_LastReceivedTime = lastReceived;
+ message = msg;
+ }
+ // Report if the client has bad ping
+ else if (meanRTT > DEFAULT_TURN_LENGTH_MP)
+ {
+ CClientPerformanceMessage* msg = new CClientPerformanceMessage();
+ CClientPerformanceMessage::S_m_Clients client;
+ client.m_GUID = m_Sessions[i]->GetGUID();
+ client.m_MeanRTT = meanRTT;
+ msg->m_Clients.push_back(client);
+ message = msg;
+ }
+
+ // Send to all clients except the affected one
+ // (since that will show the locally triggered warning instead)
+ if (message)
+ for (size_t j = 0; j < m_Sessions.size(); ++j)
+ if (i != j)
+ m_Sessions[j]->SendMessage(message);
+
+ SAFE_DELETE(message);
+ }
+}
+
void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session)
{
// Handle non-FSM messages first
Status status = session->GetFileTransferer().HandleMessageReceive(message);
if (status != INFO::SKIPPED)
return;
if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
{
CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message;
// Rejoining client got our JoinSyncStart after we received the state from
// another client, and has now requested that we forward it to them
ENSURE(!m_JoinSyncFile.empty());
session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile);
return;
}
// Update FSM
bool ok = session->Update(message->GetType(), (void*)message);
if (!ok)
LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState());
}
void CNetServerWorker::SetupSession(CNetServerSession* session)
{
void* context = session;
// Set up transitions for session
session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, (void*)&OnClientHandshake, context);
session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, (void*)&OnChat, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_READY, NSS_PREGAME, (void*)&OnReady, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnJoinSyncingLoadedGame, context);
session->AddTransition(NSS_INGAME, (uint)NMT_REJOINED, NSS_INGAME, (void*)&OnRejoined, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, (void*)&OnChat, context);
session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, (void*)&OnInGame, context);
session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnInGame, context);
session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnInGame, context);
// Set first state
session->SetFirstState(NSS_HANDSHAKE);
}
bool CNetServerWorker::HandleConnect(CNetServerSession* session)
{
if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), session->GetIPAddress()) != m_BannedIPs.end())
{
session->Disconnect(NDR_BANNED);
return false;
}
CSrvHandshakeMessage handshake;
handshake.m_Magic = PS_PROTOCOL_MAGIC;
handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
return session->SendMessage(&handshake);
}
void CNetServerWorker::OnUserJoin(CNetServerSession* session)
{
AddPlayer(session->GetGUID(), session->GetUserName());
// Host is the first to join
if (m_HostGUID.empty())
m_HostGUID = session->GetGUID();
CGameSetupMessage gameSetupMessage(GetScriptInterface());
gameSetupMessage.m_Data = m_GameAttributes.get();
session->SendMessage(&gameSetupMessage);
CPlayerAssignmentMessage assignMessage;
ConstructPlayerAssignmentMessage(assignMessage);
session->SendMessage(&assignMessage);
}
void CNetServerWorker::OnUserLeave(CNetServerSession* session)
{
RemovePlayer(session->GetGUID());
if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING)
m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers
// TODO: ought to switch the player controlled by that client
// back to AI control, or something?
}
void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name)
{
// Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1)
std::set usedIDs;
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
if (it->second.m_Enabled && it->second.m_PlayerID != -1)
usedIDs.insert(it->second.m_PlayerID);
// If the player is rejoining after disconnecting, try to give them
// back their old player ID
i32 playerID = -1;
// Try to match GUID first
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
{
playerID = it->second.m_PlayerID;
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
goto found;
}
}
// Try to match username next
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
{
playerID = it->second.m_PlayerID;
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
goto found;
}
}
// Otherwise leave the player ID as -1 (observer) and let gamesetup change it as needed.
found:
PlayerAssignment assignment;
assignment.m_Enabled = true;
assignment.m_Name = name;
assignment.m_PlayerID = playerID;
assignment.m_Status = 0;
m_PlayerAssignments[guid] = assignment;
// Send the new assignments to all currently active players
// (which does not include the one that's just joining)
SendPlayerAssignments();
}
void CNetServerWorker::RemovePlayer(const CStr& guid)
{
m_PlayerAssignments[guid].m_Enabled = false;
SendPlayerAssignments();
}
void CNetServerWorker::SetPlayerReady(const CStr& guid, const int ready)
{
m_PlayerAssignments[guid].m_Status = ready;
SendPlayerAssignments();
}
void CNetServerWorker::ClearAllPlayerReady()
{
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
it->second.m_Status = 0;
SendPlayerAssignments();
}
bool CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban)
{
// Find the user with that name
std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
[&](CNetServerSession* session) { return session->GetUserName() == playerName; });
// and return if no one or the host has that name
if (it == m_Sessions.end() || (*it)->GetGUID() == m_HostGUID)
return false;
if (ban)
{
// Remember name
if (std::find(m_BannedPlayers.begin(), m_BannedPlayers.end(), playerName) == m_BannedPlayers.end())
m_BannedPlayers.push_back(playerName);
// Remember IP address
CStr ipAddress = (*it)->GetIPAddress();
if (!ipAddress.empty() && std::find(m_BannedIPs.begin(), m_BannedIPs.end(), ipAddress) == m_BannedIPs.end())
m_BannedIPs.push_back(ipAddress);
}
// Disconnect that user
(*it)->Disconnect(ban ? NDR_BANNED : NDR_KICKED);
// Send message notifying other clients
CKickedMessage kickedMessage;
kickedMessage.m_Name = playerName;
kickedMessage.m_Ban = ban;
Broadcast(&kickedMessage);
return true;
}
void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid)
{
// Remove anyone who's already assigned to this player
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (it->second.m_PlayerID == playerID)
it->second.m_PlayerID = -1;
}
// Update this host's assignment if it exists
if (m_PlayerAssignments.find(guid) != m_PlayerAssignments.end())
m_PlayerAssignments[guid].m_PlayerID = playerID;
SendPlayerAssignments();
}
void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message)
{
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled)
continue;
CPlayerAssignmentMessage::S_m_Hosts h;
h.m_GUID = it->first;
h.m_Name = it->second.m_Name;
h.m_PlayerID = it->second.m_PlayerID;
h.m_Status = it->second.m_Status;
message.m_Hosts.push_back(h);
}
}
void CNetServerWorker::SendPlayerAssignments()
{
CPlayerAssignmentMessage message;
ConstructPlayerAssignmentMessage(message);
Broadcast(&message);
}
ScriptInterface& CNetServerWorker::GetScriptInterface()
{
return *m_ScriptInterface;
}
void CNetServerWorker::SetTurnLength(u32 msecs)
{
if (m_ServerTurnManager)
m_ServerTurnManager->SetTurnLength(msecs);
}
bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef();
if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION)
{
session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION);
return false;
}
CSrvHandshakeResponseMessage handshakeResponse;
handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION;
handshakeResponse.m_Message = server.m_WelcomeMessage;
handshakeResponse.m_Flags = 0;
session->SendMessage(&handshakeResponse);
return true;
}
bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Prohibit joins while the game is loading
if (server.m_State == SERVER_STATE_LOADING)
{
LOGMESSAGE("Refused connection while the game is loading");
session->Disconnect(NDR_SERVER_LOADING);
return true;
}
CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef();
CStrW username = server.DeduplicatePlayerName(SanitisePlayerName(message->m_Name));
// Disconnect banned usernames
if (std::find(server.m_BannedPlayers.begin(), server.m_BannedPlayers.end(), username) != server.m_BannedPlayers.end())
{
session->Disconnect(NDR_BANNED);
return true;
}
// Optionally allow observers to join after the game has started
bool observerLateJoin = false;
ScriptInterface& scriptInterface = server.GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue settings(cx);
scriptInterface.GetProperty(server.m_GameAttributes.get(), "settings", &settings);
if (scriptInterface.HasProperty(settings, "ObserverLateJoin"))
scriptInterface.GetProperty(settings, "ObserverLateJoin", observerLateJoin);
// If the game has already started, only allow rejoins
bool isRejoining = false;
if (server.m_State != SERVER_STATE_PREGAME)
{
// Search for an old disconnected player of the same name
// (TODO: if GUIDs were stable, we should use them instead)
isRejoining =
observerLateJoin ||
std::find_if(
server.m_PlayerAssignments.begin(), server.m_PlayerAssignments.end(),
[&username] (const std::pair& pair)
{ return !pair.second.m_Enabled && pair.second.m_Name == username; })
!= server.m_PlayerAssignments.end();
// Players who weren't already in the game are not allowed to join now that it's started
if (!isRejoining)
{
LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username));
session->Disconnect(NDR_SERVER_ALREADY_IN_GAME);
return true;
}
}
// TODO: check server password etc?
u32 newHostID = server.m_NextHostID++;
session->SetUserName(username);
session->SetGUID(message->m_GUID);
session->SetHostID(newHostID);
CAuthenticateResultMessage authenticateResult;
authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK;
authenticateResult.m_HostID = newHostID;
authenticateResult.m_Message = L"Logged in";
session->SendMessage(&authenticateResult);
server.OnUserJoin(session);
if (isRejoining)
{
// Request a copy of the current game state from an existing player,
// so we can send it on to the new player
// Assume session 0 is most likely the local player, so they're
// the most efficient client to request a copy from
CNetServerSession* sourceSession = server.m_Sessions.at(0);
sourceSession->GetFileTransferer().StartTask(
shared_ptr(new CNetFileReceiveTask_ServerRejoin(server, newHostID))
);
session->SetNextState(NSS_JOIN_SYNCING);
}
return true;
}
bool CNetServerWorker::OnInGame(void* context, CFsmEvent* event)
{
// TODO: should split each of these cases into a separate method
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CNetMessage* message = (CNetMessage*)event->GetParamRef();
if (message->GetType() == (uint)NMT_SIMULATION_COMMAND)
{
CSimulationMessage* simMessage = static_cast (message);
// Ignore messages sent by one player on behalf of another player
// unless cheating is enabled
bool cheatsEnabled = false;
ScriptInterface& scriptInterface = server.GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue settings(cx);
scriptInterface.GetProperty(server.m_GameAttributes.get(), "settings", &settings);
if (scriptInterface.HasProperty(settings, "CheatsEnabled"))
scriptInterface.GetProperty(settings, "CheatsEnabled", cheatsEnabled);
PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID());
// When cheating is disabled, fail if the player the message claims to
// represent does not exist or does not match the sender's player name
if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != simMessage->m_Player))
return true;
// Send it back to all clients immediately
server.Broadcast(simMessage);
// Save all the received commands
if (server.m_SavedCommands.size() < simMessage->m_Turn + 1)
server.m_SavedCommands.resize(simMessage->m_Turn + 1);
server.m_SavedCommands[simMessage->m_Turn].push_back(*simMessage);
// TODO: we shouldn't send the message back to the client that first sent it
}
else if (message->GetType() == (uint)NMT_SYNC_CHECK)
{
CSyncCheckMessage* syncMessage = static_cast (message);
server.m_ServerTurnManager->NotifyFinishedClientUpdate(session->GetHostID(), session->GetUserName(), syncMessage->m_Turn, syncMessage->m_Hash);
}
else if (message->GetType() == (uint)NMT_END_COMMAND_BATCH)
{
CEndCommandBatchMessage* endMessage = static_cast (message);
server.m_ServerTurnManager->NotifyFinishedClientCommands(session->GetHostID(), endMessage->m_Turn);
}
return true;
}
bool CNetServerWorker::OnChat(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CHAT);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CChatMessage* message = (CChatMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message);
return true;
}
bool CNetServerWorker::OnReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_READY);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CReadyMessage* message = (CReadyMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message);
return true;
}
bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// We're in the loading state, so wait until every player has loaded before
// starting the game
ENSURE(server.m_State == SERVER_STATE_LOADING);
server.CheckGameLoadStatus(session);
return true;
}
bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event)
{
// A client rejoining an in-progress game has now finished loading the
// map and deserialized the initial state.
// The simulation may have progressed since then, so send any subsequent
// commands to them and set them as an active player so they can participate
// in all future turns.
//
// (TODO: if it takes a long time for them to receive and execute all these
// commands, the other players will get frozen for that time and may be unhappy;
// we could try repeating this process a few times until the client converges
// on the up-to-date state, before setting them as active.)
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
u32 turn = message->m_CurrentTurn;
u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn();
// Send them all commands received since their saved state,
// and turn-ended messages for any turns that have already been processed
for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i)
{
if (i < server.m_SavedCommands.size())
for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j)
session->SendMessage(&server.m_SavedCommands[i][j]);
if (i <= readyTurn)
{
CEndCommandBatchMessage endMessage;
endMessage.m_Turn = i;
endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i);
session->SendMessage(&endMessage);
}
}
// Tell the turn manager to expect commands from this new client
server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn);
// Tell the client that everything has finished loading and it should start now
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = readyTurn;
session->SendMessage(&loaded);
return true;
}
bool CNetServerWorker::OnRejoined(void* context, CFsmEvent* event)
{
// A client has finished rejoining and the loading screen disappeared.
ENSURE(event->GetType() == (uint)NMT_REJOINED);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message);
return true;
}
bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
server.OnUserLeave(session);
return true;
}
void CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
{
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
if (m_Sessions[i] != changedSession && m_Sessions[i]->GetCurrState() != NSS_INGAME)
return;
}
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = 0;
Broadcast(&loaded);
m_State = SERVER_STATE_INGAME;
}
void CNetServerWorker::StartGame()
{
m_ServerTurnManager = new CNetServerTurnManager(*this);
for (size_t i = 0; i < m_Sessions.size(); ++i)
m_ServerTurnManager->InitialiseClient(m_Sessions[i]->GetHostID(), 0); // TODO: only for non-observers
m_State = SERVER_STATE_LOADING;
// Send the final setup state to all clients
UpdateGameAttributes(&m_GameAttributes.get());
// Remove players and observers that are not present when the game starts
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end();)
if (it->second.m_Enabled)
++it;
else
it = m_PlayerAssignments.erase(it);
SendPlayerAssignments();
CGameStartMessage gameStart;
Broadcast(&gameStart);
}
void CNetServerWorker::UpdateGameAttributes(JS::MutableHandleValue attrs)
{
m_GameAttributes.set(m_ScriptInterface->GetJSRuntime(), attrs);
if (!m_Host)
return;
CGameSetupMessage gameSetupMessage(GetScriptInterface());
gameSetupMessage.m_Data.set(m_GameAttributes.get());
Broadcast(&gameSetupMessage);
}
CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
{
const size_t MAX_LENGTH = 32;
CStrW name = original;
name.Replace(L"[", L"{"); // remove GUI tags
name.Replace(L"]", L"}"); // remove for symmetry
// Restrict the length
if (name.length() > MAX_LENGTH)
name = name.Left(MAX_LENGTH);
// Don't allow surrounding whitespace
name.Trim(PS_TRIM_BOTH);
// Don't allow empty name
if (name.empty())
name = L"Anonymous";
return name;
}
CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
{
CStrW name = original;
// Try names "Foo", "Foo (2)", "Foo (3)", etc
size_t id = 2;
while (true)
{
bool unique = true;
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
if (m_Sessions[i]->GetUserName() == name)
{
unique = false;
break;
}
}
if (unique)
return name;
name = original + L" (" + CStrW::FromUInt(id++) + L")";
}
}
CNetServer::CNetServer(int autostartPlayers) :
m_Worker(new CNetServerWorker(autostartPlayers))
{
}
CNetServer::~CNetServer()
{
delete m_Worker;
}
bool CNetServer::SetupConnection()
{
return m_Worker->SetupConnection();
}
bool CNetServer::KickPlayer(const CStrW& playerName, const bool ban)
{
CScopeLock lock(m_Worker->m_WorkerMutex);
return m_Worker->KickPlayer(playerName, ban);
}
void CNetServer::AssignPlayer(int playerID, const CStr& guid)
{
CScopeLock lock(m_Worker->m_WorkerMutex);
m_Worker->m_AssignPlayerQueue.emplace_back(playerID, guid);
}
void CNetServer::SetPlayerReady(const CStr& guid, int ready)
{
CScopeLock lock(m_Worker->m_WorkerMutex);
m_Worker->m_PlayerReadyQueue.emplace_back(guid, ready);
}
void CNetServer::ClearAllPlayerReady()
{
CScopeLock lock(m_Worker->m_WorkerMutex);
m_Worker->m_PlayerResetReadyQueue.push_back(false);
}
void CNetServer::StartGame()
{
CScopeLock lock(m_Worker->m_WorkerMutex);
m_Worker->m_StartGameQueue.push_back(true);
}
void CNetServer::UpdateGameAttributes(JS::MutableHandleValue attrs, ScriptInterface& scriptInterface)
{
// Pass the attributes as JSON, since that's the easiest safe
// cross-thread way of passing script data
std::string attrsJSON = scriptInterface.StringifyJSON(attrs, false);
CScopeLock lock(m_Worker->m_WorkerMutex);
m_Worker->m_GameAttributesQueue.push_back(attrsJSON);
}
void CNetServer::SetTurnLength(u32 msecs)
{
CScopeLock lock(m_Worker->m_WorkerMutex);
m_Worker->m_TurnLengthQueue.push_back(msecs);
}
Index: ps/trunk/source/network/NetServer.h
===================================================================
--- ps/trunk/source/network/NetServer.h (revision 17729)
+++ ps/trunk/source/network/NetServer.h (revision 17730)
@@ -1,366 +1,375 @@
-/* Copyright (C) 2015 Wildfire Games.
+/* 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 .
*/
#ifndef NETSERVER_H
#define NETSERVER_H
#include "NetFileTransfer.h"
#include "NetHost.h"
#include "lib/config2.h"
#include "ps/ThreadUtil.h"
#include "scriptinterface/ScriptVal.h"
#include
class CNetServerSession;
class CNetServerTurnManager;
class CFsmEvent;
class ScriptInterface;
class CPlayerAssignmentMessage;
class CNetStatsTable;
class CSimulationMessage;
class CNetServerWorker;
enum NetServerState
{
// We haven't opened the port yet, we're just setting some stuff up.
// The worker thread has not been started.
SERVER_STATE_UNCONNECTED,
// The server is open and accepting connections. This is the screen where
// rules are set up by the operator and where players join and select civs
// and stuff.
SERVER_STATE_PREGAME,
// All the hosts are connected and are loading the game
SERVER_STATE_LOADING,
// The one with all the killing ;-)
SERVER_STATE_INGAME,
// The game is over and someone has won. Players might linger to chat or
// download the replay log.
SERVER_STATE_POSTGAME
};
/**
* Server session representation of client state
*/
enum NetServerSessionState
{
// The client has disconnected or been disconnected
NSS_UNCONNECTED,
// The client has just connected and we're waiting for its handshake message,
// to agree on the protocol version
NSS_HANDSHAKE,
// The client has handshook and we're waiting for its authentication message,
// to find its name and check its password etc
NSS_AUTHENTICATE,
// The client has fully joined, and is in the pregame setup stage
// or is loading the game.
// Server must be in SERVER_STATE_PREGAME or SERVER_STATE_LOADING.
NSS_PREGAME,
// The client has authenticated but the game was already started,
// so it's synchronising with the game state from other clients
NSS_JOIN_SYNCING,
// The client is running the game.
// Server must be in SERVER_STATE_LOADING or SERVER_STATE_INGAME.
NSS_INGAME
};
/**
* Network server interface. Handles all the coordination between players.
* One person runs this object, and every player (including the host) connects their CNetClient to it.
*
* The actual work is performed by CNetServerWorker in a separate thread.
*/
class CNetServer
{
NONCOPYABLE(CNetServer);
public:
/**
* Construct a new network server.
* @param autostartPlayers if positive then StartGame will be called automatically
* once this many players are connected (intended for the command-line testing mode).
*/
CNetServer(int autostartPlayers = -1);
~CNetServer();
/**
* Begin listening for network connections.
* This function is synchronous (it won't return until the connection is established).
* @return true on success, false on error (e.g. port already in use)
*/
bool SetupConnection();
/**
* Call from the GUI to update the player assignments.
* The given GUID will be (re)assigned to the given player ID.
* Any player currently using that ID will be unassigned.
* The changes will be asynchronously propagated to all clients.
*/
void AssignPlayer(int playerID, const CStr& guid);
/**
* Call from the GUI to update the player readiness.
* The changes will be asynchronously propagated to all clients.
*/
void SetPlayerReady(const CStr& guid, int ready);
/**
* Call from the GUI to set the all player readiness to 0.
* The changes will be asynchronously propagated to all clients.
*/
void ClearAllPlayerReady();
/**
* Disconnects a player from gamesetup or session.
*/
bool KickPlayer(const CStrW& playerName, const bool ban);
/**
* Call from the GUI to asynchronously notify all clients that they should start loading the game.
*/
void StartGame();
/**
* Call from the GUI to update the game setup attributes.
* This must be called at least once before starting the game.
* The changes will be asynchronously propagated to all clients.
* @param attrs game attributes, in the script context of scriptInterface
*/
void UpdateGameAttributes(JS::MutableHandleValue attrs, ScriptInterface& scriptInterface);
/**
* Set the turn length to a fixed value.
* TODO: we should replace this with some adapative lag-dependent computation.
*/
void SetTurnLength(u32 msecs);
private:
CNetServerWorker* m_Worker;
};
/**
* Network server worker thread.
* (This is run in a thread so that client/server communication is not delayed
* by the host player's framerate - the only delay should be the network latency.)
*
* Thread-safety:
* - SetupConnection and constructor/destructor must be called from the main thread.
* - The main thread may push commands onto the Queue members,
* while holding the m_WorkerMutex lock.
* - Public functions (SendMessage, Broadcast) must be called from the network
* server thread.
*/
class CNetServerWorker
{
NONCOPYABLE(CNetServerWorker);
public:
// Public functions for CNetSession/CNetServerTurnManager to use:
/**
* Send a message to the given network peer.
*/
bool SendMessage(ENetPeer* peer, const CNetMessage* message);
/**
* Disconnects a player from gamesetup or session.
*/
bool KickPlayer(const CStrW& playerName, const bool ban);
/**
* Send a message to all clients who have completed the full connection process
* (i.e. are in the pre-game or in-game states).
*/
bool Broadcast(const CNetMessage* message);
private:
friend class CNetServer;
friend class CNetFileReceiveTask_ServerRejoin;
CNetServerWorker(int autostartPlayers);
~CNetServerWorker();
/**
* Begin listening for network connections.
* @return true on success, false on error (e.g. port already in use)
*/
bool SetupConnection();
/**
* Call from the GUI to update the player assignments.
* The given GUID will be (re)assigned to the given player ID.
* Any player currently using that ID will be unassigned.
* The changes will be propagated to all clients.
*/
void AssignPlayer(int playerID, const CStr& guid);
/**
* Call from the GUI to notify all clients that they should start loading the game.
*/
void StartGame();
/**
* Call from the GUI to update the game setup attributes.
* This must be called at least once before starting the game.
* The changes will be propagated to all clients.
* @param attrs game attributes, in the script context of GetScriptInterface()
*/
void UpdateGameAttributes(JS::MutableHandleValue attrs);
/**
* Make a player name 'nicer' by limiting the length and removing forbidden characters etc.
*/
static CStrW SanitisePlayerName(const CStrW& original);
/**
* Make a player name unique, if it matches any existing session's name.
*/
CStrW DeduplicatePlayerName(const CStrW& original);
/**
* Get the script context used for game attributes.
*/
ScriptInterface& GetScriptInterface();
/**
* Set the turn length to a fixed value.
* TODO: we should replace this with some adaptive lag-dependent computation.
*/
void SetTurnLength(u32 msecs);
void AddPlayer(const CStr& guid, const CStrW& name);
void RemovePlayer(const CStr& guid);
void SetPlayerReady(const CStr& guid, const int ready);
void SendPlayerAssignments();
void ClearAllPlayerReady();
void SetupSession(CNetServerSession* session);
bool HandleConnect(CNetServerSession* session);
void OnUserJoin(CNetServerSession* session);
void OnUserLeave(CNetServerSession* session);
static bool OnClientHandshake(void* context, CFsmEvent* event);
static bool OnAuthenticate(void* context, CFsmEvent* event);
static bool OnInGame(void* context, CFsmEvent* event);
static bool OnChat(void* context, CFsmEvent* event);
static bool OnReady(void* context, CFsmEvent* event);
static bool OnLoadedGame(void* context, CFsmEvent* event);
static bool OnJoinSyncingLoadedGame(void* context, CFsmEvent* event);
static bool OnRejoined(void* context, CFsmEvent* event);
static bool OnDisconnect(void* context, CFsmEvent* event);
void CheckGameLoadStatus(CNetServerSession* changedSession);
void ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message);
void HandleMessageReceive(const CNetMessage* message, CNetServerSession* session);
+ /**
+ * Send a network warning if the connection to a client is being lost or has bad latency.
+ */
+ void CheckClientConnections();
/**
* Internal script context for (de)serializing script messages,
* and for storing game attributes.
* (TODO: we shouldn't bother deserializing (except for debug printing of messages),
* we should just forward messages blindly and efficiently.)
*/
ScriptInterface* m_ScriptInterface;
PlayerAssignmentMap m_PlayerAssignments;
/**
* Stores the most current game attributes.
*/
DefPersistentRooted m_GameAttributes;
int m_AutostartPlayers;
ENetHost* m_Host;
std::vector m_Sessions;
CNetStatsTable* m_Stats;
NetServerState m_State;
CStrW m_ServerName;
CStrW m_WelcomeMessage;
std::vector m_BannedIPs;
std::vector m_BannedPlayers;
u32 m_NextHostID;
CNetServerTurnManager* m_ServerTurnManager;
CStr m_HostGUID;
/**
* A copy of all simulation commands received so far, indexed by
* turn number, to simplify support for rejoining etc.
* TODO: verify this doesn't use too much RAM.
*/
std::vector > m_SavedCommands;
/**
* The latest copy of the simulation state, received from an existing
* client when a new client has asked to rejoin the game.
*/
std::string m_JoinSyncFile;
+ /**
+ * Time when the clients connections were last checked for timeouts and latency.
+ */
+ std::time_t m_LastConnectionCheck;
+
private:
// Thread-related stuff:
#if CONFIG2_MINIUPNPC
/**
* Try to find a UPnP root on the network and setup port forwarding.
*/
static void* SetupUPnP(void*);
pthread_t m_UPnPThread;
#endif
static void* RunThread(void* data);
void Run();
bool RunStep();
pthread_t m_WorkerThread;
CMutex m_WorkerMutex;
bool m_Shutdown; // protected by m_WorkerMutex
// Queues for messages sent by the game thread:
std::vector > m_AssignPlayerQueue; // protected by m_WorkerMutex
std::vector m_StartGameQueue; // protected by m_WorkerMutex
std::vector > m_PlayerReadyQueue; // protected by m_WorkerMutex
std::vector m_PlayerResetReadyQueue; // protected by m_WorkerMutex
std::vector m_GameAttributesQueue; // protected by m_WorkerMutex
std::vector m_TurnLengthQueue; // protected by m_WorkerMutex
};
/// Global network server for the standard game
extern CNetServer *g_NetServer;
#endif // NETSERVER_H
Index: ps/trunk/source/network/NetSession.cpp
===================================================================
--- ps/trunk/source/network/NetSession.cpp (revision 17729)
+++ ps/trunk/source/network/NetSession.cpp (revision 17730)
@@ -1,202 +1,236 @@
/* 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 "NetSession.h"
#include "NetClient.h"
#include "NetServer.h"
#include "NetMessage.h"
#include "NetStats.h"
#include "lib/external_libraries/enet.h"
#include "ps/CLogger.h"
#include "scriptinterface/ScriptInterface.h"
+const u32 NETWORK_WARNING_TIMEOUT = 4000;
+
static const int CHANNEL_COUNT = 1;
CNetClientSession::CNetClientSession(CNetClient& client) :
m_Client(client), m_FileTransferer(this), m_Host(NULL), m_Server(NULL), m_Stats(NULL)
{
}
CNetClientSession::~CNetClientSession()
{
delete m_Stats;
if (m_Host && m_Server)
{
// Disconnect immediately (we can't wait for acks)
enet_peer_disconnect_now(m_Server, NDR_SERVER_SHUTDOWN);
enet_host_destroy(m_Host);
m_Host = NULL;
m_Server = NULL;
}
}
bool CNetClientSession::Connect(u16 port, const CStr& server)
{
ENSURE(!m_Host);
ENSURE(!m_Server);
// Create ENet host
ENetHost* host = enet_host_create(NULL, 1, CHANNEL_COUNT, 0, 0);
if (!host)
return false;
// Bind to specified host
ENetAddress addr;
addr.port = port;
if (enet_address_set_host(&addr, server.c_str()) < 0)
return false;
// Initiate connection to server
ENetPeer* peer = enet_host_connect(host, &addr, CHANNEL_COUNT, 0);
if (!peer)
return false;
m_Host = host;
m_Server = peer;
m_Stats = new CNetStatsTable(m_Server);
if (CProfileViewer::IsInitialised())
g_ProfileViewer.AddRootTable(m_Stats);
return true;
}
void CNetClientSession::Disconnect(u32 reason)
{
ENSURE(m_Host && m_Server);
// TODO: ought to do reliable async disconnects, probably
enet_peer_disconnect_now(m_Server, reason);
enet_host_destroy(m_Host);
m_Host = NULL;
m_Server = NULL;
SAFE_DELETE(m_Stats);
}
void CNetClientSession::Poll()
{
PROFILE3("net client poll");
ENSURE(m_Host && m_Server);
m_FileTransferer.Poll();
ENetEvent event;
while (enet_host_service(m_Host, &event, 0) > 0)
{
switch (event.type)
{
case ENET_EVENT_TYPE_CONNECT:
{
ENSURE(event.peer == m_Server);
// Report the server address
char hostname[256] = "(error)";
enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
LOGMESSAGE("Net client: Connected to %s:%u", hostname, (unsigned int)event.peer->address.port);
m_Client.HandleConnect();
break;
}
case ENET_EVENT_TYPE_DISCONNECT:
{
ENSURE(event.peer == m_Server);
LOGMESSAGE("Net client: Disconnected");
m_Client.HandleDisconnect(event.data);
return;
}
case ENET_EVENT_TYPE_RECEIVE:
{
CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, m_Client.GetScriptInterface());
if (msg)
{
LOGMESSAGE("Net client: Received message %s of size %lu from server", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength());
m_Client.HandleMessage(msg);
delete msg;
}
enet_packet_destroy(event.packet);
break;
}
case ENET_EVENT_TYPE_NONE:
break;
}
}
}
void CNetClientSession::Flush()
{
PROFILE3("net client flush");
ENSURE(m_Host && m_Server);
enet_host_flush(m_Host);
}
bool CNetClientSession::SendMessage(const CNetMessage* message)
{
ENSURE(m_Host && m_Server);
return CNetHost::SendMessage(message, m_Server, "server");
}
+u32 CNetClientSession::GetLastReceivedTime() const
+{
+ if (!m_Server)
+ return 0;
+
+ return enet_time_get() - m_Server->lastReceiveTime;
+}
+
+u32 CNetClientSession::GetMeanRTT() const
+{
+ if (!m_Server)
+ return 0;
+
+ return m_Server->roundTripTime;
+}
+
CNetServerSession::CNetServerSession(CNetServerWorker& server, ENetPeer* peer) :
m_Server(server), m_FileTransferer(this), m_Peer(peer)
{
}
CStr CNetServerSession::GetIPAddress() const
{
char ipAddress[256] = "";
if (enet_address_get_host_ip(&m_Peer->address, ipAddress, ARRAY_SIZE(ipAddress)) < 0)
LOGMESSAGE("Could not get IP address of a client!");
return ipAddress;
}
+u32 CNetServerSession::GetLastReceivedTime() const
+{
+ if (!m_Peer)
+ return 0;
+
+ return enet_time_get() - m_Peer->lastReceiveTime;
+}
+
+u32 CNetServerSession::GetMeanRTT() const
+{
+ if (!m_Peer)
+ return 0;
+
+ return m_Peer->roundTripTime;
+}
+
void CNetServerSession::Disconnect(u32 reason)
{
Update((uint)NMT_CONNECTION_LOST, NULL);
enet_peer_disconnect(m_Peer, reason);
}
void CNetServerSession::DisconnectNow(u32 reason)
{
enet_peer_disconnect_now(m_Peer, reason);
}
bool CNetServerSession::SendMessage(const CNetMessage* message)
{
return m_Server.SendMessage(m_Peer, message);
}
Index: ps/trunk/source/network/NetSession.h
===================================================================
--- ps/trunk/source/network/NetSession.h (revision 17729)
+++ ps/trunk/source/network/NetSession.h (revision 17730)
@@ -1,161 +1,186 @@
-/* Copyright (C) 2015 Wildfire Games.
+/* 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 .
*/
#ifndef NETSESSION_H
#define NETSESSION_H
#include "network/fsm.h"
#include "network/NetFileTransfer.h"
#include "network/NetHost.h"
#include "ps/CStr.h"
#include "scriptinterface/ScriptVal.h"
+/**
+ * Report the peer if we didn't receive a packet after this time (milliseconds).
+ */
+extern const u32 NETWORK_WARNING_TIMEOUT;
+
class CNetClient;
class CNetServerWorker;
class CNetStatsTable;
/**
* @file
* Network client/server sessions.
*
* Each session has two classes: CNetClientSession runs on the client,
* and CNetServerSession runs on the server.
* A client runs one session at once; a server typically runs many.
*/
/**
* Interface for sessions to which messages can be sent.
*/
class INetSession
{
public:
virtual ~INetSession() {}
virtual bool SendMessage(const CNetMessage* message) = 0;
};
/**
* The client end of a network session.
* Provides an abstraction of the network interface, allowing communication with the server.
*/
class CNetClientSession : public INetSession
{
NONCOPYABLE(CNetClientSession);
public:
CNetClientSession(CNetClient& client);
~CNetClientSession();
bool Connect(u16 port, const CStr& server);
/**
* Process queued incoming messages.
*/
void Poll();
/**
* Flush queued outgoing network messages.
*/
void Flush();
/**
* Disconnect from the server.
* Sends a disconnection notification to the server.
*/
void Disconnect(u32 reason);
/**
* Send a message to the server.
*/
virtual bool SendMessage(const CNetMessage* message);
+ /**
+ * Number of milliseconds since the most recent packet of the server was received.
+ */
+ u32 GetLastReceivedTime() const;
+
+ /**
+ * Average round trip time to the server.
+ */
+ u32 GetMeanRTT() const;
+
CNetFileTransferer& GetFileTransferer() { return m_FileTransferer; }
private:
CNetClient& m_Client;
CNetFileTransferer m_FileTransferer;
ENetHost* m_Host;
ENetPeer* m_Server;
CNetStatsTable* m_Stats;
};
/**
* The server's end of a network session.
* Represents an abstraction of the state of the client, storing all the per-client data
* needed by the server.
*
* Thread-safety:
* - This is constructed and used by CNetServerWorker in the network server thread.
*/
class CNetServerSession : public CFsm, public INetSession
{
NONCOPYABLE(CNetServerSession);
public:
CNetServerSession(CNetServerWorker& server, ENetPeer* peer);
CNetServerWorker& GetServer() { return m_Server; }
const CStr& GetGUID() const { return m_GUID; }
void SetGUID(const CStr& guid) { m_GUID = guid; }
const CStrW& GetUserName() const { return m_UserName; }
void SetUserName(const CStrW& name) { m_UserName = name; }
u32 GetHostID() const { return m_HostID; }
void SetHostID(u32 id) { m_HostID = id; }
CStr GetIPAddress() const;
/**
+ * Number of milliseconds since the latest packet of that client was received.
+ */
+ u32 GetLastReceivedTime() const;
+
+ /**
+ * Average round trip time to the client.
+ */
+ u32 GetMeanRTT() const;
+
+ /**
* Sends a disconnection notification to the client,
* and sends a NMT_CONNECTION_LOST message to the session FSM.
* The server will receive a disconnection notification after a while.
* The server will not receive any further messages sent via this session.
*/
void Disconnect(u32 reason);
/**
* Sends an unreliable disconnection notification to the client.
* The server will not receive any disconnection notification.
* The server will not receive any further messages sent via this session.
*/
void DisconnectNow(u32 reason);
/**
* Send a message to the client.
*/
virtual bool SendMessage(const CNetMessage* message);
CNetFileTransferer& GetFileTransferer() { return m_FileTransferer; }
private:
CNetServerWorker& m_Server;
CNetFileTransferer m_FileTransferer;
ENetPeer* m_Peer;
CStr m_GUID;
CStrW m_UserName;
u32 m_HostID;
};
#endif // NETSESSION_H
Index: ps/trunk/source/network/NetTurnManager.cpp
===================================================================
--- ps/trunk/source/network/NetTurnManager.cpp (revision 17729)
+++ ps/trunk/source/network/NetTurnManager.cpp (revision 17730)
@@ -1,719 +1,719 @@
/* 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 "NetTurnManager.h"
#include "NetMessage.h"
#include "network/NetServer.h"
#include "network/NetClient.h"
#include "network/NetMessage.h"
#include "gui/GUIManager.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "ps/Profile.h"
#include "ps/Pyrogenesis.h"
#include "ps/Replay.h"
#include "ps/SavedGame.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
#include
#include
#include
-static const int DEFAULT_TURN_LENGTH_MP = 500;
-static const int DEFAULT_TURN_LENGTH_SP = 200;
+const u32 DEFAULT_TURN_LENGTH_MP = 500;
+const u32 DEFAULT_TURN_LENGTH_SP = 200;
static const int COMMAND_DELAY = 2;
#if 0
#define NETTURN_LOG(args) debug_printf args
#else
#define NETTURN_LOG(args)
#endif
static std::string Hexify(const std::string& s)
{
std::stringstream str;
str << std::hex;
for (size_t i = 0; i < s.size(); ++i)
str << std::setfill('0') << std::setw(2) << (int)(unsigned char)s[i];
return str.str();
}
CNetTurnManager::CNetTurnManager(CSimulation2& simulation, u32 defaultTurnLength, int clientId, IReplayLogger& replay) :
m_Simulation2(simulation), m_CurrentTurn(0), m_ReadyTurn(1), m_TurnLength(defaultTurnLength), m_DeltaSimTime(0),
m_PlayerId(-1), m_ClientId(clientId), m_HasSyncError(false), m_Replay(replay),
m_TimeWarpNumTurns(0), m_FinalTurn(0)
{
// When we are on turn n, we schedule new commands for n+2.
// We know that all other clients have finished scheduling commands for n (else we couldn't have got here).
// We know we have not yet finished scheduling commands for n+2.
// Hence other clients can be on turn n-1, n, n+1, and no other.
// So they can be sending us commands scheduled for n+1, n+2, n+3.
// So we need a 3-element buffer:
m_QueuedCommands.resize(COMMAND_DELAY + 1);
}
void CNetTurnManager::ResetState(u32 newCurrentTurn, u32 newReadyTurn)
{
m_CurrentTurn = newCurrentTurn;
m_ReadyTurn = newReadyTurn;
m_DeltaSimTime = 0;
size_t queuedCommandsSize = m_QueuedCommands.size();
m_QueuedCommands.clear();
m_QueuedCommands.resize(queuedCommandsSize);
}
void CNetTurnManager::SetPlayerID(int playerId)
{
m_PlayerId = playerId;
}
bool CNetTurnManager::WillUpdate(float simFrameLength)
{
// Keep this in sync with the return value of Update()
if (m_FinalTurn > 0 && m_CurrentTurn > m_FinalTurn)
return false;
if (m_DeltaSimTime + simFrameLength < 0)
return false;
if (m_ReadyTurn <= m_CurrentTurn)
return false;
return true;
}
bool CNetTurnManager::Update(float simFrameLength, size_t maxTurns)
{
if (m_FinalTurn > 0 && m_CurrentTurn > m_FinalTurn)
return false;
m_DeltaSimTime += simFrameLength;
// If the game becomes laggy, m_DeltaSimTime increases progressively.
// The engine will fast forward accordingly to catch up.
// To keep the game playable, stop fast forwarding after 2 turn lengths.
m_DeltaSimTime = std::min(m_DeltaSimTime, 2.0f * m_TurnLength / 1000.0f);
// If we haven't reached the next turn yet, do nothing
if (m_DeltaSimTime < 0)
return false;
NETTURN_LOG((L"Update current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn));
// Check that the next turn is ready for execution
if (m_ReadyTurn <= m_CurrentTurn)
{
// Oops, we wanted to start the next turn but it's not ready yet -
// there must be too much network lag.
// TODO: complain to the user.
// TODO: send feedback to the server to increase the turn length.
// Reset the next-turn timer to 0 so we try again next update but
// so we don't rush to catch up in subsequent turns.
// TODO: we should do clever rate adjustment instead of just pausing like this.
m_DeltaSimTime = 0;
return false;
}
maxTurns = std::max((size_t)1, maxTurns); // always do at least one turn
for (size_t i = 0; i < maxTurns; ++i)
{
// Check that we've reached the i'th next turn
if (m_DeltaSimTime < 0)
break;
// Check that the i'th next turn is still ready
if (m_ReadyTurn <= m_CurrentTurn)
break;
NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY);
m_CurrentTurn += 1; // increase the turn number now, so Update can send new commands for a subsequent turn
// Clean up any destroyed entities since the last turn (e.g. placement previews
// or rally point flags generated by the GUI). (Must do this before the time warp
// serialization.)
m_Simulation2.FlushDestroyedEntities();
// Save the current state for rewinding, if enabled
if (m_TimeWarpNumTurns && (m_CurrentTurn % m_TimeWarpNumTurns) == 0)
{
PROFILE3("time warp serialization");
std::stringstream stream;
m_Simulation2.SerializeState(stream);
m_TimeWarpStates.push_back(stream.str());
}
// Put all the client commands into a single list, in a globally consistent order
std::vector commands;
for (std::map >::iterator it = m_QueuedCommands[0].begin(); it != m_QueuedCommands[0].end(); ++it)
{
commands.insert(commands.end(), std::make_move_iterator(it->second.begin()), std::make_move_iterator(it->second.end()));
}
m_QueuedCommands.pop_front();
m_QueuedCommands.resize(m_QueuedCommands.size() + 1);
m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands);
NETTURN_LOG((L"Running %d cmds\n", commands.size()));
m_Simulation2.Update(m_TurnLength, commands);
NotifyFinishedUpdate(m_CurrentTurn);
// Set the time for the next turn update
m_DeltaSimTime -= m_TurnLength / 1000.f;
}
return true;
}
bool CNetTurnManager::UpdateFastForward()
{
m_DeltaSimTime = 0;
NETTURN_LOG((L"UpdateFastForward current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn));
// Check that the next turn is ready for execution
if (m_ReadyTurn <= m_CurrentTurn)
return false;
while (m_ReadyTurn > m_CurrentTurn)
{
// TODO: It would be nice to remove some of the duplication with Update()
// (This is similar but doesn't call any Notify functions or update DeltaTime,
// it just updates the simulation state)
m_CurrentTurn += 1;
m_Simulation2.FlushDestroyedEntities();
// Put all the client commands into a single list, in a globally consistent order
std::vector commands;
for (std::map >::iterator it = m_QueuedCommands[0].begin(); it != m_QueuedCommands[0].end(); ++it)
{
commands.insert(commands.end(), std::make_move_iterator(it->second.begin()), std::make_move_iterator(it->second.end()));
}
m_QueuedCommands.pop_front();
m_QueuedCommands.resize(m_QueuedCommands.size() + 1);
m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands);
NETTURN_LOG((L"Running %d cmds\n", commands.size()));
m_Simulation2.Update(m_TurnLength, commands);
}
return true;
}
void CNetTurnManager::OnSyncError(u32 turn, const CStr& expectedHash, std::vector& playerNames)
{
NETTURN_LOG((L"OnSyncError(%d, %hs)\n", turn, Hexify(expectedHash).c_str()));
// Only complain the first time
if (m_HasSyncError)
return;
bool quick = !TurnNeedsFullHash(turn);
std::string hash;
ENSURE(m_Simulation2.ComputeStateHash(hash, quick));
OsPath path = psLogDir()/"oos_dump.txt";
std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
m_Simulation2.DumpDebugState(file);
file.close();
hash = Hexify(hash);
const std::string& expectedHashHex = Hexify(expectedHash);
DisplayOOSError(turn, hash, expectedHashHex, false, &playerNames, &path);
}
void CNetTurnManager::DisplayOOSError(u32 turn, const CStr& hash, const CStr& expectedHash, bool isReplay, std::vector* playerNames = NULL, OsPath* path = NULL)
{
m_HasSyncError = true;
std::stringstream msg;
msg << "Out of sync on turn " << turn;
if (playerNames)
for (size_t i = 0; i < playerNames->size(); ++i)
msg << (i == 0 ? "\nPlayers: " : ", ") << utf8_from_wstring((*playerNames)[i].m_Name);
if (isReplay)
msg << "\n\n" << "The current game state is different from the original game state.";
else
msg << "\n\n" << "Your game state is " << (expectedHash == hash ? "identical to" : "different from") << " the hosts game state.";
if (path)
msg << "\n\n" << "Dumping current state to " << CStr(path->string8()).EscapeToPrintableASCII();
LOGERROR("%s", msg.str());
if (g_GUI)
g_GUI->DisplayMessageBox(600, 350, L"Sync error", wstring_from_utf8(msg.str()));
}
void CNetTurnManager::Interpolate(float simFrameLength, float realFrameLength)
{
// TODO: using m_TurnLength might be a bit dodgy when length changes - maybe
// we need to save the previous turn length?
float offset = clamp(m_DeltaSimTime / (m_TurnLength / 1000.f) + 1.0, 0.0, 1.0);
if (m_FinalTurn > 0 && m_CurrentTurn > m_FinalTurn)
simFrameLength = 0;
m_Simulation2.Interpolate(simFrameLength, offset, realFrameLength);
}
void CNetTurnManager::AddCommand(int client, int player, JS::HandleValue data, u32 turn)
{
NETTURN_LOG((L"AddCommand(client=%d player=%d turn=%d)\n", client, player, turn));
if (!(m_CurrentTurn < turn && turn <= m_CurrentTurn + COMMAND_DELAY + 1))
{
debug_warn(L"Received command for invalid turn");
return;
}
m_Simulation2.GetScriptInterface().FreezeObject(data, true);
m_QueuedCommands[turn - (m_CurrentTurn+1)][client].emplace_back(player, m_Simulation2.GetScriptInterface().GetContext(), data);
}
void CNetTurnManager::FinishedAllCommands(u32 turn, u32 turnLength)
{
NETTURN_LOG((L"FinishedAllCommands(%d, %d)\n", turn, turnLength));
ENSURE(turn == m_ReadyTurn + 1);
m_ReadyTurn = turn;
m_TurnLength = turnLength;
}
bool CNetTurnManager::TurnNeedsFullHash(u32 turn)
{
// Check immediately for errors caused by e.g. inconsistent game versions
// (The hash is computed after the first sim update, so we start at turn == 1)
if (turn == 1)
return true;
// Otherwise check the full state every ~10 seconds in multiplayer games
// (TODO: should probably remove this when we're reasonably sure the game
// isn't too buggy, since the full hash is still pretty slow)
if (turn % 20 == 0)
return true;
return false;
}
void CNetTurnManager::EnableTimeWarpRecording(size_t numTurns)
{
m_TimeWarpStates.clear();
m_TimeWarpNumTurns = numTurns;
}
void CNetTurnManager::RewindTimeWarp()
{
if (m_TimeWarpStates.empty())
return;
std::stringstream stream(m_TimeWarpStates.back());
m_Simulation2.DeserializeState(stream);
m_TimeWarpStates.pop_back();
// Reset the turn manager state, so we won't execute stray commands and
// won't do the next snapshot until the appropriate time.
// (Ideally we ought to serialise the turn manager state and restore it
// here, but this is simpler for now.)
ResetState(0, 1);
}
void CNetTurnManager::QuickSave()
{
TIMER(L"QuickSave");
std::stringstream stream;
if (!m_Simulation2.SerializeState(stream))
{
LOGERROR("Failed to quicksave game");
return;
}
m_QuickSaveState = stream.str();
if (g_GUI)
m_QuickSaveMetadata = g_GUI->GetSavedGameData();
else
m_QuickSaveMetadata = std::string();
LOGMESSAGERENDER("Quicksaved game");
}
void CNetTurnManager::QuickLoad()
{
TIMER(L"QuickLoad");
if (m_QuickSaveState.empty())
{
LOGERROR("Cannot quickload game - no game was quicksaved");
return;
}
std::stringstream stream(m_QuickSaveState);
if (!m_Simulation2.DeserializeState(stream))
{
LOGERROR("Failed to quickload game");
return;
}
if (g_GUI && !m_QuickSaveMetadata.empty())
g_GUI->RestoreSavedGameData(m_QuickSaveMetadata);
LOGMESSAGERENDER("Quickloaded game");
// See RewindTimeWarp
ResetState(0, 1);
}
CNetClientTurnManager::CNetClientTurnManager(CSimulation2& simulation, CNetClient& client, int clientId, IReplayLogger& replay) :
CNetTurnManager(simulation, DEFAULT_TURN_LENGTH_MP, clientId, replay), m_NetClient(client)
{
}
void CNetClientTurnManager::PostCommand(JS::HandleValue data)
{
NETTURN_LOG((L"PostCommand()\n"));
// Transmit command to server
CSimulationMessage msg(m_Simulation2.GetScriptInterface(), m_ClientId, m_PlayerId, m_CurrentTurn + COMMAND_DELAY, data);
m_NetClient.SendMessage(&msg);
// Add to our local queue
//AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + COMMAND_DELAY);
// TODO: we should do this when the server stops sending our commands back to us
}
void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn)
{
NETTURN_LOG((L"NotifyFinishedOwnCommands(%d)\n", turn));
// Send message to the server
CEndCommandBatchMessage msg;
msg.m_TurnLength = DEFAULT_TURN_LENGTH_MP; // TODO: why do we send this?
msg.m_Turn = turn;
m_NetClient.SendMessage(&msg);
}
void CNetClientTurnManager::NotifyFinishedUpdate(u32 turn)
{
bool quick = !TurnNeedsFullHash(turn);
std::string hash;
{
PROFILE3("state hash check");
ENSURE(m_Simulation2.ComputeStateHash(hash, quick));
}
NETTURN_LOG((L"NotifyFinishedUpdate(%d, %hs)\n", turn, Hexify(hash).c_str()));
m_Replay.Hash(hash, quick);
// Don't send the hash if OOS
if (m_HasSyncError)
return;
// Send message to the server
CSyncCheckMessage msg;
msg.m_Turn = turn;
msg.m_Hash = hash;
m_NetClient.SendMessage(&msg);
}
void CNetClientTurnManager::OnDestroyConnection()
{
NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY);
}
void CNetClientTurnManager::OnSimulationMessage(CSimulationMessage* msg)
{
// Command received from the server - store it for later execution
AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, msg->m_Turn);
}
CNetLocalTurnManager::CNetLocalTurnManager(CSimulation2& simulation, IReplayLogger& replay) :
CNetTurnManager(simulation, DEFAULT_TURN_LENGTH_SP, 0, replay)
{
}
void CNetLocalTurnManager::PostCommand(JS::HandleValue data)
{
// Add directly to the next turn, ignoring COMMAND_DELAY,
// because we don't need to compensate for network latency
AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + 1);
}
void CNetLocalTurnManager::NotifyFinishedOwnCommands(u32 turn)
{
FinishedAllCommands(turn, m_TurnLength);
}
void CNetLocalTurnManager::NotifyFinishedUpdate(u32 UNUSED(turn))
{
#if 0 // this hurts performance and is only useful for verifying log replays
std::string hash;
{
PROFILE3("state hash check");
ENSURE(m_Simulation2.ComputeStateHash(hash));
}
m_Replay.Hash(hash);
#endif
}
void CNetLocalTurnManager::OnSimulationMessage(CSimulationMessage* UNUSED(msg))
{
debug_warn(L"This should never be called");
}
CNetReplayTurnManager::CNetReplayTurnManager(CSimulation2& simulation, IReplayLogger& replay) :
CNetLocalTurnManager(simulation, replay)
{
}
void CNetReplayTurnManager::StoreReplayCommand(u32 turn, int player, const std::string& command)
{
// Using the pair we make sure that commands per turn will be processed in the correct order
m_ReplayCommands[turn].emplace_back(player, command);
}
void CNetReplayTurnManager::StoreReplayHash(u32 turn, const std::string& hash, bool quick)
{
m_ReplayHash[turn] = std::make_pair(hash, quick);
}
void CNetReplayTurnManager::StoreReplayTurnLength(u32 turn, u32 turnLength)
{
m_ReplayTurnLengths[turn] = turnLength;
// Initialize turn length
if (turn == 0)
m_TurnLength = m_ReplayTurnLengths[0];
}
void CNetReplayTurnManager::StoreFinalReplayTurn(u32 turn)
{
m_FinalTurn = turn;
}
void CNetReplayTurnManager::NotifyFinishedUpdate(u32 turn)
{
if (turn > m_FinalTurn)
return;
debug_printf("Executing turn %d of %d\n", turn, m_FinalTurn);
DoTurn(turn);
// Compare hash if it exists in the replay and if we didn't have an OOS already
if (m_HasSyncError || m_ReplayHash.find(turn) == m_ReplayHash.end())
return;
std::string expectedHash = m_ReplayHash[turn].first;
bool quickHash = m_ReplayHash[turn].second;
// Compute hash
std::string hash;
ENSURE(m_Simulation2.ComputeStateHash(hash, quickHash));
hash = Hexify(hash);
if (hash != expectedHash)
DisplayOOSError(turn, hash, expectedHash, true);
}
void CNetReplayTurnManager::DoTurn(u32 turn)
{
// Save turn length
m_TurnLength = m_ReplayTurnLengths[turn];
// Simulate commands for that turn
for (const std::pair& pair : m_ReplayCommands[turn])
{
JS::RootedValue command(m_Simulation2.GetScriptInterface().GetContext());
m_Simulation2.GetScriptInterface().ParseJSON(pair.second, &command);
AddCommand(m_ClientId, pair.first, command, m_CurrentTurn + 1);
}
if (turn == m_FinalTurn)
g_GUI->SendEventToAll("ReplayFinished");
}
CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server) :
m_NetServer(server), m_ReadyTurn(1), m_TurnLength(DEFAULT_TURN_LENGTH_MP), m_HasSyncError(false)
{
// The first turn we will actually execute is number 2,
// so store dummy values into the saved lengths list
m_SavedTurnLengths.push_back(0);
m_SavedTurnLengths.push_back(0);
}
void CNetServerTurnManager::NotifyFinishedClientCommands(int client, u32 turn)
{
NETTURN_LOG((L"NotifyFinishedClientCommands(client=%d, turn=%d)\n", client, turn));
// Must be a client we've already heard of
ENSURE(m_ClientsReady.find(client) != m_ClientsReady.end());
// Clients must advance one turn at a time
ENSURE(turn == m_ClientsReady[client] + 1);
m_ClientsReady[client] = turn;
// Check whether this was the final client to become ready
CheckClientsReady();
}
void CNetServerTurnManager::CheckClientsReady()
{
// See if all clients (including self) are ready for a new turn
for (std::map::iterator it = m_ClientsReady.begin(); it != m_ClientsReady.end(); ++it)
{
NETTURN_LOG((L" %d: %d <=? %d\n", it->first, it->second, m_ReadyTurn));
if (it->second <= m_ReadyTurn)
return; // wasn't ready for m_ReadyTurn+1
}
// Advance the turn
++m_ReadyTurn;
NETTURN_LOG((L"CheckClientsReady: ready for turn %d\n", m_ReadyTurn));
// Tell all clients that the next turn is ready
CEndCommandBatchMessage msg;
msg.m_TurnLength = m_TurnLength;
msg.m_Turn = m_ReadyTurn;
m_NetServer.Broadcast(&msg);
// Save the turn length in case it's needed later
ENSURE(m_SavedTurnLengths.size() == m_ReadyTurn);
m_SavedTurnLengths.push_back(m_TurnLength);
}
void CNetServerTurnManager::NotifyFinishedClientUpdate(int client, const CStrW& playername, u32 turn, const CStr& hash)
{
// Clients must advance one turn at a time
ENSURE(turn == m_ClientsSimulated[client] + 1);
m_ClientsSimulated[client] = turn;
// Check for OOS only if in sync
if (m_HasSyncError)
return;
m_ClientPlayernames[client] = playername;
m_ClientStateHashes[turn][client] = hash;
// Find the newest turn which we know all clients have simulated
u32 newest = std::numeric_limits::max();
for (std::map::iterator it = m_ClientsSimulated.begin(); it != m_ClientsSimulated.end(); ++it)
{
if (it->second < newest)
newest = it->second;
}
// For every set of state hashes that all clients have simulated, check for OOS
for (std::map >::iterator it = m_ClientStateHashes.begin(); it != m_ClientStateHashes.end(); ++it)
{
if (it->first > newest)
break;
// Assume the host is correct (maybe we should choose the most common instead to help debugging)
std::string expected = it->second.begin()->second;
// Find all players that are OOS on that turn
std::vector OOSPlayerNames;
for (std::map::iterator cit = it->second.begin(); cit != it->second.end(); ++cit)
{
NETTURN_LOG((L"sync check %d: %d = %hs\n", it->first, cit->first, Hexify(cit->second).c_str()));
if (cit->second != expected)
{
// Oh no, out of sync
m_HasSyncError = true;
OOSPlayerNames.push_back(m_ClientPlayernames[cit->first]);
}
}
// Tell everyone about it
if (m_HasSyncError)
{
CSyncErrorMessage msg;
msg.m_Turn = it->first;
msg.m_HashExpected = expected;
for (const CStrW& playername : OOSPlayerNames)
{
CSyncErrorMessage::S_m_PlayerNames h;
h.m_Name = playername;
msg.m_PlayerNames.push_back(h);
}
m_NetServer.Broadcast(&msg);
break;
}
}
// Delete the saved hashes for all turns that we've already verified
m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1));
}
void CNetServerTurnManager::InitialiseClient(int client, u32 turn)
{
NETTURN_LOG((L"InitialiseClient(client=%d, turn=%d)\n", client, turn));
ENSURE(m_ClientsReady.find(client) == m_ClientsReady.end());
m_ClientsReady[client] = turn + 1;
m_ClientsSimulated[client] = turn;
}
void CNetServerTurnManager::UninitialiseClient(int client)
{
NETTURN_LOG((L"UninitialiseClient(client=%d)\n", client));
ENSURE(m_ClientsReady.find(client) != m_ClientsReady.end());
m_ClientsReady.erase(client);
m_ClientsSimulated.erase(client);
// Check whether we're ready for the next turn now that we're not
// waiting for this client any more
CheckClientsReady();
}
void CNetServerTurnManager::SetTurnLength(u32 msecs)
{
m_TurnLength = msecs;
}
u32 CNetServerTurnManager::GetSavedTurnLength(u32 turn)
{
ENSURE(turn <= m_ReadyTurn);
return m_SavedTurnLengths.at(turn);
}
Index: ps/trunk/source/network/NetTurnManager.h
===================================================================
--- ps/trunk/source/network/NetTurnManager.h (revision 17729)
+++ ps/trunk/source/network/NetTurnManager.h (revision 17730)
@@ -1,353 +1,356 @@
-/* Copyright (C) 2015 Wildfire Games.
+/* 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 .
*/
#ifndef INCLUDED_NETTURNMANAGER
#define INCLUDED_NETTURNMANAGER
#include "simulation2/helpers/SimulationCommand.h"
#include "lib/os_path.h"
#include "NetMessage.h"
#include
#include