Index: ps/trunk/binaries/data/config/default.cfg
===================================================================
--- ps/trunk/binaries/data/config/default.cfg (revision 25155)
+++ ps/trunk/binaries/data/config/default.cfg (revision 25156)
@@ -1,550 +1,552 @@
; Global Configuration Settings
;
; **************************************************************
; * DO NOT EDIT THIS FILE if you want personal customisations: *
; * create a text file called "local.cfg" instead, and copy *
; * the lines from this file that you want to change. *
; * *
; * If a setting is part of a section (for instance [hotkey]) *
; * you need to append the section name at the beginning of *
; * your custom line (for instance you need to write *
; * "hotkey.pause = Space" if you want to change the pausing *
; * hotkey to the spacebar). *
; * *
; * On Linux, create: *
; * $XDG_CONFIG_HOME/0ad/config/local.cfg *
; * (Note: $XDG_CONFIG_HOME defaults to ~/.config) *
; * *
; * On OS X, create: *
; * ~/Library/Application\ Support/0ad/config/local.cfg *
; * *
; * On Windows, create: *
; * %appdata%\0ad\config\local.cfg *
; * *
; **************************************************************
; Enable/disable windowed mode by default. (Use Alt+Enter to toggle in the game.)
windowed = false
; Show detailed tooltips (Unit stats)
showdetailedtooltips = false
; Pause the game on window focus loss (Only applicable to single player mode)
pauseonfocusloss = true
; Persist settings after leaving the game setup screen
persistmatchsettings = true
; Default player name to use in multiplayer
; playername = "anonymous"
; Default server name or IP to use in multiplayer
multiplayerserver = "127.0.0.1"
; Force a particular resolution. (If these are 0, the default is
; to keep the current desktop resolution in fullscreen mode or to
; use 1024x768 in windowed mode.)
xres = 0
yres = 0
; Force a non-standard bit depth (if 0 then use the current desktop bit depth)
bpp = 0
; Preferred display (for multidisplay setups, only works with SDL 2.0)
display = 0
; Allows to force GL version for SDL
forceglversion = false
forceglprofile = "compatibility" ; Possible values: compatibility, core, es
forceglmajorversion = 3
forceglminorversion = 3
; Big screenshot tiles
screenshot.tiles = 4
screenshot.tilewidth = 480
screenshot.tileheight = 270
; Emulate right-click with Ctrl+Click on Mac mice
macmouse = false
; System settings:
; if false, actors won't be rendered but anything entity will be.
renderactors = true
watereffects=true ; When disabled, force usage of the fixed pipeline water. This is faster, but really, really ugly.
waterfancyeffects = false
waterrealdepth = true
waterrefraction = true
waterreflection = true
shadows = true
shadowquality = 0 ; Shadow map resolution. (-2 - Very Low, -1 - Low, 0 - Medium, 1 - High, 2 - Very High)
; High values can crash the game when using a graphics card with low memory!
shadowpcf = true
shadowsfixed = false ; When enabled shadows are rendered only on the
shadowsfixeddistance = 300.0 ; fixed distance and without swimming effect.
vsync = false
particles = true
fog = true
silhouettes = true
showsky = true
; Uses a synchonized call to a GL driver to get an error state. Useful
; for a debugging of a system without GL_KHR_debug.
gl.checkerrorafterswap = false
novbo = false
; Disable hardware cursors
nohwcursor = false
; 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
; Use anti-aliasing techniques.
antialiasing = "disabled"
; Use sharpening techniques.
sharpening = "disabled"
sharpness = 0.3
; Quality level of shader effects (set to 10 to display all effects)
materialmgr.quality = 2.0
; Maximum distance to display parallax effect. Set to 0 to disable parallax.
materialmgr.PARALLAX_DIST.max = 150
; Maximum distance to display high quality parallax effect.
materialmgr.PARALLAX_HQ_DIST.max = 75
; Maximum distance to display very high quality parallax effect. Set to 30 to enable.
materialmgr.PARALLAX_VHQ_DIST.max = 0
;;;;;;;;;;;;;;;;;;;;;;;;
; Replace alpha-blending with alpha-testing, for performance experiments
forcealphatest = false
; Color of the sky (in "r g b" format)
skycolor = "0 0 0"
[adaptivefps]
session = 60 ; Throttle FPS in running games (prevents 100% CPU workload).
menu = 60 ; Throttle FPS in menus only.
[profiler2]
server = "127.0.0.1"
server.port = "8000" ; Use a free port on your machine.
server.threads = "6" ; Enough for the browser's parallel connection limit
[hotkey]
; Each one of the specified keys will trigger the action on the left
; for multiple-key combinations, separate keys with '+'.
; See keys.txt for the list of key names.
; > SYSTEM SETTINGS
exit = "Ctrl+Break", "Super+Q", "Alt+F4" ; Exit to desktop
cancel = Escape ; Close or cancel the current dialog box/popup
confirm = Return ; Confirm the current command
pause = Pause, "Shift+Space" ; Pause/unpause game
screenshot = F2 ; Take PNG screenshot
bigscreenshot = "Shift+F2" ; Take large BMP screenshot
togglefullscreen = "Alt+Return" ; Toggle fullscreen/windowed mode
screenshot.watermark = "Alt+K" ; Toggle product/company watermark for official screenshots
wireframe = "Alt+Shift+W" ; Toggle wireframe mode
silhouettes = "Alt+Shift+S" ; Toggle unit silhouettes
; > DIALOG HOTKEYS
summary = "Ctrl+Tab" ; Toggle in-game summary
lobby = "Alt+L" ; Show the multiplayer lobby in a dialog window.
structree = "Alt+Shift+T" ; Show structure tree
civinfo = "Alt+Shift+H" ; Show civilization info
; > CLIPBOARD CONTROLS
copy = "Ctrl+C" ; Copy to clipboard
paste = "Ctrl+V" ; Paste from clipboard
cut = "Ctrl+X" ; Cut selected text and copy to the clipboard
; > CONSOLE SETTINGS
console.toggle = BackQuote, F9 ; Open/close console
; > OVERLAY KEYS
fps.toggle = "Alt+F" ; Toggle frame counter
realtime.toggle = "Alt+T" ; Toggle current display of computer time
timeelapsedcounter.toggle = "F12" ; Toggle time elapsed counter
ceasefirecounter.toggle = "" ; Toggle ceasefire counter
; > HOTKEYS ONLY
chat = Return ; Toggle chat window
teamchat = "T" ; Toggle chat window in team chat mode
privatechat = "L" ; Toggle chat window and select the previous private chat partner
; > QUICKSAVE
quicksave = "Shift+F5"
quickload = "Shift+F8"
[hotkey.camera]
reset = "R" ; Reset camera rotation to default.
follow = "F" ; Follow the first unit in the selection
rallypointfocus = "" ; Focus the camera on the rally point of the selected building
zoom.in = Plus, 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]
cancel = Esc ; Un-select all units and cancel building placement
add = Shift ; Add units to selection
militaryonly = Alt ; Add only military units to the selection
nonmilitaryonly = "Alt+Y" ; Add only non-military units to the selection
idleonly = "I" ; Select only idle units
woundedonly = "O" ; Select only wounded units
remove = Ctrl ; Remove units from selection
idleworker = Period, NumDecimal ; Select next idle worker
idlewarrior = Slash, NumDivide ; Select next idle warrior
idleunit = BackSlash ; Select next idle unit
offscreen = Alt ; Include offscreen units in selection
[hotkey.selection.group.add]
0 = "Shift+0", "Shift+Num0"
1 = "Shift+1", "Shift+Num1"
2 = "Shift+2", "Shift+Num2"
3 = "Shift+3", "Shift+Num3"
4 = "Shift+4", "Shift+Num4"
5 = "Shift+5", "Shift+Num5"
6 = "Shift+6", "Shift+Num6"
7 = "Shift+7", "Shift+Num7"
8 = "Shift+8", "Shift+Num8"
9 = "Shift+9", "Shift+Num9"
[hotkey.selection.group.save]
0 = "Ctrl+0", "Ctrl+Num0"
1 = "Ctrl+1", "Ctrl+Num1"
2 = "Ctrl+2", "Ctrl+Num2"
3 = "Ctrl+3", "Ctrl+Num3"
4 = "Ctrl+4", "Ctrl+Num4"
5 = "Ctrl+5", "Ctrl+Num5"
6 = "Ctrl+6", "Ctrl+Num6"
7 = "Ctrl+7", "Ctrl+Num7"
8 = "Ctrl+8", "Ctrl+Num8"
9 = "Ctrl+9", "Ctrl+Num9"
[hotkey.selection.group.select]
0 = 0, Num0
1 = 1, Num1
2 = 2, Num2
3 = 3, Num3
4 = 4, Num4
5 = 5, Num5
6 = 6, Num6
7 = 7, Num7
8 = 8, Num8
9 = 9, Num9
[hotkey.gamesetup]
mapbrowser.open = "M"
[hotkey.session]
kill = Delete, Backspace ; Destroy selected units
stop = "H" ; Stop the current action
backtowork = "Y" ; The unit will go back to work
unload = "U" ; Unload garrisoned units when a building/mechanical unit is selected
unloadturrets = "U" ; Unload turreted units.
move = "" ; Modifier to move to a point instead of another action (e.g. gather)
attack = Ctrl ; Modifier to attack instead of another action (e.g. capture)
attackmove = Ctrl ; Modifier to attackmove when clicking on a point
attackmoveUnit = "Ctrl+Q" ; Modifier to attackmove targeting only units when clicking on a point
garrison = Ctrl ; Modifier to garrison when clicking on building
occupyturret = Ctrl ; Modifier to occupy a turret when clicking on a turret holder.
autorallypoint = Ctrl ; Modifier to set the rally point on the building itself
guard = "G" ; Modifier to escort/guard when clicking on unit/building
patrol = "P" ; Modifier to patrol a unit
repair = "J" ; Modifier to repair when clicking on building/mechanical unit
queue = Shift ; Modifier to queue unit orders instead of replacing
pushorderfront = "" ; Modifier to push unit orders to the front instead of replacing.
orderone = Alt ; Modifier to order only one entity in selection.
batchtrain = Shift ; Modifier to train units in batches
massbarter = Shift ; Modifier to barter bunch of resources
masstribute = Shift ; Modifier to tribute bunch of resources
noconfirmation = Shift ; Do not ask confirmation when deleting a building/unit
fulltradeswap = Shift ; Modifier to put the desired trade resource to 100%
unloadtype = Shift ; Modifier to unload all units of type
deselectgroup = Ctrl ; Modifier to deselect units when clicking group icon, instead of selecting
rotate.cw = RightBracket ; Rotate building placement preview clockwise
rotate.ccw = LeftBracket ; Rotate building placement preview anticlockwise
snaptoedges = Ctrl ; Modifier to align new structures with nearby existing structure
toggledefaultformation = "" ; Switch between null default formation and the last default formation used (defaults to "box")
; Overlays
showstatusbars = Tab ; Toggle display of status bars
devcommands.toggle = "Alt+D" ; Toggle developer commands panel
highlightguarding = PageDown ; Toggle highlight of guarding units
highlightguarded = PageUp ; Toggle highlight of guarded units
diplomacycolors = "Alt+X" ; Toggle diplomacy colors
toggleattackrange = "Alt+C" ; Toggle display of attack range overlays of selected defensive structures
toggleaurasrange = "Alt+V" ; Toggle display of aura range overlays of selected units and structures
togglehealrange = "Alt+B" ; Toggle display of heal range overlays of selected units
[hotkey.session.gui]
toggle = "Alt+G" ; Toggle visibility of session GUI
menu.toggle = "F10" ; Toggle in-game menu
diplomacy.toggle = "Ctrl+H" ; Toggle in-game diplomacy page
barter.toggle = "Ctrl+B" ; Toggle in-game barter/trade page
objectives.toggle = "Ctrl+O" ; Toggle in-game objectives page
tutorial.toggle = "Ctrl+P" ; Toggle in-game tutorial panel
[hotkey.session.savedgames]
delete = Delete, Backspace ; 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 = "Shift+Backspace" ; If timewarp mode enabled, go back to earlier point in the game
[hotkey.tab]
next = "Tab", "Alt+S" ; Show the next tab
prev = "Shift+Tab", "Alt+W" ; Show the previous tab
[hotkey.text] ; > GUI TEXTBOX HOTKEYS
delete.left = "Ctrl+Backspace" ; Delete word to the left of cursor
delete.right = "Ctrl+Del" ; Delete word to the right of cursor
move.left = "Ctrl+LeftArrow" ; Move cursor to start of word to the left of cursor
move.right = "Ctrl+RightArrow" ; Move cursor to start of word to the right of cursor
[gui]
cursorblinkrate = 0.5 ; Cursor blink rate in seconds (0.0 to disable blinking)
scale = 1.0 ; GUI scaling factor, for improved compatibility with 4K displays
[gui.gamesetup]
enabletips = true ; Enable/Disable tips during gamesetup (for newcomers)
assignplayers = everyone ; Whether to assign joining clients to free playerslots. Possible values: everyone, buddies, disabled.
aidifficulty = 3 ; Difficulty level, from 0 (easiest) to 5 (hardest)
aibehavior = "random" ; Default behavior of the AI (random, balanced, aggressive or defensive)
settingsslide = true ; Enable/Disable settings panel slide
[gui.loadingscreen]
progressdescription = false ; Whether to display the progress percent or a textual description
[gui.session]
camerajump.threshold = 40 ; How close do we have to be to the actual location in order to jump back to the previous one?
timeelapsedcounter = false ; Show the game duration in the top right corner
ceasefirecounter = false ; Show the remaining ceasefire time in the top right corner
batchtrainingsize = 5 ; Number of units to be trained per batch by default (when pressing the hotkey)
scrollbatchratio = 1 ; Number of times you have to scroll to increase/decrease the batchsize by 1
woundedunithotkeythreshold = 33 ; The wounded unit hotkey considers the selected units as wounded if their health percentage falls below this number
attackrange = true ; Display attack range overlays of selected defensive structures
aurasrange = true ; Display aura range overlays of selected units and structures
healrange = true ; Display heal range overlays of selected units
rankabovestatusbar = true ; Show rank icons above status bars
experiencestatusbar = true ; Show an experience status bar above each selected unit
respoptooltipsort = 0 ; Sorting players in the resources and population tooltip by value (0 - no sort, -1 - ascending, 1 - descending)
snaptoedges = "disabled" ; Possible values: disabled, enabled.
snaptoedgesdistancethreshold = 15 ; On which distance we don't snap to edges
disjointcontrolgroups = "true" ; Whether control groups are disjoint sets or entities can be in multiple control groups at the same time.
defaultformation = "special/formations/box" ; For walking orders, automatically put units into this formation if they don't have one already.
formationwalkonly = "true" ; Formations are disabled when giving gather/attack/... orders.
howtoshownames = 0 ; Whether the specific names are show as default, as opposed to the generic names. And whether the secondary names are shown. (0 - show both; specific names primary, 1 - show both; generic names primary, 2 - show only specific names, 3 - show only generic names)
[gui.session.minimap]
blinkduration = 1.7 ; The blink duration while pinging
pingduration = 50.0 ; The duration for which an entity will be pinged after an attack notification
[gui.session.notifications]
attack = true ; Show a chat notification if you are attacked by another player
tribute = true ; Show a chat notification if an ally tributes resources to another team member if teams are locked, and all tributes in observer mode
barter = true ; Show a chat notification to observers when a player bartered resources
phase = completed ; Show a chat notification if you or an ally have started, aborted or completed a new phase, and phases of all players in observer mode. Possible values: none, completed, all.
[gui.splashscreen]
enable = true ; Enable/disable the splashscreen
version = 0 ; Splashscreen version (date of last modification). By default, 0 to force splashscreen to appear at first launch
[gui.session.diplomacycolors]
self = "21 55 149" ; Color of your units when diplomacy colors are enabled
ally = "86 180 31" ; Color of allies when diplomacy colors are enabled
neutral = "231 200 5" ; Color of neutral players when diplomacy colors are enabled
enemy = "150 20 20" ; Color of enemies when diplomacy colors are enabled
[joystick] ; EXPERIMENTAL: joystick/gamepad settings
enable = false
deadzone = 8192
[chat]
timestamp = true ; Show at which time chat messages have been sent
[chat.session]
extended = true ; Whether to display the chat history
[lobby]
history = 0 ; Number of past messages to display on join
room = "arena25" ; Default MUC room to join
server = "lobby.wildfiregames.com" ; Address of lobby server
tls = true ; Whether to use TLS encryption when connecting to the server.
verify_certificate = false ; Whether to reject connecting to the lobby if the TLS certificate is invalid (TODO: wait for Gloox GnuTLS trust implementation to be fixed)
terms_url = "https://trac.wildfiregames.com/browser/ps/trunk/binaries/data/mods/public/gui/prelobby/common/terms/"; Allows the user to save the text and print the terms
terms_of_service = "0" ; Version (hash) of the Terms of Service that the user has accepted
terms_of_use = "0" ; Version (hash) of the Terms of Use that the user has accepted
privacy_policy = "0" ; Version (hash) of the Privacy Policy that the user has accepted
xpartamupp = "wfgbot25" ; Name of the server-side XMPP-account that manage games
echelon = "echelon25" ; Name of the server-side XMPP-account that manages ratings
buddies = "," ; Comma separated list of playernames that the current user has marked as buddies
rememberpassword = true ; Whether to store the encrypted password in the user config
[lobby.columns]
gamerating = false ; Show the average rating of the participating players in a column of the gamelist
[lobby.stun]
enabled = true ; The STUN protocol allows hosting games without configuring the firewall and router.
; If STUN is disabled, the game relies on direct connection, UPnP and port forwarding.
server = "lobby.wildfiregames.com" ; Address of the STUN server.
port = 3478 ; Port of the STUN server.
delay = 200 ; Duration in milliseconds that is waited between STUN messages.
; Smaller numbers speed up joins but also become less stable.
[mod]
enabledmods = "mod public"
[modio]
public_key = "RWRcbM/EwV7bucTiQVCcRBhCkYkXmJEO7s4ktyufkB+gW/NxHhOZ38xh" ; Public key corresponding to the private key valid mods are signed with
disclaimer = "0" ; Version (hash) of the Disclaimer that the user has accepted
[modio.v1]
baseurl = "https://api.mod.io/v1"
api_key = "23df258a71711ea6e4b50893acc1ba55"
name_id = "0ad"
[network]
duplicateplayernames = false ; Rename joining player to "User (2)" if "User" is already connected, otherwise prohibit join.
lateobservers = everyone ; Allow observers to join the game after it started. Possible values: everyone, buddies, disabled.
observerlimit = 8 ; Prevent further observer joins in running games if this limit is reached
+observermaxlag = 10 ; Make clients wait for observers if they lag more than X turns behind. -1 means "never wait for observers".
+autocatchup = true ; Auto-accelerate the sim rate if lagging behind (as an observer).
[overlay]
fps = "false" ; Show frames per second in top right corner
realtime = "false" ; Show current system time in top right corner
netwarnings = "true" ; Show warnings if the network connection is bad
[profiler2]
autoenable = false ; Enable HTTP server output at startup (default off for security/performance)
gpu.arb.enable = true ; Allow GL_ARB_timer_query timing mode when available
gpu.ext.enable = true ; Allow GL_EXT_timer_query timing mode when available
gpu.intel.enable = true ; Allow GL_INTEL_performance_queries timing mode when available
[rlinterface]
address = "127.0.0.1:6000"
[sound]
mastergain = 0.9
musicgain = 0.2
ambientgain = 0.6
actiongain = 0.7
uigain = 0.7
[sound.notify]
nick = true ; Play a sound when someone mentions your name in the lobby or game
gamesetup.join = false ; Play a sound when a new client joins the game setup
[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_upload = "https://feedback.wildfiregames.com/report/upload/v1/" ; URL where UserReports are uploaded to
url_publication = "https://feedback.wildfiregames.com/" ; URL where UserReports were analyzed and published
url_terms = "https://trac.wildfiregames.com/browser/ps/trunk/binaries/data/mods/public/gui/userreport/Terms_and_Conditions.txt"; Allows the user to save the text and print the terms
terms = "0" ; Version (hash) of the UserReporter Terms that the user has accepted
[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/options/options.json
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 25155)
+++ ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 25156)
@@ -1,638 +1,652 @@
[
{
"label": "General",
"options":
[
{
"type": "string",
"label": "Player name (single-player)",
"tooltip": "How you want to be addressed in single-player matches.",
"config": "playername.singleplayer"
},
{
"type": "string",
"label": "Player name (multiplayer)",
"tooltip": "How you want to be addressed in multiplayer matches (except lobby).",
"config": "playername.multiplayer"
},
{
"type": "boolean",
"label": "Background pause",
"tooltip": "Pause single-player games when window loses focus.",
"config": "pauseonfocusloss"
},
{
"type": "boolean",
"label": "Enable welcome screen",
"tooltip": "If you disable it, the welcome screen will still appear once, each time a new version is available. You can always launch it from the main menu.",
"config": "gui.splashscreen.enable"
},
{
"type": "boolean",
- "label": "Network warnings",
- "tooltip": "Show which player has a bad connection in multiplayer games.",
- "config": "overlay.netwarnings"
- },
- {
- "type": "boolean",
"label": "FPS overlay",
"tooltip": "Show frames per second in top right corner.",
"config": "overlay.fps"
},
{
"type": "boolean",
"label": "Real time overlay",
"tooltip": "Show current system time in top right corner.",
"config": "overlay.realtime"
},
{
"type": "boolean",
"label": "Game time overlay",
"tooltip": "Show current simulation time in top right corner.",
"config": "gui.session.timeelapsedcounter"
},
{
"type": "boolean",
"label": "Ceasefire time overlay",
"tooltip": "Always show the remaining ceasefire time.",
"config": "gui.session.ceasefirecounter"
},
{
- "type": "dropdown",
- "label": "Late observer joins",
- "tooltip": "Allow everybody or buddies only to join the game as observer after it started.",
- "config": "network.lateobservers",
- "list": [
- { "value": "everyone", "label": "Everyone" },
- { "value": "buddies", "label": "Buddies" },
- { "value": "disabled", "label": "Disabled" }
- ]
- },
- {
- "type": "number",
- "label": "Observer limit",
- "tooltip": "Prevent further observers from joining if the limit is reached.",
- "config": "network.observerlimit",
- "min": 0,
- "max": 32
- },
- {
"type": "boolean",
"label": "Chat timestamp",
"tooltip": "Display the time at which a chat message was posted.",
"config": "chat.timestamp"
},
{
"type": "dropdown",
"label": "Naming of entities.",
"tooltip": "How to show entity names.",
"config": "gui.session.howtoshownames",
"list": [
{
"value": 0,
"label": "Specific primary.",
"tooltip": "Show specific names as primary and generic names as secondary."
},
{
"value": 1,
"label": "Generic primary.",
"tooltip": "Show generic names as primary and specific names as secondary."
},
{
"value": 2,
"label": "Only Specific.",
"tooltip": "Show only specific names for units."
},
{
"value": 3,
"label": "Only Generic.",
"tooltip": "Show only generic names for units."
}
]
}
]
},
{
"label": "Graphics",
"tooltip": "Set the balance between performance and visual appearance.",
"options":
[
{
"type": "boolean",
"label": "Windowed mode",
"tooltip": "Start 0 A.D. in a window.",
"config": "windowed"
},
{
"type": "boolean",
"label": "Prefer GLSL",
"tooltip": "Use OpenGL 2.0 shaders (recommended).",
"config": "preferglsl"
},
{
"type": "boolean",
"label": "Fog",
"tooltip": "Enable fog.",
"dependencies": ["preferglsl"],
"config": "fog"
},
{
"type": "boolean",
"label": "Post-processing",
"tooltip": "Use screen-space post-processing filters (HDR, Bloom, DOF, etc).",
"config": "postproc"
},
{
"type": "dropdown",
"label": "Antialiasing",
"tooltip": "Reduce aliasing effect on edges.",
"dependencies": ["postproc", "preferglsl"],
"config": "antialiasing",
"list": [
{ "value": "disabled", "label": "Disabled", "tooltip": "Do not use antialiasing." },
{ "value": "fxaa", "label": "FXAA", "tooltip": "Fast, but simple antialiasing." },
{ "value": "msaa2", "label": "MSAA (2×)", "tooltip": "Slow, but high-quality antialiasing, uses two samples per pixel. Supported for GL3.3+." },
{ "value": "msaa4", "label": "MSAA (4×)", "tooltip": "Slow, but high-quality antialiasing, uses four samples per pixel. Supported for GL3.3+." },
{ "value": "msaa8", "label": "MSAA (8×)", "tooltip": "Slow, but high-quality antialiasing, uses eight samples per pixel. Supported for GL3.3+." },
{ "value": "msaa16", "label": "MSAA (16×)", "tooltip": "Slow, but high-quality antialiasing, uses sixteen samples per pixel. Supported for GL3.3+." }
]
},
{
"type": "dropdown",
"label": "Sharpening",
"tooltip": "Reduce blurry effects.",
"dependencies": ["postproc", "preferglsl"],
"config": "sharpening",
"list": [
{ "value": "disabled", "label": "Disabled", "tooltip": "Do not use sharpening." },
{ "value": "cas", "label": "FidelityFX CAS", "tooltip": "Contrast adaptive sharpening, a fast, contrast based sharpening pass." }
]
},
{
"type": "slider",
"label": "Sharpness factor",
"tooltip": "The sharpness of the choosen pass.",
"dependencies": ["postproc", "preferglsl"],
"config": "sharpness",
"min": 0,
"max": 1
},
{
"type": "slider",
"label": "Shader effects",
"tooltip": "Number of shader effects. REQUIRES GAME RESTART",
"config": "materialmgr.quality",
"min": 0,
"max": 10
},
{
"type": "boolean",
"label": "Shadows",
"tooltip": "Enable shadows.",
"config": "shadows"
},
{
"type": "dropdown",
"label": "Shadow quality",
"tooltip": "Shadow map resolution. High values can crash the game when using a graphics card with low memory!",
"dependencies": ["shadows"],
"config": "shadowquality",
"list": [
{ "value": -2, "label": "Very Low" },
{ "value": -1, "label": "Low" },
{ "value": 0, "label": "Medium" },
{ "value": 1, "label": "High" },
{ "value": 2, "label": "Very High" }
]
},
{
"type": "boolean",
"label": "Shadow filtering",
"tooltip": "Smooth shadows.",
"dependencies": ["shadows"],
"config": "shadowpcf"
},
{
"type": "boolean",
"label": "Unit silhouettes",
"tooltip": "Show outlines of units behind structures.",
"config": "silhouettes"
},
{
"type": "boolean",
"label": "Particles",
"tooltip": "Enable particles.",
"config": "particles"
},
{
"type": "boolean",
"label": "Water effects",
"tooltip": "When OFF, use the lowest settings possible to render water. This makes other settings irrelevant.",
"config": "watereffects"
},
{
"type": "boolean",
"label": "High-quality water effects",
"tooltip": "Use higher-quality effects for water, rendering coastal waves, shore foam, and ships trails.",
"dependencies": ["watereffects"],
"config": "waterfancyeffects"
},
{
"type": "boolean",
"label": "Water reflections",
"tooltip": "Allow water to reflect a mirror image.",
"dependencies": ["watereffects"],
"config": "waterreflection"
},
{
"type": "boolean",
"label": "Water refraction",
"tooltip": "Use a real water refraction map and not transparency.",
"dependencies": ["watereffects"],
"config": "waterrefraction"
},
{
"type": "boolean",
"label": "Real water depth",
"tooltip": "Use actual water depth in rendering calculations.",
"dependencies": ["watereffects", "waterrefraction"],
"config": "waterrealdepth"
},
{
"type": "boolean",
"label": "Smooth vision",
"tooltip": "Lift darkness and fog of war smoothly.",
"config": "smoothlos"
},
{
"type": "boolean",
"label": "VSync",
"tooltip": "Run vertical sync to fix screen tearing. REQUIRES GAME RESTART",
"config": "vsync"
},
{
"type": "slider",
"label": "FPS throttling in menus",
"tooltip": "To save CPU workload, throttle render frequency in all menus. Set to maximum to disable throttling.",
"config": "adaptivefps.menu",
"min": 20,
"max": 100
},
{
"type": "slider",
"label": "FPS throttling in games",
"tooltip": "To save CPU workload, throttle render frequency in running games. Set to maximum to disable throttling.",
"config": "adaptivefps.session",
"min": 20,
"max": 100
}
]
},
{
"label": "Sound",
"options":
[
{
"type": "slider",
"label": "Master volume",
"tooltip": "Master audio gain.",
"config": "sound.mastergain",
"function": "SetMasterGain",
"min": 0,
"max": 2
},
{
"type": "slider",
"label": "Music volume",
"tooltip": "In game music gain.",
"config": "sound.musicgain",
"function": "SetMusicGain",
"min": 0,
"max": 2
},
{
"type": "slider",
"label": "Ambient volume",
"tooltip": "In game ambient sound gain.",
"config": "sound.ambientgain",
"function": "SetAmbientGain",
"min": 0,
"max": 2
},
{
"type": "slider",
"label": "Action volume",
"tooltip": "In game unit action sound gain.",
"config": "sound.actiongain",
"function": "SetActionGain",
"min": 0,
"max": 2
},
{
"type": "slider",
"label": "UI volume",
"tooltip": "UI sound gain.",
"config": "sound.uigain",
"function": "SetUIGain",
"min": 0,
"max": 2
},
{
"type": "boolean",
"label": "Nick notification",
"tooltip": "Receive audio notification when someone types your nick.",
"config": "sound.notify.nick"
},
{
"type": "boolean",
"label": "New player notification in game setup",
"tooltip": "Receive audio notification when a new client joins the game setup.",
"config": "sound.notify.gamesetup.join"
}
]
},
{
"label": "Game Setup",
"options":
[
{
"type": "boolean",
"label": "Enable game setting tips",
"tooltip": "Show tips when setting up a game.",
"config": "gui.gamesetup.enabletips"
},
{
"type": "boolean",
"label": "Enable settings panel slide",
"tooltip": "Slide the settings panel when opening, closing or resizing.",
"config": "gui.gamesetup.settingsslide"
},
{
"type": "boolean",
"label": "Persist match settings",
"tooltip": "Save and restore match settings for quick reuse when hosting another game.",
"config": "persistmatchsettings"
},
{
"type": "dropdown",
"label": "Default AI difficulty",
"tooltip": "Default difficulty of the AI.",
"config": "gui.gamesetup.aidifficulty",
"list": [
{ "value": 0, "label": "Sandbox" },
{ "value": 1, "label": "Very Easy" },
{ "value": 2, "label": "Easy" },
{ "value": 3, "label": "Medium" },
{ "value": 4, "label": "Hard" },
{ "value": 5, "label": "Very Hard" }
]
},
{
"type": "dropdown",
"label": "Default AI behavior",
"tooltip": "Default behavior of the AI.",
"config": "gui.gamesetup.aibehavior",
"list": [
{ "value": "random", "label": "Random" },
{ "value": "balanced", "label": "Balanced" },
{ "value": "aggressive", "label": "Aggressive" },
{ "value": "defensive", "label": "Defensive" }
]
},
{
"type": "dropdown",
"label": "Assign players",
"tooltip": "Automatically assign joining clients to free player slots during the match setup.",
"config": "gui.gamesetup.assignplayers",
"list": [
{
"value": "everyone",
"label": "Everyone",
"tooltip": "Players joining the match will be assigned if there is a free slot."
},
{
"value": "buddies",
"label": "Buddies",
"tooltip": "Players joining the match will only be assigned if they are a buddy of the host and if there is a free slot."
},
{
"value": "disabled",
"label": "Disabled",
"tooltip": "Players only receive a slot when the host assigns them explicitly."
}
]
}
]
},
{
- "label": "Lobby",
+ "label": "Networking / Lobby",
"tooltip": "These settings only affect the multiplayer.",
"options":
[
{
"type": "boolean",
"label": "TLS encryption",
"tooltip": "Protect login and data exchanged with the lobby server using TLS encryption.",
"config": "lobby.tls"
},
{
"type": "number",
"label": "Chat backlog",
"tooltip": "Number of backlogged messages to load when joining the lobby.",
"config": "lobby.history",
"min": "0"
},
{
"type": "boolean",
"label": "Game rating column",
"tooltip": "Show the average rating of the participating players in a column of the gamelist.",
"config": "lobby.columns.gamerating"
+ },
+ {
+ "type": "boolean",
+ "label": "Network warnings",
+ "tooltip": "Show which player has a bad connection in multiplayer games.",
+ "config": "overlay.netwarnings"
+ },
+ {
+ "type": "dropdown",
+ "label": "Late observer joins",
+ "tooltip": "Allow everybody or buddies only to join the game as observer after it started.",
+ "config": "network.lateobservers",
+ "list": [
+ { "value": "everyone", "label": "Everyone" },
+ { "value": "buddies", "label": "Buddies" },
+ { "value": "disabled", "label": "Disabled" }
+ ]
+ },
+ {
+ "type": "number",
+ "label": "Observer limit",
+ "tooltip": "Prevent further observers from joining if the limit is reached.",
+ "config": "network.observerlimit",
+ "min": 0,
+ "max": 32
+ },
+ {
+ "type": "number",
+ "label": "Max lag for observers",
+ "tooltip": "When hosting, pause the game if observers are lagging more than this many turns. If set to -1, observers are ignored.",
+ "config": "network.observermaxlag",
+ "min": -1,
+ "max": 10000
+ },
+ {
+ "type": "boolean",
+ "label": "(Observer) Speed up when lagging.",
+ "tooltip": "When observing a game, automatically speed up if you start lagging, to catch up with the live match.",
+ "config": "network.autocatchup"
}
]
},
{
"label": "Game Session",
"tooltip": "Change options regarding the in-game settings.",
"options":
[
{
"type": "slider",
"label": "Wounded unit health",
"tooltip": "The wounded unit hotkey considers the selected units as wounded if their health percentage falls below this number.",
"config": "gui.session.woundedunithotkeythreshold",
"min": 0,
"max": 100
},
{
"type": "number",
"label": "Batch training size",
"tooltip": "Number of units trained per batch by default.",
"config": "gui.session.batchtrainingsize",
"min": 1,
"max": 20
},
{
"type": "slider",
"label": "Scroll batch increment ratio",
"tooltip": "Number of times you have to scroll to increase/decrease the batchsize by 1.",
"config": "gui.session.scrollbatchratio",
"min": 0.1,
"max": 30
},
{
"type": "boolean",
"label": "Chat notification attack",
"tooltip": "Show a chat notification if you are attacked by another player.",
"config": "gui.session.notifications.attack"
},
{
"type": "boolean",
"label": "Chat notification tribute",
"tooltip": "Show a chat notification if an ally tributes resources to another team member if teams are locked, and all tributes in observer mode.",
"config": "gui.session.notifications.tribute"
},
{
"type": "boolean",
"label": "Chat notification barter",
"tooltip": "Show a chat notification to observers when a player bartered resources.",
"config": "gui.session.notifications.barter"
},
{
"type": "dropdown",
"label": "Chat notification phase",
"tooltip": "Show a chat notification if you or an ally have started, aborted or completed a new phase, and phases of all players in observer mode.",
"config": "gui.session.notifications.phase",
"list": [
{ "value": "none", "label": "Disable" },
{ "value": "completed", "label": "Completed" },
{ "value": "all", "label": "All displayed" }
]
},
{
"type": "boolean",
"label": "Attack range visualization",
"tooltip": "Display the attack range of selected defensive structures. (It can also be toggled with the hotkey during a game).",
"config": "gui.session.attackrange"
},
{
"type": "boolean",
"label": "Aura range visualization",
"tooltip": "Display the range of auras of selected units and structures. (It can also be toggled with the hotkey during a game).",
"config": "gui.session.aurasrange"
},
{
"type": "boolean",
"label": "Heal range visualization",
"tooltip": "Display the healing range of selected units. (It can also be toggled with the hotkey during a game).",
"config": "gui.session.healrange"
},
{
"type": "boolean",
"label": "Rank icon above status bar",
"tooltip": "Show rank icons above status bars.",
"config": "gui.session.rankabovestatusbar"
},
{
"type": "boolean",
"label": "Experience status bar",
"tooltip": "Show an experience status bar above each selected unit.",
"config": "gui.session.experiencestatusbar"
},
{
"type": "boolean",
"label": "Detailed tooltips",
"tooltip": "Show detailed tooltips for trainable units in unit-producing structures.",
"config": "showdetailedtooltips"
},
{
"type": "dropdown",
"label": "Sort resources and population tooltip",
"tooltip": "Dynamically sort players in the resources and population tooltip by value.",
"config": "gui.session.respoptooltipsort",
"list": [
{ "value": 0, "label": "Unordered" },
{ "value": -1, "label": "Ascending" },
{ "value": 1, "label": "Descending" }
]
},
{
"type": "color",
"label": "Diplomacy colors: self",
"tooltip": "Color of your units when diplomacy colors are enabled.",
"config": "gui.session.diplomacycolors.self"
},
{
"type": "color",
"label": "Diplomacy colors: ally",
"tooltip": "Color of allies when diplomacy colors are enabled.",
"config": "gui.session.diplomacycolors.ally"
},
{
"type": "color",
"label": "Diplomacy colors: neutral",
"tooltip": "Color of neutral players when diplomacy colors are enabled.",
"config": "gui.session.diplomacycolors.neutral"
},
{
"type": "color",
"label": "Diplomacy colors: enemy",
"tooltip": "Color of enemies when diplomacy colors are enabled.",
"config": "gui.session.diplomacycolors.enemy"
},
{
"type": "dropdown",
"label": "Snap to edges",
"tooltip": "This option allows to align new structures with nearby structures.",
"config": "gui.session.snaptoedges",
"list": [
{
"value": "disabled",
"label": "Hotkey to enable snapping",
"tooltip": "New structures are aligned with nearby structures while pressing the hotkey."
},
{
"value": "enabled",
"label": "Hotkey to disable snapping",
"tooltip": "New structures are aligned with nearby structures unless the hotkey is pressed."
}
]
},
{
"type": "dropdown",
"label": "Control group membership",
"tooltip": "Decide whether units can be part of multiple control groups.",
"config": "gui.session.disjointcontrolgroups",
"list": [
{
"value": "true",
"label": "Single",
"tooltip": "When adding a Unit or Structure to a control group, they are removed from other control groups. Use this choice if you want control groups to refer to distinct armies."
},
{
"value": "false",
"label": "Multiple",
"tooltip": "Units and Structures can be part of multiple control groups. This is useful to keep control groups for distinct armies and a control group for the entire army simultaneously."
}
]
},
{
"type": "dropdown",
"label": "Formation control",
"tooltip": "Decide whether formations are enabled for all orders or only 'Walk' and 'Patrol'.",
"config": "gui.session.formationwalkonly",
"list": [
{
"value": "true",
"label": "Walk/Patrol Only",
"tooltip": "Other orders will disband existing formations."
},
{
"value": "false",
"label": "No Override",
"tooltip": "Units in formations stay in formations."
}
]
}
]
}
]
Index: ps/trunk/binaries/data/mods/public/gui/session/NetworkDelayOverlay.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/NetworkDelayOverlay.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/NetworkDelayOverlay.js (revision 25156)
@@ -0,0 +1,63 @@
+/**
+ * Shows an overlay if the game is lagging behind the net server.
+ */
+class NetworkDelayOverlay
+{
+ constructor()
+ {
+ this.netDelayOverlay = Engine.GetGUIObjectByName("netDelayOverlay");
+
+ this.netDelayOverlay.caption="toto";
+ this.caption = translate(this.Caption);
+ this.sprintfData = {};
+
+ this.initialSimRate = Engine.GetSimRate();
+ this.currentSimRate = this.initialSimRate;
+
+ setTimeout(() => this.CheckDelay(), 1000);
+ }
+
+ CheckDelay()
+ {
+ setTimeout(() => this.CheckDelay(), 1000);
+ let delay = +(Engine.HasNetClient() && Engine.GetPendingTurns());
+
+ if (g_IsObserver && Engine.ConfigDB_GetValue("user", "network.autocatchup"))
+ {
+ if (delay > this.MAX_LIVE_DELAY && this.currentSimRate <= this.initialSimRate)
+ {
+ this.currentSimRate = this.initialSimRate * 1.1;
+ Engine.SetSimRate(this.currentSimRate);
+ }
+ else if (delay <= this.NORMAL_DELAY && this.currentSimRate > this.initialSimRate)
+ {
+ this.currentSimRate = this.initialSimRate;
+ Engine.SetSimRate(this.currentSimRate);
+ }
+ }
+
+ if (delay < this.MAX_LIVE_DELAY)
+ {
+ this.netDelayOverlay.hidden = true;
+ return;
+ }
+ this.netDelayOverlay.hidden = false;
+ this.sprintfData.delay = (delay / this.TURNS_PER_SECOND);
+ this.sprintfData.delay = this.sprintfData.delay.toFixed(this.sprintfData.delay < 5 ? 1 : 0);
+ this.netDelayOverlay.caption = sprintf(this.caption, this.sprintfData);
+ }
+}
+
+/**
+ * Because of command delay, we can still be several turns behind the 'ready' turn and not
+ * particularly late. This should be kept in sync with the command delay.
+ */
+NetworkDelayOverlay.prototype.NORMAL_DELAY = 3;
+NetworkDelayOverlay.prototype.MAX_LIVE_DELAY = 6;
+
+/**
+ * This needs to be kept in sync with the turn length.
+ */
+NetworkDelayOverlay.prototype.TURNS_PER_SECOND = 5;
+
+NetworkDelayOverlay.prototype.Caption = translate("Delay to live stream: %(delay)ss");
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/NetworkDelayOverlay.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/NetworkDelayOverlay.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/NetworkDelayOverlay.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/NetworkDelayOverlay.xml (revision 25156)
@@ -0,0 +1,14 @@
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/NetworkDelayOverlay.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/session.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 25155)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 25156)
@@ -1,842 +1,844 @@
const g_IsReplay = Engine.IsVisualReplay();
const g_CivData = loadCivData(false, true);
const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
const g_WorldPopulationCapacities = prepareForDropdown(g_Settings && g_Settings.WorldPopulationCapacities);
const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources);
const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions;
var g_Ambient;
var g_AutoFormation;
var g_Chat;
var g_Cheats;
var g_DeveloperOverlay;
var g_DiplomacyColors;
var g_DiplomacyDialog;
var g_GameSpeedControl;
var g_Menu;
var g_MiniMapPanel;
var g_NetworkStatusOverlay;
+var g_NetworkDelayOverlay;
var g_ObjectivesDialog;
var g_OutOfSyncNetwork;
var g_OutOfSyncReplay;
var g_PanelEntityManager;
var g_PauseControl;
var g_PauseOverlay;
var g_PlayerViewControl;
var g_QuitConfirmationDefeat;
var g_QuitConfirmationReplay;
var g_RangeOverlayManager;
var g_ResearchProgress;
var g_TimeNotificationOverlay;
var g_TopPanel;
var g_TradeDialog;
/**
* Map, player and match settings set in game setup.
*/
const g_InitAttributes = deepfreeze(Engine.GuiInterfaceCall("GetInitAttributes"));
/**
* True if this is a multiplayer game.
*/
const g_IsNetworked = Engine.HasNetClient();
/**
* Is this user in control of game settings (i.e. is a network server, or offline player).
*/
var g_IsController = !g_IsNetworked || Engine.IsNetController();
/**
* Whether we have finished the synchronization and
* can start showing simulation related message boxes.
*/
var g_IsNetworkedActive = 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;
/**
* True if the current user has rejoined (or joined the game after it started).
*/
var g_HasRejoined = false;
/**
* The playerID selected in the change perspective tool.
*/
var g_ViewedPlayer = Engine.GetPlayerID();
/**
* True if the camera should focus on attacks and player commands
* and select the affected units.
*/
var g_FollowPlayer = false;
/**
* Cache the basic player data (name, civ, color).
*/
var g_Players = [];
/**
* Last time when onTick was called().
* Used for animating the main menu.
*/
var g_LastTickTime = Date.now();
/**
* Recalculate which units have their status bars shown with this frequency in milliseconds.
*/
var g_StatusBarUpdate = 200;
/**
* For restoring selection, order and filters when returning to the replay menu
*/
var g_ReplaySelectionData;
/**
* Remembers which clients are assigned to which player slots.
* The keys are GUIDs or "local" in single-player.
*/
var g_PlayerAssignments;
/**
* Whether the entire UI should be hidden (useful for promotional screenshots).
* Can be toggled with a hotkey.
*/
var g_ShowGUI = true;
/**
* Whether status bars should be shown for all of the player's units.
*/
var g_ShowAllStatusBars = false;
/**
* Cache of simulation state and template data (apart from TechnologyData, updated on every simulation update).
*/
var g_SimState;
var g_EntityStates = {};
var g_TemplateData = {};
var g_TechnologyData = {};
var g_ResourceData = new Resources();
/**
* These handlers are called each time a new turn was simulated.
* Use this as sparely as possible.
*/
var g_SimulationUpdateHandlers = new Set();
/**
* These handlers are called after the player states have been initialized.
*/
var g_PlayersInitHandlers = new Set();
/**
* These handlers are called when a player has been defeated or won the game.
*/
var g_PlayerFinishedHandlers = new Set();
/**
* These events are fired whenever the player added or removed entities from the selection.
*/
var g_EntitySelectionChangeHandlers = new Set();
/**
* These events are fired when the user has performed a hotkey assignment change.
* Currently only fired on init, but to be fired from any hotkey editor dialog.
*/
var g_HotkeyChangeHandlers = new Set();
/**
* List of additional entities to highlight.
*/
var g_ShowGuarding = false;
var g_ShowGuarded = false;
var g_AdditionalHighlight = [];
/**
* Order in which the panel entities are shown.
*/
var g_PanelEntityOrder = ["Hero", "Relic"];
/**
* Unit classes to be checked for the idle-worker-hotkey.
*/
var g_WorkerTypes = ["FemaleCitizen", "Trader", "FishingBoat", "Citizen"];
/**
* Unit classes to be checked for the military-only-selection modifier and for the idle-warrior-hotkey.
*/
var g_MilitaryTypes = ["Melee", "Ranged"];
function GetSimState()
{
if (!g_SimState)
g_SimState = deepfreeze(Engine.GuiInterfaceCall("GetSimulationState"));
return g_SimState;
}
function GetMultipleEntityStates(ents)
{
if (!ents.length)
return null;
let entityStates = Engine.GuiInterfaceCall("GetMultipleEntityStates", ents);
for (let item of entityStates)
g_EntityStates[item.entId] = item.state && deepfreeze(item.state);
return entityStates;
}
function GetEntityState(entId)
{
if (!g_EntityStates[entId])
{
let entityState = Engine.GuiInterfaceCall("GetEntityState", entId);
g_EntityStates[entId] = entityState && deepfreeze(entityState);
}
return g_EntityStates[entId];
}
/**
* Returns template data calling GetTemplateData defined in GuiInterface.js
* and deepfreezing returned object.
* @param {string} templateName - Data of this template will be returned.
* @param {number|undefined} player - Modifications of this player will be applied to the template.
* If undefined, id of player calling this method will be used.
*/
function GetTemplateData(templateName, player)
{
if (!(templateName in g_TemplateData))
{
let template = Engine.GuiInterfaceCall("GetTemplateData", { "templateName": templateName, "player": player });
translateObjectKeys(template, ["specific", "generic", "tooltip"]);
g_TemplateData[templateName] = deepfreeze(template);
}
return g_TemplateData[templateName];
}
function GetTechnologyData(technologyName, civ)
{
if (!g_TechnologyData[civ])
g_TechnologyData[civ] = {};
if (!(technologyName in g_TechnologyData[civ]))
{
let template = GetTechnologyDataHelper(TechnologyTemplates.Get(technologyName), civ, g_ResourceData);
translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]);
g_TechnologyData[civ][technologyName] = deepfreeze(template);
}
return g_TechnologyData[civ][technologyName];
}
function init(initData, hotloadData)
{
if (!g_Settings)
{
Engine.EndGame();
Engine.SwitchGuiPage("page_pregame.xml");
return;
}
// Fallback used by atlas
g_PlayerAssignments = initData ? initData.playerAssignments : { "local": { "player": 1 } };
// Fallback used by atlas and autostart games
if (g_PlayerAssignments.local && !g_PlayerAssignments.local.name)
g_PlayerAssignments.local.name = singleplayerName();
if (initData)
{
g_ReplaySelectionData = initData.replaySelectionData;
g_HasRejoined = initData.isRejoining;
if (initData.savedGUIData)
restoreSavedGameData(initData.savedGUIData);
}
if (g_InitAttributes.campaignData)
g_CampaignSession = new CampaignSession(g_InitAttributes.campaignData);
let mapCache = new MapCache();
g_Cheats = new Cheats();
g_DiplomacyColors = new DiplomacyColors();
g_PlayerViewControl = new PlayerViewControl();
g_PlayerViewControl.registerViewedPlayerChangeHandler(g_DiplomacyColors.updateDisplayedPlayerColors.bind(g_DiplomacyColors));
g_DiplomacyColors.registerDiplomacyColorsChangeHandler(g_PlayerViewControl.rebuild.bind(g_PlayerViewControl));
g_DiplomacyColors.registerDiplomacyColorsChangeHandler(updateGUIObjects);
g_PauseControl = new PauseControl();
g_PlayerViewControl.registerPreViewedPlayerChangeHandler(removeStatusBarDisplay);
g_PlayerViewControl.registerViewedPlayerChangeHandler(resetTemplates);
g_Ambient = new Ambient();
g_AutoFormation = new AutoFormation();
g_Chat = new Chat(g_PlayerViewControl, g_Cheats);
g_DeveloperOverlay = new DeveloperOverlay(g_PlayerViewControl, g_Selection);
g_DiplomacyDialog = new DiplomacyDialog(g_PlayerViewControl, g_DiplomacyColors);
g_GameSpeedControl = new GameSpeedControl(g_PlayerViewControl);
g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat);
g_MiniMapPanel = new MiniMapPanel(g_PlayerViewControl, g_DiplomacyColors, g_WorkerTypes);
g_NetworkStatusOverlay = new NetworkStatusOverlay();
+ g_NetworkDelayOverlay = new NetworkDelayOverlay();
g_ObjectivesDialog = new ObjectivesDialog(g_PlayerViewControl, mapCache);
g_OutOfSyncNetwork = new OutOfSyncNetwork();
g_OutOfSyncReplay = new OutOfSyncReplay();
g_PanelEntityManager = new PanelEntityManager(g_PlayerViewControl, g_Selection, g_PanelEntityOrder);
g_PauseOverlay = new PauseOverlay(g_PauseControl);
g_QuitConfirmationDefeat = new QuitConfirmationDefeat();
g_QuitConfirmationReplay = new QuitConfirmationReplay();
g_RangeOverlayManager = new RangeOverlayManager(g_Selection);
g_ResearchProgress = new ResearchProgress(g_PlayerViewControl, g_Selection);
g_TradeDialog = new TradeDialog(g_PlayerViewControl);
g_TopPanel = new TopPanel(g_PlayerViewControl, g_DiplomacyDialog, g_TradeDialog, g_ObjectivesDialog, g_GameSpeedControl);
g_TimeNotificationOverlay = new TimeNotificationOverlay(g_PlayerViewControl);
initBatchTrain();
initDisplayedNames();
initSelectionPanels();
LoadModificationTemplates();
updatePlayerData();
initializeMusic(); // before changing the perspective
Engine.SetBoundingBoxDebugOverlay(false);
for (let handler of g_PlayersInitHandlers)
handler();
for (let handler of g_HotkeyChangeHandlers)
handler();
if (hotloadData)
{
g_Selection.selected = hotloadData.selection;
g_PlayerAssignments = hotloadData.playerAssignments;
g_Players = hotloadData.player;
}
// TODO: use event instead
onSimulationUpdate();
setTimeout(displayGamestateNotifications, 1000);
}
function registerPlayersInitHandler(handler)
{
g_PlayersInitHandlers.add(handler);
}
function registerPlayersFinishedHandler(handler)
{
g_PlayerFinishedHandlers.add(handler);
}
function registerSimulationUpdateHandler(handler)
{
g_SimulationUpdateHandlers.add(handler);
}
function unregisterSimulationUpdateHandler(handler)
{
g_SimulationUpdateHandlers.delete(handler);
}
function registerEntitySelectionChangeHandler(handler)
{
g_EntitySelectionChangeHandlers.add(handler);
}
function unregisterEntitySelectionChangeHandler(handler)
{
g_EntitySelectionChangeHandlers.delete(handler);
}
function registerHotkeyChangeHandler(handler)
{
g_HotkeyChangeHandlers.add(handler);
}
function updatePlayerData()
{
let simState = GetSimState();
if (!simState)
return;
let playerData = [];
for (let i = 0; i < simState.players.length; ++i)
{
let playerState = simState.players[i];
playerData.push({
"name": playerState.name,
"civ": playerState.civ,
"color": {
"r": playerState.color.r * 255,
"g": playerState.color.g * 255,
"b": playerState.color.b * 255,
"a": playerState.color.a * 255
},
"team": playerState.team,
"teamsLocked": playerState.teamsLocked,
"cheatsEnabled": playerState.cheatsEnabled,
"state": playerState.state,
"isAlly": playerState.isAlly,
"isMutualAlly": playerState.isMutualAlly,
"isNeutral": playerState.isNeutral,
"isEnemy": playerState.isEnemy,
"guid": undefined, // network guid for players controlled by hosts
"offline": g_Players[i] && !!g_Players[i].offline
});
}
for (let guid in g_PlayerAssignments)
{
let playerID = g_PlayerAssignments[guid].player;
if (!playerData[playerID])
continue;
playerData[playerID].guid = guid;
playerData[playerID].name = g_PlayerAssignments[guid].name;
}
g_Players = playerData;
}
/**
* @param {number} ent - The entity to get its ID for.
* @return {number} - The entity ID of the entity or of its garrisonHolder.
*/
function getEntityOrHolder(ent)
{
let entState = GetEntityState(ent);
if (entState && !entState.position && entState.garrisonable && entState.garrisonable.holder != INVALID_ENTITY)
return getEntityOrHolder(entState.garrisonable.holder);
return ent;
}
function initializeMusic()
{
initMusic();
if (g_ViewedPlayer != -1 && g_CivData[g_Players[g_ViewedPlayer].civ].Music)
global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music);
global.music.setState(global.music.states.PEACE);
}
function resetTemplates()
{
// Update GUI and clear player-dependent cache
g_TemplateData = {};
Engine.GuiInterfaceCall("ResetTemplateModified");
// TODO: do this more selectively
onSimulationUpdate();
}
/**
* Returns true if the player with that ID is in observermode.
*/
function isPlayerObserver(playerID)
{
let playerStates = GetSimState().players;
return !playerStates[playerID] || playerStates[playerID].state != "active";
}
/**
* Returns true if the current user can issue commands for that player.
*/
function controlsPlayer(playerID)
{
let playerStates = GetSimState().players;
return !!playerStates[Engine.GetPlayerID()] &&
playerStates[Engine.GetPlayerID()].controlsAll ||
Engine.GetPlayerID() == playerID &&
!!playerStates[playerID] &&
playerStates[playerID].state != "defeated";
}
/**
* Called when one or more players have won or were defeated.
*
* @param {array} - IDs of the players who have won or were defeated.
* @param {Object} - a plural string stating the victory reason.
* @param {boolean} - whether these players have won or lost.
*/
function playersFinished(players, victoryString, won)
{
addChatMessage({
"type": "playerstate",
"message": victoryString,
"players": players
});
updatePlayerData();
// TODO: The other calls in this function should move too
for (let handler of g_PlayerFinishedHandlers)
handler(players, won);
if (players.indexOf(Engine.GetPlayerID()) == -1 || Engine.IsAtlasRunning())
return;
global.music.setState(
won ?
global.music.states.VICTORY :
global.music.states.DEFEAT
);
}
function resumeGame()
{
g_PauseControl.implicitResume();
}
function closeOpenDialogs()
{
g_Menu.close();
g_Chat.closePage();
g_DiplomacyDialog.close();
g_ObjectivesDialog.close();
g_TradeDialog.close();
}
function endGame()
{
// Before ending the game
let replayDirectory = Engine.GetCurrentReplayDirectory();
let simData = Engine.GuiInterfaceCall("GetReplayMetadata");
let playerID = Engine.GetPlayerID();
Engine.EndGame();
// After the replay file was closed in EndGame
// Done here to keep EndGame small
if (!g_IsReplay)
Engine.AddReplayToCache(replayDirectory);
if (g_IsController && Engine.HasXmppClient())
Engine.SendUnregisterGame();
let summaryData = {
"sim": simData,
"gui": {
"dialog": false,
"assignedPlayer": playerID,
"disconnected": g_Disconnected,
"isReplay": g_IsReplay,
"replayDirectory": !g_HasRejoined && replayDirectory,
"replaySelectionData": g_ReplaySelectionData
}
};
if (g_InitAttributes.campaignData)
{
let menu = g_CampaignSession.getMenu();
if (g_InitAttributes.campaignData.skipSummary)
{
Engine.SwitchGuiPage(menu);
return;
}
summaryData.campaignData = { "filename": g_InitAttributes.campaignData.run };
summaryData.nextPage = menu;
}
Engine.SwitchGuiPage("page_summary.xml", summaryData);
}
// Return some data that we'll use when hotloading this file after changes
function getHotloadData()
{
return {
"selection": g_Selection.selected,
"playerAssignments": g_PlayerAssignments,
"player": g_Players,
};
}
function getSavedGameData()
{
return {
"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 = Date.now();
let tickLength = now - g_LastTickTime;
g_LastTickTime = now;
handleNetMessages();
updateCursorAndTooltip();
if (g_Selection.dirty)
{
g_Selection.dirty = false;
// When selection changed, get the entityStates of new entities
GetMultipleEntityStates(g_Selection.toList().filter(entId => !g_EntityStates[entId]));
for (let handler of g_EntitySelectionChangeHandlers)
handler();
updateGUIObjects();
// Display rally points for selected structures.
if (Engine.GetPlayerID() != -1)
Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() });
}
else if (g_ShowAllStatusBars && now % g_StatusBarUpdate <= tickLength)
recalculateStatusBarDisplay();
updateTimers();
Engine.GuiInterfaceCall("ClearRenamedEntities");
}
function onSimulationUpdate()
{
// Templates change depending on technologies and auras, so they have to be reloaded after such a change.
// g_TechnologyData data never changes, so it shouldn't be deleted.
g_EntityStates = {};
if (Engine.GuiInterfaceCall("IsTemplateModified"))
{
g_TemplateData = {};
Engine.GuiInterfaceCall("ResetTemplateModified");
}
g_SimState = undefined;
// Some changes may require re-rendering the selection.
if (Engine.GuiInterfaceCall("IsSelectionDirty"))
{
g_Selection.onChange();
Engine.GuiInterfaceCall("ResetSelectionDirty");
}
if (!GetSimState())
return;
GetMultipleEntityStates(g_Selection.toList());
for (let handler of g_SimulationUpdateHandlers)
handler();
// TODO: Move to handlers
updateCinemaPath();
handleNotifications();
updateGUIObjects();
}
function toggleGUI()
{
g_ShowGUI = !g_ShowGUI;
updateCinemaPath();
}
function updateCinemaPath()
{
let isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected;
Engine.GetGUIObjectByName("session").hidden = !g_ShowGUI || isPlayingCinemaPath;
Engine.ConfigDB_CreateValue("user", "silhouettes", !isPlayingCinemaPath && Engine.ConfigDB_GetValue("user", "silhouettes") == "true" ? "true" : "false");
}
// TODO: Use event subscription onSimulationUpdate, onEntitySelectionChange, onPlayerViewChange, ... instead
function updateGUIObjects()
{
g_Selection.update();
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay();
if (g_ShowGuarding || g_ShowGuarded)
updateAdditionalHighlight();
updateGroups();
updateSelectionDetails();
updateBuildingPlacementPreview();
if (!g_IsObserver)
{
// 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 updateGroups()
{
g_Groups.update();
// Determine the sum of the costs of a given template
let getCostSum = (ent) => {
let cost = GetTemplateData(GetEntityState(ent).template).cost;
return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0;
};
for (let i in Engine.GetGUIObjectByName("unitGroupPanel").children)
{
Engine.GetGUIObjectByName("unitGroupLabel[" + i + "]").caption = i;
let button = Engine.GetGUIObjectByName("unitGroupButton[" + 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);
// Choose the icon of the most common template (or the most costly if it's not unique)
if (g_Groups.groups[i].getTotalCount() > 0)
{
let icon = GetTemplateData(GetEntityState(g_Groups.groups[i].getEntsGrouped().reduce((pre, cur) => {
if (pre.ents.length == cur.ents.length)
return getCostSum(pre.ents[0]) > getCostSum(cur.ents[0]) ? pre : cur;
return pre.ents.length > cur.ents.length ? pre : cur;
}).ents[0]).template).icon;
Engine.GetGUIObjectByName("unitGroupIcon[" + i + "]").sprite =
icon ? ("stretched:session/portraits/" + icon) : "groupsIcon";
}
setPanelObjectPosition(button, i, 1);
}
}
/**
* Toggles the display of status bars for all of the player's entities.
*
* @param {boolean} remove - Whether to hide all previously shown status bars.
*/
function recalculateStatusBarDisplay(remove = false)
{
let entities;
if (g_ShowAllStatusBars && !remove)
entities = g_ViewedPlayer == -1 ?
Engine.PickNonGaiaEntitiesOnScreen() :
Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer);
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(
g_ViewedPlayer == -1 ? "GetNonGaiaEntities" : "GetPlayerEntities", {
"viewedPlayer": g_ViewedPlayer
}).filter(idx => selected.indexOf(idx) == -1);
}
Engine.GuiInterfaceCall("SetStatusBars", {
"entities": entities,
"enabled": g_ShowAllStatusBars && !remove,
"showRank": Engine.ConfigDB_GetValue("user", "gui.session.rankabovestatusbar") == "true",
"showExperience": Engine.ConfigDB_GetValue("user", "gui.session.experiencestatusbar") == "true"
});
}
function removeStatusBarDisplay()
{
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay(true);
}
/**
* Updates the primary/secondary names in the simulation and GUI.
*/
function updateDisplayedNames()
{
g_SpecificNamesPrimary = Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 0 || Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 2;
g_ShowSecondaryNames = Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 0 || Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 1;
}
/**
* Inverts the given configuration boolean and returns the current state.
* For example "silhouettes".
*/
function toggleConfigBool(configName)
{
let enabled = Engine.ConfigDB_GetValue("user", configName) != "true";
Engine.ConfigDB_CreateAndWriteValueToFile("user", configName, String(enabled), "config/user.cfg");
return enabled;
}
// 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;
}
Index: ps/trunk/binaries/data/mods/public/gui/session/session.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 25155)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 25156)
@@ -1,119 +1,120 @@
Index: ps/trunk/source/network/NetServer.cpp
===================================================================
--- ps/trunk/source/network/NetServer.cpp (revision 25155)
+++ ps/trunk/source/network/NetServer.cpp (revision 25156)
@@ -1,1719 +1,1725 @@
/* Copyright (C) 2021 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 "NetServerTurnManager.h"
#include "NetStats.h"
#include "lib/external_libraries/enet.h"
#include "lib/types.h"
#include "network/StunClient.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/GUID.h"
#include "ps/Profile.h"
#include "ps/Threading.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
#include "simulation2/system/TurnManager.h"
#if CONFIG2_MINIUPNPC
#include
#include
#include
#include
#endif
#include
/**
* Number of peers to allocate for the enet host.
* Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
*
* At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason.
*/
#define MAX_CLIENTS 41
#define DEFAULT_SERVER_NAME L"Unnamed Server"
constexpr int CHANNEL_COUNT = 1;
constexpr int FAILED_PASSWORD_TRIES_BEFORE_BAN = 3;
/**
* 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;
/**
* Once ping goes above turn length * command delay,
* the game will start 'freezing' for other clients while we catch up.
* Since commands are sent client -> server -> client, divide by 2.
* (duplicated in NetServer.cpp to avoid having to fetch the constants in a header file)
*/
constexpr u32 NETWORK_BAD_PING = DEFAULT_TURN_LENGTH * COMMAND_DELAY_MP / 2;
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 (CNetServerSession* serverSession : m_Server.m_Sessions)
{
if (serverSession->GetHostID() == m_RejoinerHostID)
{
session = serverSession;
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;
// Send the init attributes alongside - these should be correct since the game should be started.
CJoinSyncStartMessage message;
message.m_InitAttributes = m_Server.GetScriptInterface().StringifyJSON(&m_Server.m_InitAttributes);
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(bool useLobbyAuth, int autostartPlayers) :
m_AutostartPlayers(autostartPlayers),
m_LobbyAuth(useLobbyAuth),
m_Shutdown(false),
m_ScriptInterface(NULL),
m_NextHostID(1), m_Host(NULL), m_ControllerGUID(), m_Stats(NULL),
m_LastConnectionCheck(0)
{
m_State = SERVER_STATE_UNCONNECTED;
m_ServerTurnManager = NULL;
m_ServerName = DEFAULT_SERVER_NAME;
}
CNetServerWorker::~CNetServerWorker()
{
if (m_State != SERVER_STATE_UNCONNECTED)
{
// Tell the thread to shut down
{
std::lock_guard lock(m_WorkerMutex);
m_Shutdown = true;
}
// Wait for it to shut down cleanly
m_WorkerThread.join();
}
#if CONFIG2_MINIUPNPC
if (m_UPnPThread.joinable())
m_UPnPThread.detach();
#endif
// Clean up resources
delete m_Stats;
for (CNetServerSession* session : m_Sessions)
{
session->DisconnectNow(NDR_SERVER_SHUTDOWN);
delete session;
}
if (m_Host)
enet_host_destroy(m_Host);
delete m_ServerTurnManager;
}
void CNetServerWorker::SetPassword(const CStr& hashedPassword)
{
m_Password = hashedPassword;
}
void CNetServerWorker::SetControllerSecret(const std::string& secret)
{
m_ControllerSecret = secret;
}
bool CNetServerWorker::SetupConnection(const u16 port)
{
ENSURE(m_State == SERVER_STATE_UNCONNECTED);
ENSURE(!m_Host);
// Bind to default host
ENetAddress addr;
addr.host = ENET_HOST_ANY;
addr.port = 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
m_WorkerThread = std::thread(Threading::HandleExceptions::Wrapper, this);
#if CONFIG2_MINIUPNPC
// Launch the UPnP thread
m_UPnPThread = std::thread(Threading::HandleExceptions::Wrapper);
#endif
return true;
}
#if CONFIG2_MINIUPNPC
void CNetServerWorker::SetupUPnP()
{
debug_SetThreadName("UPnP");
// 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.
bool allocatedUrls = false;
struct UPNPUrls urls;
struct IGDdatas data;
struct UPNPDev* devlist = NULL;
// Make sure everything is properly freed.
std::function freeUPnP = [&allocatedUrls, &urls, &devlist]()
{
if (allocatedUrls)
FreeUPNPUrls(&urls);
freeUPNPDevlist(devlist);
// IGDdatas does not need to be freed according to UPNP_GetIGDFromUrl
};
// 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;
// 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.");
freeUPnP();
return;
}
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));
freeUPnP();
return;
}
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));
freeUPnP();
return;
}
// 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));
freeUPnP();
return;
}
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.WriteValueToFile(CFG_USER, "network.upnprootdescurl", urls.controlURL);
LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls.controlURL);
freeUPnP();
}
#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, const std::vector& targetStates)
{
ENSURE(m_Host);
bool ok = true;
// 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
for (CNetServerSession* session : m_Sessions)
if (std::find(targetStates.begin(), targetStates.end(), static_cast(session->GetCurrState())) != targetStates.end() &&
!session->SendMessage(message))
ok = false;
return ok;
}
void CNetServerWorker::RunThread(CNetServerWorker* data)
{
debug_SetThreadName("NetServer");
data->Run();
}
void CNetServerWorker::Run()
{
// The script context uses the profiler and therefore the thread must be registered before the context is created
g_Profiler2.RegisterCurrentThread("Net server");
// We create a new ScriptContext for this network thread, with a single ScriptInterface.
shared_ptr netServerContext = ScriptContext::CreateContext();
m_ScriptInterface = new ScriptInterface("Engine", "Net server", netServerContext);
m_InitAttributes.init(m_ScriptInterface->GetGeneralJSContext(), JS::UndefinedValue());
while (true)
{
if (!RunStep())
break;
// Implement autostart mode
if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers)
StartGame(m_ScriptInterface->StringifyJSON(&m_InitAttributes));
// Update profiler stats
m_Stats->LatchHostState(m_Host);
}
// Clear roots before deleting their context
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->GetContext()->MaybeIncrementalGC(0.5f);
ScriptRequest rq(m_ScriptInterface);
std::vector newStartGame;
std::vector newGameAttributes;
std::vector> newLobbyAuths;
std::vector newTurnLength;
{
std::lock_guard lock(m_WorkerMutex);
if (m_Shutdown)
return false;
newStartGame.swap(m_StartGameQueue);
newGameAttributes.swap(m_InitAttributesQueue);
newLobbyAuths.swap(m_LobbyAuthQueue);
newTurnLength.swap(m_TurnLengthQueue);
}
if (!newGameAttributes.empty())
{
if (m_State != SERVER_STATE_UNCONNECTED && m_State != SERVER_STATE_PREGAME)
LOGERROR("NetServer: Init Attributes cannot be changed after the server starts loading.");
else
{
JS::RootedValue gameAttributesVal(rq.cx);
GetScriptInterface().ParseJSON(newGameAttributes.back(), &gameAttributesVal);
m_InitAttributes = gameAttributesVal;
}
}
if (!newTurnLength.empty())
SetTurnLength(newTurnLength.back());
while (!newLobbyAuths.empty())
{
const std::pair& auth = newLobbyAuths.back();
ProcessLobbyAuth(auth.first, auth.second);
newLobbyAuths.pop_back();
}
// Perform file transfers
for (CNetServerSession* session : m_Sessions)
session->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;
}
if (m_State == SERVER_STATE_LOADING)
CheckGameLoadStatus(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()
{
// 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 > NETWORK_BAD_PING)
{
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).
// Also send it to clients that finished the loading screen while
// the game is still waiting for other clients to finish the loading screen.
if (message)
for (size_t j = 0; j < m_Sessions.size(); ++j)
{
if (i != j && (
(m_Sessions[j]->GetCurrState() == NSS_PREGAME && m_State == SERVER_STATE_PREGAME) ||
m_Sessions[j]->GetCurrState() == NSS_INGAME))
{
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
if (!session->Update(message->GetType(), (void*)message))
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_LOBBY_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, 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_CLEAR_ALL_READY, NSS_PREGAME, (void*)&OnClearAllReady, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_SETUP, NSS_PREGAME, (void*)&OnGameSetup, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, (void*)&OnAssignPlayer, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, (void*)&OnKickPlayer, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, (void*)&OnGameStart, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, (void*)&OnKickPlayer, 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_KICKED, NSS_INGAME, (void*)&OnKickPlayer, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CLIENT_PAUSED, NSS_INGAME, (void*)&OnClientPaused, 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*)&OnSimulationCommand, context);
session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnSyncCheck, context);
session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnEndCommandBatch, 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());
CPlayerAssignmentMessage assignMessage;
ConstructPlayerAssignmentMessage(assignMessage);
session->SendMessage(&assignMessage);
}
void CNetServerWorker::OnUserLeave(CNetServerSession* session)
{
std::vector::iterator pausing = std::find(m_PausingPlayers.begin(), m_PausingPlayers.end(), session->GetGUID());
if (pausing != m_PausingPlayers.end())
m_PausingPlayers.erase(pausing);
RemovePlayer(session->GetGUID());
if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING)
- m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers
+ m_ServerTurnManager->UninitialiseClient(session->GetHostID());
// 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 (const std::pair& p : m_PlayerAssignments)
if (p.second.m_Enabled && p.second.m_PlayerID != -1)
usedIDs.insert(p.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::ClearAllPlayerReady()
{
for (std::pair& p : m_PlayerAssignments)
if (p.second.m_Status != 2)
p.second.m_Status = 0;
SendPlayerAssignments();
}
void 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_ControllerGUID)
return;
if (ban)
{
// Remember name
if (std::find(m_BannedPlayers.begin(), m_BannedPlayers.end(), playerName) == m_BannedPlayers.end())
m_BannedPlayers.push_back(m_LobbyAuth ? CStrW(playerName.substr(0, playerName.find(L" ("))) : playerName);
// Remember IP address
u32 ipAddress = (*it)->GetIPAddress();
if (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, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
}
void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid)
{
// Remove anyone who's already assigned to this player
for (std::pair& p : m_PlayerAssignments)
{
if (p.second.m_PlayerID == playerID)
p.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 (const std::pair& p : m_PlayerAssignments)
{
if (!p.second.m_Enabled)
continue;
CPlayerAssignmentMessage::S_m_Hosts h;
h.m_GUID = p.first;
h.m_Name = p.second.m_Name;
h.m_PlayerID = p.second.m_PlayerID;
h.m_Status = p.second.m_Status;
message.m_Hosts.push_back(h);
}
}
void CNetServerWorker::SendPlayerAssignments()
{
CPlayerAssignmentMessage message;
ConstructPlayerAssignmentMessage(message);
Broadcast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
}
const ScriptInterface& CNetServerWorker::GetScriptInterface()
{
return *m_ScriptInterface;
}
void CNetServerWorker::SetTurnLength(u32 msecs)
{
if (m_ServerTurnManager)
m_ServerTurnManager->SetTurnLength(msecs);
}
void CNetServerWorker::ProcessLobbyAuth(const CStr& name, const CStr& token)
{
LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name, token);
// Find the user with that guid
std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
[&](CNetServerSession* session)
{ return session->GetGUID() == token; });
if (it == m_Sessions.end())
return;
(*it)->SetUserName(name.FromUTF8());
// Send an empty message to request the authentication message from the client
// after its identity has been confirmed via the lobby
CAuthenticateMessage emptyMessage;
(*it)->SendMessage(&emptyMessage);
}
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;
}
CStr guid = ps_generate_guid();
int count = 0;
// Ensure unique GUID
while(std::find_if(
server.m_Sessions.begin(), server.m_Sessions.end(),
[&guid] (const CNetServerSession* session)
{ return session->GetGUID() == guid; }) != server.m_Sessions.end())
{
if (++count > 100)
{
session->Disconnect(NDR_GUID_FAILED);
return true;
}
guid = ps_generate_guid();
}
session->SetGUID(guid);
CSrvHandshakeResponseMessage handshakeResponse;
handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION;
handshakeResponse.m_GUID = guid;
handshakeResponse.m_Flags = 0;
if (server.m_LobbyAuth)
{
handshakeResponse.m_Flags |= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH;
session->SetNextState(NSS_LOBBY_AUTHENTICATE);
}
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 = SanitisePlayerName(message->m_Name);
CStrW usernameWithoutRating(username.substr(0, username.find(L" (")));
// Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176
// "[...] comparisons will be made in case-normalized canonical form."
if (server.m_LobbyAuth && usernameWithoutRating.LowerCase() != session->GetUserName().LowerCase())
{
LOGERROR("Net server: lobby auth: %s tried joining as %s",
session->GetUserName().ToUTF8(),
usernameWithoutRating.ToUTF8());
session->Disconnect(NDR_LOBBY_AUTH_FAILED);
return true;
}
// Check the password before anything else.
if (server.m_Password != message->m_Password)
{
// Noisy logerror because players are not supposed to be able to get the IP,
// so this might be someone targeting the host for some reason
// (or TODO a dedicated server and we do want to log anyways)
LOGERROR("Net server: user %s tried joining with the wrong password",
session->GetUserName().ToUTF8());
session->Disconnect(NDR_SERVER_REFUSED);
return true;
}
// Either deduplicate or prohibit join if name is in use
bool duplicatePlayernames = false;
CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames);
// If lobby authentication is enabled, the clients playername has already been registered.
// There also can't be any duplicated names.
if (!server.m_LobbyAuth && duplicatePlayernames)
username = server.DeduplicatePlayerName(username);
else
{
std::vector::iterator it = std::find_if(
server.m_Sessions.begin(), server.m_Sessions.end(),
[&username] (const CNetServerSession* session)
{ return session->GetUserName() == username; });
if (it != server.m_Sessions.end() && (*it) != session)
{
session->Disconnect(NDR_PLAYERNAME_IN_USE);
return true;
}
}
// Disconnect banned usernames
if (std::find(server.m_BannedPlayers.begin(), server.m_BannedPlayers.end(), server.m_LobbyAuth ? usernameWithoutRating : username) != server.m_BannedPlayers.end())
{
session->Disconnect(NDR_BANNED);
return true;
}
int maxObservers = 0;
CFG_GET_VAL("network.observerlimit", maxObservers);
bool isRejoining = false;
bool serverFull = false;
if (server.m_State == SERVER_STATE_PREGAME)
{
// Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned
serverFull = server.m_Sessions.size() >= MAX_CLIENTS;
}
else
{
bool isObserver = true;
int disconnectedPlayers = 0;
int connectedPlayers = 0;
// (TODO: if GUIDs were stable, we should use them instead)
for (const std::pair& p : server.m_PlayerAssignments)
{
const PlayerAssignment& assignment = p.second;
if (!assignment.m_Enabled && assignment.m_Name == username)
{
isObserver = assignment.m_PlayerID == -1;
isRejoining = true;
}
if (assignment.m_PlayerID == -1)
continue;
if (assignment.m_Enabled)
++connectedPlayers;
else
++disconnectedPlayers;
}
// Optionally allow everyone or only buddies to join after the game has started
if (!isRejoining)
{
CStr observerLateJoin;
CFG_GET_VAL("network.lateobservers", observerLateJoin);
if (observerLateJoin == "everyone")
{
isRejoining = true;
}
else if (observerLateJoin == "buddies")
{
CStr buddies;
CFG_GET_VAL("lobby.buddies", buddies);
std::wstringstream buddiesStream(wstring_from_utf8(buddies));
CStrW buddy;
while (std::getline(buddiesStream, buddy, L','))
{
if (buddy == usernameWithoutRating)
{
isRejoining = true;
break;
}
}
}
}
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;
}
// Ensure all players will be able to rejoin
serverFull = isObserver && (
(int) server.m_Sessions.size() - connectedPlayers > maxObservers ||
(int) server.m_Sessions.size() + disconnectedPlayers >= MAX_CLIENTS);
}
if (serverFull)
{
session->Disconnect(NDR_SERVER_FULL);
return true;
}
u32 newHostID = server.m_NextHostID++;
session->SetUserName(username);
session->SetHostID(newHostID);
CAuthenticateResultMessage authenticateResult;
authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK;
authenticateResult.m_HostID = newHostID;
authenticateResult.m_Message = L"Logged in";
authenticateResult.m_IsController = 0;
if (message->m_ControllerSecret == server.m_ControllerSecret)
{
if (server.m_ControllerGUID.empty())
{
server.m_ControllerGUID = session->GetGUID();
authenticateResult.m_IsController = 1;
}
// TODO: we could probably handle having several controllers, or swapping?
}
session->SendMessage(&authenticateResult);
server.OnUserJoin(session);
if (isRejoining)
{
ENSURE(server.m_State != SERVER_STATE_UNCONNECTED && server.m_State != SERVER_STATE_PREGAME);
// 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::OnSimulationCommand(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SIMULATION_COMMAND);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CSimulationMessage* message = (CSimulationMessage*)event->GetParamRef();
// Ignore messages sent by one player on behalf of another player
// unless cheating is enabled
bool cheatsEnabled = false;
const ScriptInterface& scriptInterface = server.GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue settings(rq.cx);
scriptInterface.GetProperty(server.m_InitAttributes, "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 != message->m_Player))
return true;
// Send it back to all clients that have finished
// the loading screen (and the synchronization when rejoining)
server.Broadcast(message, { NSS_INGAME });
// Save all the received commands
if (server.m_SavedCommands.size() < message->m_Turn + 1)
server.m_SavedCommands.resize(message->m_Turn + 1);
server.m_SavedCommands[message->m_Turn].push_back(*message);
// TODO: we shouldn't send the message back to the client that first sent it
return true;
}
bool CNetServerWorker::OnSyncCheck(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SYNC_CHECK);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CSyncCheckMessage* message = (CSyncCheckMessage*)event->GetParamRef();
server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, message->m_Turn, message->m_Hash);
return true;
}
bool CNetServerWorker::OnEndCommandBatch(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CEndCommandBatchMessage* message = (CEndCommandBatchMessage*)event->GetParamRef();
// The turn-length field is ignored
server.m_ServerTurnManager->NotifyFinishedClientCommands(*session, message->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, { NSS_PREGAME, NSS_INGAME });
return true;
}
bool CNetServerWorker::OnReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_READY);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Occurs if a client presses not-ready
// in the very last moment before the hosts starts the game
if (server.m_State == SERVER_STATE_LOADING)
return true;
CReadyMessage* message = (CReadyMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message, { NSS_PREGAME });
server.m_PlayerAssignments[message->m_GUID].m_Status = message->m_Status;
return true;
}
bool CNetServerWorker::OnClearAllReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLEAR_ALL_READY);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_ControllerGUID)
server.ClearAllPlayerReady();
return true;
}
bool CNetServerWorker::OnGameSetup(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Changing the settings after gamestart is not implemented and would cause an Out-of-sync error.
// This happened when doubleclicking on the startgame button.
if (server.m_State != SERVER_STATE_PREGAME)
return true;
// Only the controller is allowed to send game setup updates.
// TODO: it would be good to allow other players to request changes to some settings,
// e.g. their civilisation.
// Possibly this should use another message, to enforce a single source of truth.
if (session->GetGUID() == server.m_ControllerGUID)
{
CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef();
server.Broadcast(message, { NSS_PREGAME });
}
return true;
}
bool CNetServerWorker::OnAssignPlayer(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_ASSIGN_PLAYER);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_ControllerGUID)
{
CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef();
server.AssignPlayer(message->m_PlayerID, message->m_GUID);
}
return true;
}
bool CNetServerWorker::OnGameStart(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_START);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() != server.m_ControllerGUID)
return true;
CGameStartMessage* message = (CGameStartMessage*)event->GetParamRef();
server.StartGame(message->m_InitAttributes);
return true;
}
bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetServerSession* loadedSession = (CNetServerSession*)context;
CNetServerWorker& server = loadedSession->GetServer();
// We're in the loading state, so wait until every client has loaded
// before starting the game
ENSURE(server.m_State == SERVER_STATE_LOADING);
if (server.CheckGameLoadStatus(loadedSession))
return true;
CClientsLoadingMessage message;
// We always send all GUIDs of clients in the loading state
// so that we don't have to bother about switching GUI pages
for (CNetServerSession* session : server.m_Sessions)
if (session->GetCurrState() != NSS_INGAME && loadedSession->GetGUID() != session->GetGUID())
{
CClientsLoadingMessage::S_m_Clients client;
client.m_GUID = session->GetGUID();
message.m_Clients.push_back(client);
}
// Send to the client who has loaded the game but did not reach the NSS_INGAME state yet
loadedSession->SendMessage(&message);
server.Broadcast(&message, { NSS_INGAME });
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);
+ // Special case: the controller shouldn't be treated as an observer in any case.
+ bool isObserver = server.m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && server.m_ControllerGUID != session->GetGUID();
+ server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn, isObserver);
// 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();
// Inform everyone of the client having rejoined
CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message, { NSS_INGAME });
// Send all pausing players to the rejoined client.
for (const CStr& guid : server.m_PausingPlayers)
{
CClientPausedMessage pausedMessage;
pausedMessage.m_GUID = guid;
pausedMessage.m_Pause = true;
session->SendMessage(&pausedMessage);
}
return true;
}
bool CNetServerWorker::OnKickPlayer(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_KICKED);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_ControllerGUID)
{
CKickedMessage* message = (CKickedMessage*)event->GetParamRef();
server.KickPlayer(message->m_Name, message->m_Ban);
}
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;
}
bool CNetServerWorker::OnClientPaused(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
// Update the list of pausing players.
std::vector::iterator player = std::find(server.m_PausingPlayers.begin(), server.m_PausingPlayers.end(), session->GetGUID());
if (message->m_Pause)
{
if (player != server.m_PausingPlayers.end())
return true;
server.m_PausingPlayers.push_back(session->GetGUID());
}
else
{
if (player == server.m_PausingPlayers.end())
return true;
server.m_PausingPlayers.erase(player);
}
// Send messages to clients that are in game, and are not the client who paused.
for (CNetServerSession* netSession : server.m_Sessions)
if (netSession->GetCurrState() == NSS_INGAME && message->m_GUID != netSession->GetGUID())
netSession->SendMessage(message);
return true;
}
bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
{
for (const CNetServerSession* session : m_Sessions)
if (session != changedSession && session->GetCurrState() != NSS_INGAME)
return false;
// Inform clients that everyone has loaded the map and that the game can start
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = 0;
// Notice the changedSession is still in the NSS_PREGAME state
Broadcast(&loaded, { NSS_PREGAME, NSS_INGAME });
m_State = SERVER_STATE_INGAME;
return true;
}
void CNetServerWorker::StartGame(const CStr& initAttribs)
{
for (std::pair& player : m_PlayerAssignments)
if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0)
{
LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player.second.m_Name).c_str());
return;
}
m_ServerTurnManager = new CNetServerTurnManager(*this);
for (CNetServerSession* session : m_Sessions)
- m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0); // TODO: only for non-observers
+ {
+ // Special case: the controller shouldn't be treated as an observer in any case.
+ bool isObserver = m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && m_ControllerGUID != session->GetGUID();
+ m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0, isObserver);
+ }
m_State = SERVER_STATE_LOADING;
// 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();
// Update init attributes. They should no longer change.
m_ScriptInterface->ParseJSON(initAttribs, &m_InitAttributes);
CGameStartMessage gameStart;
gameStart.m_InitAttributes = initAttribs;
Broadcast(&gameStart, { NSS_PREGAME });
}
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 (const CNetServerSession* session : m_Sessions)
{
if (session->GetUserName() == name)
{
unique = false;
break;
}
}
if (unique)
return name;
name = original + L" (" + CStrW::FromUInt(id++) + L")";
}
}
void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
{
if (m_Host)
StunClient::SendHolePunchingMessages(*m_Host, ipStr, port);
}
CNetServer::CNetServer(bool useLobbyAuth, int autostartPlayers) :
m_Worker(new CNetServerWorker(useLobbyAuth, autostartPlayers)),
m_LobbyAuth(useLobbyAuth), m_UseSTUN(false), m_PublicIp(""), m_PublicPort(20595), m_Password()
{
}
CNetServer::~CNetServer()
{
delete m_Worker;
}
bool CNetServer::GetUseSTUN() const
{
return m_UseSTUN;
}
bool CNetServer::UseLobbyAuth() const
{
return m_LobbyAuth;
}
bool CNetServer::SetupConnection(const u16 port)
{
return m_Worker->SetupConnection(port);
}
u16 CNetServer::GetPublicPort() const
{
return m_PublicPort;
}
CStr CNetServer::GetPublicIp() const
{
return m_PublicIp;
}
void CNetServer::SetConnectionData(const CStr& ip, const u16 port, bool useSTUN)
{
m_PublicIp = ip;
m_PublicPort = port;
m_UseSTUN = useSTUN;
}
bool CNetServer::CheckPasswordAndIncrement(const CStr& password, const std::string& username)
{
std::unordered_map::iterator it = m_FailedAttempts.find(username);
if (m_Password == password)
{
if (it != m_FailedAttempts.end())
it->second = 0;
return true;
}
if (it == m_FailedAttempts.end())
m_FailedAttempts.emplace(username, 1);
else
it->second++;
return false;
}
bool CNetServer::IsBanned(const std::string& username) const
{
std::unordered_map::const_iterator it = m_FailedAttempts.find(username);
return it != m_FailedAttempts.end() && it->second >= FAILED_PASSWORD_TRIES_BEFORE_BAN;
}
void CNetServer::SetPassword(const CStr& password)
{
m_Password = password;
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->SetPassword(password);
}
void CNetServer::SetControllerSecret(const std::string& secret)
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->SetControllerSecret(secret);
}
void CNetServer::StartGame()
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_StartGameQueue.push_back(true);
}
void CNetServer::UpdateInitAttributes(JS::MutableHandleValue attrs, const 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);
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_InitAttributesQueue.push_back(attrsJSON);
}
void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token)
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token));
}
void CNetServer::SetTurnLength(u32 msecs)
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_TurnLengthQueue.push_back(msecs);
}
void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port)
{
m_Worker->SendHolePunchingMessage(ip, port);
}
Index: ps/trunk/source/network/NetServerTurnManager.cpp
===================================================================
--- ps/trunk/source/network/NetServerTurnManager.cpp (revision 25155)
+++ ps/trunk/source/network/NetServerTurnManager.cpp (revision 25156)
@@ -1,205 +1,216 @@
/* Copyright (C) 2021 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 "NetServerTurnManager.h"
#include "NetServer.h"
#include "NetSession.h"
#include "lib/utf8.h"
#include "ps/CLogger.h"
+#include "ps/ConfigDB.h"
#include "simulation2/system/TurnManager.h"
#if 0
#include "ps/Util.h"
#define NETSERVERTURN_LOG(...) debug_printf(__VA_ARGS__)
#else
#define NETSERVERTURN_LOG(...)
#endif
CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server)
: m_NetServer(server), m_ReadyTurn(COMMAND_DELAY_MP - 1), m_TurnLength(DEFAULT_TURN_LENGTH), m_HasSyncError(false)
{
// Turn 0 is not actually executed, store a dummy value.
m_SavedTurnLengths.push_back(0);
// Turns [1..COMMAND_DELAY - 1] are special: all clients run them without waiting on a server command batch.
// Because of this, they are always run with the default MP turn length.
for (u32 i = 1; i < COMMAND_DELAY_MP; ++i)
m_SavedTurnLengths.push_back(m_TurnLength);
}
void CNetServerTurnManager::NotifyFinishedClientCommands(CNetServerSession& session, u32 turn)
{
int client = session.GetHostID();
NETSERVERTURN_LOG("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
if (turn != m_ClientsReady[client] + 1)
{
LOGERROR("NotifyFinishedClientCommands: Client %d (%s) is ready for turn %d, but expected %d",
client,
utf8_from_wstring(session.GetUserName()).c_str(),
turn,
m_ClientsReady[client] + 1);
session.Disconnect(NDR_INCORRECT_READY_TURN_COMMANDS);
}
m_ClientsReady[client] = turn;
// Check whether this was the final client to become ready
CheckClientsReady();
}
void CNetServerTurnManager::CheckClientsReady()
{
+ int max_observer_lag = -1;
+ CFG_GET_VAL("network.observermaxlag", max_observer_lag);
+ // Clamp to 0-10000 turns, below/above that is no limit.
+ max_observer_lag = max_observer_lag < 0 ? -1 : max_observer_lag > 10000 ? -1 : max_observer_lag;
+
// See if all clients (including self) are ready for a new turn
for (const std::pair& clientReady : m_ClientsReady)
{
+ // Observers are allowed to lag more than regular clients.
+ if (m_ClientsObserver[clientReady.first] && (max_observer_lag == -1 || clientReady.second > m_ReadyTurn - max_observer_lag))
+ continue;
NETSERVERTURN_LOG(" %d: %d <=? %d\n", clientReady.first, clientReady.second, m_ReadyTurn);
if (clientReady.second <= m_ReadyTurn)
return; // wasn't ready for m_ReadyTurn+1
}
++m_ReadyTurn;
NETSERVERTURN_LOG("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, { NSS_INGAME });
ENSURE(m_SavedTurnLengths.size() == m_ReadyTurn);
m_SavedTurnLengths.push_back(m_TurnLength);
}
void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& session, u32 turn, const CStr& hash)
{
int client = session.GetHostID();
const CStrW& playername = session.GetUserName();
// Clients must advance one turn at a time
if (turn != m_ClientsSimulated[client] + 1)
{
LOGERROR("NotifyFinishedClientUpdate: Client %d (%s) is ready for turn %d, but expected %d",
client,
utf8_from_wstring(playername).c_str(),
turn,
m_ClientsReady[client] + 1);
session.Disconnect(NDR_INCORRECT_READY_TURN_SIMULATED);
}
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 (const std::pair& clientSimulated : m_ClientsSimulated)
if (clientSimulated.second < newest)
newest = clientSimulated.second;
// For every set of state hashes that all clients have simulated, check for OOS
for (const std::pair>& clientStateHash : m_ClientStateHashes)
{
if (clientStateHash.first > newest)
break;
// Assume the host is correct (maybe we should choose the most common instead to help debugging)
std::string expected = clientStateHash.second.begin()->second;
// Find all players that are OOS on that turn
std::vector OOSPlayerNames;
for (const std::pair& hashPair : clientStateHash.second)
{
NETSERVERTURN_LOG("sync check %d: %d = %hs\n", clientStateHash.first, hashPair.first, Hexify(hashPair.second).c_str());
if (hashPair.second != expected)
{
// Oh no, out of sync
m_HasSyncError = true;
OOSPlayerNames.push_back(m_ClientPlayernames[hashPair.first]);
}
}
// Tell everyone about it
if (m_HasSyncError)
{
CSyncErrorMessage msg;
msg.m_Turn = clientStateHash.first;
msg.m_HashExpected = expected;
for (const CStrW& oosPlayername : OOSPlayerNames)
{
CSyncErrorMessage::S_m_PlayerNames h;
h.m_Name = oosPlayername;
msg.m_PlayerNames.push_back(h);
}
m_NetServer.Broadcast(&msg, { NSS_INGAME });
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)
+void CNetServerTurnManager::InitialiseClient(int client, u32 turn, bool observer)
{
NETSERVERTURN_LOG("InitialiseClient(client=%d, turn=%d)\n", client, turn);
ENSURE(m_ClientsReady.find(client) == m_ClientsReady.end());
m_ClientsReady[client] = turn + COMMAND_DELAY_MP - 1;
m_ClientsSimulated[client] = turn;
+ m_ClientsObserver[client] = observer;
}
void CNetServerTurnManager::UninitialiseClient(int client)
{
NETSERVERTURN_LOG("UninitialiseClient(client=%d)\n", client);
ENSURE(m_ClientsReady.find(client) != m_ClientsReady.end());
m_ClientsReady.erase(client);
m_ClientsSimulated.erase(client);
+ m_ClientsObserver.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/NetServerTurnManager.h
===================================================================
--- ps/trunk/source/network/NetServerTurnManager.h (revision 25155)
+++ ps/trunk/source/network/NetServerTurnManager.h (revision 25156)
@@ -1,100 +1,105 @@
-/* Copyright (C) 2018 Wildfire Games.
+/* Copyright (C) 2021 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_NETSERVERTURNMANAGER
#define INCLUDED_NETSERVERTURNMANAGER
-#include