Index: ps/trunk/binaries/data/config/default.cfg
===================================================================
--- ps/trunk/binaries/data/config/default.cfg (revision 27598)
+++ ps/trunk/binaries/data/config/default.cfg (revision 27599)
@@ -1,591 +1,592 @@
; 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
; Switches between real fullscreen and borderless window on a full display size.
borderless.fullscreen = true
; Hides a window border in the windowed mode.
borderless.window = 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
; Enable Hi-DPI where supported, currently working only for testing.
hidpi = false
; 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 = 8
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. (-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
; Increases details closer to the camera but decreases performance
; especially on low hardware.
shadowscascadecount = 1
shadowscascadedistanceratio = 1.7
; Hides shadows after the distance.
shadowscutoffdistance = 300.0
; If true shadows cover the whole map instead of the camera frustum.
shadowscovermap = false
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
; Different ways to draw a cursor, possible values are "sdl" and "system".
; The "system" one doesn't support a visual change of the cursor.
cursorbackend = "sdl"
; Backends for all graphics rendering:
; glarb - GL with legacy assembler-like shaders, might used only for buggy drivers.
; gl - GL with GLSL shaders, should be used by default.
; dummy - backend that does nothing, allows to check performance without backend drivers.
; vulkan - Vulkan with SPIR-V shaders.
rendererbackend = "gl"
; Enables additional debug information in renderer backend.
renderer.backend.debugcontext = "false"
renderer.backend.debugmessages = "false"
renderer.backend.debuglabels = "false"
renderer.backend.debugscopedlabels = "false"
renderer.backend.vulkan.disabledescriptorindexing = "false"
renderer.backend.vulkan.deviceindexoverride = -1
; Should not be edited. It's used only for preventing of running fixed pipeline.
renderpath = default
; (0 - low, 1 - medium, 2 - high), higher quality means worse performance.
textures.quality = 2
; (1, 2, 4, 8 and 16)
textures.maxanisotropy = 2
;;;;; EXPERIMENTAL ;;;;;
; Experimental probably-non-working GPU skinning support; requires GLSL; use at own risk
gpuskinning = false
; Use smooth LOS interpolation
smoothlos = true
; Use screen-space postprocessing filters (HDR, bloom, DOF, etc). Incompatible with fixed renderpath.
postproc = true
; Use anti-aliasing techniques.
antialiasing = "disabled"
; Use sharpening techniques.
sharpening = "disabled"
sharpness = 0.3
; Quality used for actors.
max_actor_quality=200
; Whether or not actor variants are selected randomly, possible values are "full", "limited", "none".
variant_diversity = "full"
; Quality level of shader effects (set to 10 to display all effects)
materialmgr.quality = 10.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 = "" ; 'Custom' exit to desktop, SDL handles the native command via SDL_Quit.
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
lastattackfocus = "Space" ; Focus the camera on the last notified attack
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
idlebuilder = Semicolon ; Select next idle builder
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
singleselection = "" ; Modifier to select units individually, opposed to per formation.
[hotkey.selection.group.add]
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"
10 = "Shift+0", "Shift+Num0"
[hotkey.selection.group.save]
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"
10 = "Ctrl+0", "Ctrl+Num0"
[hotkey.selection.group.select]
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
10 = 0, Num0
[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.
leaveturret = "U" ; Leave turret point.
move = "" ; Modifier to move to a point instead of another action (e.g. gather)
capture = "C" ; Modifier to capture instead of another action (e.g. attack)
attack = "" ; 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")
flare = K ; Modifier to send a flare to your allies
flareactivate = "" ; Modifier to activate the mode to send a flare to your allies
calltoarms = "" ; Modifier to call the selected units to the arms.
; 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 = "Ctrl+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]
dragdelta = 4 ; Number of pixels the mouse can move before the action is considered a drag
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
flarelifetime = 6 ; How long the flare markers on the minimap are displayed in seconds
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)
selectformationasone = "true" ; Whether to select formations as a whole by default.
[gui.session.minimap]
; Icons that are displayed for some entities on a minimap.
icons.enabled = "true"
icons.opacity = 1.0
icons.sizescale = 1.0
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 = "arena27" ; 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 = "wfgbot27" ; Name of the server-side XMPP-account that manage games
echelon = "echelon27" ; 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 = "RWQv2alKl8D0zMDJR766jpYvPy4u3y77HL/iKb/lsT1Fnf6ezoMb2x8+" ; 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 = -1 ; 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).
+enetmtu = 1372 ; Lower ENet protocol MTU in case packets get further fragmented on the UDP layer which may cause drops.
[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.
[rlinterface]
address = "127.0.0.1:6000"
[sound]
mastergain = 0.9
musicgain = 0.2
ambientgain = 0.6
actiongain = 0.7
uigain = 0.7
mindistance = 1
maxdistance = 350
maxstereoangle = 0.62 ; About PI/5 radians
[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
scroll.mouse.detectdistance = 3
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/source/network/NetClient.cpp
===================================================================
--- ps/trunk/source/network/NetClient.cpp (revision 27598)
+++ ps/trunk/source/network/NetClient.cpp (revision 27599)
@@ -1,1011 +1,1012 @@
-/* Copyright (C) 2022 Wildfire Games.
+/* Copyright (C) 2023 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "NetClient.h"
#include "NetClientTurnManager.h"
+#include "NetEnet.h"
#include "NetMessage.h"
#include "NetSession.h"
#include "lib/byte_order.h"
#include "lib/external_libraries/enet.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/sysdep/sysdep.h"
#include "lobby/IXmppClient.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/Compress.h"
#include "ps/CStr.h"
#include "ps/Game.h"
#include "ps/Hashing.h"
#include "ps/Loader.h"
#include "ps/Profile.h"
#include "ps/Threading.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/JSON.h"
#include "simulation2/Simulation2.h"
#include "network/StunClient.h"
/**
* 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;
CNetClient *g_NetClient = NULL;
/**
* Async task for receiving the initial game state when rejoining an
* in-progress network game.
*/
class CNetFileReceiveTask_ClientRejoin : public CNetFileReceiveTask
{
NONCOPYABLE(CNetFileReceiveTask_ClientRejoin);
public:
CNetFileReceiveTask_ClientRejoin(CNetClient& client, const CStr& initAttribs)
: m_Client(client), m_InitAttributes(initAttribs)
{
}
virtual void OnComplete()
{
// We've received the game state from the server
// Save it so we can use it after the map has finished loading
m_Client.m_JoinSyncBuffer = m_Buffer;
// Pretend the server told us to start the game
CGameStartMessage start;
start.m_InitAttributes = m_InitAttributes;
m_Client.HandleMessage(&start);
}
private:
CNetClient& m_Client;
CStr m_InitAttributes;
};
CNetClient::CNetClient(CGame* game) :
m_Session(NULL),
m_UserName(L"anonymous"),
m_HostID((u32)-1), m_ClientTurnManager(NULL), m_Game(game),
m_LastConnectionCheck(0),
m_ServerAddress(),
m_ServerPort(0),
m_Rejoin(false)
{
m_Game->SetTurnManager(NULL); // delete the old local turn manager so we don't accidentally use it
void* context = this;
JS_AddExtraGCRootsTracer(GetScriptInterface().GetGeneralJSContext(), CNetClient::Trace, this);
// Set up transitions for session
AddTransition(NCS_UNCONNECTED, (uint)NMT_CONNECT_COMPLETE, NCS_CONNECT, (void*)&OnConnect, context);
AddTransition(NCS_CONNECT, (uint)NMT_SERVER_HANDSHAKE, NCS_HANDSHAKE, (void*)&OnHandshake, context);
AddTransition(NCS_HANDSHAKE, (uint)NMT_SERVER_HANDSHAKE_RESPONSE, NCS_AUTHENTICATE, (void*)&OnHandshakeResponse, context);
AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NCS_AUTHENTICATE, (void*)&OnAuthenticateRequest, context);
AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE_RESULT, NCS_PREGAME, (void*)&OnAuthenticate, context);
AddTransition(NCS_PREGAME, (uint)NMT_CHAT, NCS_PREGAME, (void*)&OnChat, context);
AddTransition(NCS_PREGAME, (uint)NMT_READY, NCS_PREGAME, (void*)&OnReady, context);
AddTransition(NCS_PREGAME, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context);
AddTransition(NCS_PREGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_PREGAME, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_PREGAME, (uint)NMT_KICKED, NCS_PREGAME, (void*)&OnKicked, context);
AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_PREGAME, (void*)&OnClientTimeout, context);
AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_PREGAME, (void*)&OnClientPerformance, context);
AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, (void*)&OnGameStart, context);
AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, (void*)&OnJoinSyncStart, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, (void*)&OnChat, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_SETUP, NCS_JOIN_SYNCING, (void*)&OnGameSetup, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_JOIN_SYNCING, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_KICKED, NCS_JOIN_SYNCING, (void*)&OnKicked, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_TIMEOUT, NCS_JOIN_SYNCING, (void*)&OnClientTimeout, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_PERFORMANCE, NCS_JOIN_SYNCING, (void*)&OnClientPerformance, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_START, NCS_JOIN_SYNCING, (void*)&OnGameStart, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SIMULATION_COMMAND, NCS_JOIN_SYNCING, (void*)&OnInGame, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_END_COMMAND_BATCH, NCS_JOIN_SYNCING, (void*)&OnJoinSyncEndCommandBatch, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context);
AddTransition(NCS_LOADING, (uint)NMT_CHAT, NCS_LOADING, (void*)&OnChat, context);
AddTransition(NCS_LOADING, (uint)NMT_GAME_SETUP, NCS_LOADING, (void*)&OnGameSetup, context);
AddTransition(NCS_LOADING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_LOADING, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_LOADING, (uint)NMT_KICKED, NCS_LOADING, (void*)&OnKicked, context);
AddTransition(NCS_LOADING, (uint)NMT_CLIENT_TIMEOUT, NCS_LOADING, (void*)&OnClientTimeout, context);
AddTransition(NCS_LOADING, (uint)NMT_CLIENT_PERFORMANCE, NCS_LOADING, (void*)&OnClientPerformance, context);
AddTransition(NCS_LOADING, (uint)NMT_CLIENTS_LOADING, NCS_LOADING, (void*)&OnClientsLoading, context);
AddTransition(NCS_LOADING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context);
AddTransition(NCS_INGAME, (uint)NMT_REJOINED, NCS_INGAME, (void*)&OnRejoined, context);
AddTransition(NCS_INGAME, (uint)NMT_KICKED, NCS_INGAME, (void*)&OnKicked, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_INGAME, (void*)&OnClientTimeout, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_INGAME, (void*)&OnClientPerformance, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENTS_LOADING, NCS_INGAME, (void*)&OnClientsLoading, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PAUSED, NCS_INGAME, (void*)&OnClientPaused, context);
AddTransition(NCS_INGAME, (uint)NMT_CHAT, NCS_INGAME, (void*)&OnChat, context);
AddTransition(NCS_INGAME, (uint)NMT_GAME_SETUP, NCS_INGAME, (void*)&OnGameSetup, context);
AddTransition(NCS_INGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_INGAME, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_INGAME, (uint)NMT_SIMULATION_COMMAND, NCS_INGAME, (void*)&OnInGame, context);
AddTransition(NCS_INGAME, (uint)NMT_SYNC_ERROR, NCS_INGAME, (void*)&OnInGame, context);
AddTransition(NCS_INGAME, (uint)NMT_END_COMMAND_BATCH, NCS_INGAME, (void*)&OnInGame, context);
// Set first state
SetFirstState(NCS_UNCONNECTED);
}
CNetClient::~CNetClient()
{
// Try to flush messages before dying (probably fails).
if (m_ClientTurnManager)
m_ClientTurnManager->OnDestroyConnection();
DestroyConnection();
JS_RemoveExtraGCRootsTracer(GetScriptInterface().GetGeneralJSContext(), CNetClient::Trace, this);
}
void CNetClient::TraceMember(JSTracer *trc)
{
for (JS::Heap& guiMessage : m_GuiMessageQueue)
JS::TraceEdge(trc, &guiMessage, "m_GuiMessageQueue");
}
void CNetClient::SetUserName(const CStrW& username)
{
ENSURE(!m_Session); // must be called before we start the connection
m_UserName = username;
}
void CNetClient::SetHostJID(const CStr& jid)
{
m_HostJID = jid;
}
void CNetClient::SetGamePassword(const CStr& hashedPassword)
{
// Hash on top with the user's name, to make sure not all
// hashing data is in control of the host.
m_Password = HashCryptographically(hashedPassword, m_UserName.ToUTF8());
}
void CNetClient::SetControllerSecret(const std::string& secret)
{
m_ControllerSecret = secret;
}
bool CNetClient::SetupConnection(ENetHost* enetClient)
{
CNetClientSession* session = new CNetClientSession(*this);
bool ok = session->Connect(m_ServerAddress, m_ServerPort, enetClient);
SetAndOwnSession(session);
if (ok)
m_PollingThread = std::thread(Threading::HandleExceptions::Wrapper, m_Session);
return ok;
}
void CNetClient::SetupConnectionViaLobby()
{
g_XmppClient->SendIqGetConnectionData(m_HostJID, m_Password, m_UserName.ToUTF8(), false);
}
void CNetClient::SetupServerData(CStr address, u16 port, bool stun)
{
ENSURE(!m_Session);
m_ServerAddress = address;
m_ServerPort = port;
m_UseSTUN = stun;
}
void CNetClient::HandleGetServerDataFailed(const CStr& error)
{
if (m_Session)
return;
PushGuiMessage(
"type", "serverdata",
"status", "failed",
"reason", error
);
}
bool CNetClient::TryToConnect(const CStr& hostJID, bool localNetwork)
{
if (m_Session)
return false;
if (m_ServerAddress.empty())
{
PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", static_cast(NDR_SERVER_REFUSED));
return false;
}
ENetAddress hostAddr{ ENET_HOST_ANY, ENET_PORT_ANY };
- ENetHost* enetClient = enet_host_create(&hostAddr, 1, 1, 0, 0);
+ ENetHost* enetClient = PS::Enet::CreateHost(&hostAddr, 1, 1);
if (!enetClient)
{
PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", static_cast(NDR_STUN_PORT_FAILED));
return false;
}
CStr ip;
u16 port = 0;
if (g_XmppClient && m_UseSTUN)
{
if (!StunClient::FindPublicIP(*enetClient, ip, port))
{
PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", static_cast(NDR_STUN_ENDPOINT_FAILED));
return false;
}
// If the host is on the same network, we risk failing to connect
// on routers that don't support NAT hairpinning/NAT loopback.
// To work around that, send again a connection data request, but for internal IP this time.
if (ip == m_ServerAddress)
{
g_XmppClient->SendIqGetConnectionData(m_HostJID, m_Password, m_UserName.ToUTF8(), true);
// Return true anyways - we're on a success path here.
return true;
}
}
else if (g_XmppClient && localNetwork)
{
// We may need to punch a hole through the local firewall, so fetch our local IP.
// NB: we'll ignore failures here, and hope that the firewall will be open to connection
// if we fail to fetch the local IP (which is unlikely anyways).
if (!StunClient::FindLocalIP(ip))
ip = "";
// Check if we're hosting on localhost, and if so, explicitly use that
// (this circumvents, at least, the 'block all incoming connections' setting
// on the MacOS firewall).
if (ip == m_ServerAddress)
{
m_ServerAddress = "127.0.0.1";
ip = "";
}
port = enetClient->address.port;
}
LOGMESSAGE("NetClient: connecting to server at %s:%i", m_ServerAddress, m_ServerPort);
if (!ip.empty())
{
// UDP hole-punching
// Step 0: send a message, via XMPP, to the server with our external IP & port.
g_XmppClient->SendStunEndpointToHost(ip, port, hostJID);
// Step 1b: Wait some time - we need the host to receive the stun endpoint and start punching a hole themselves before
// we try to establish the connection below.
SDL_Delay(1000);
// Step 2: Send a message ourselves to the server so that the NAT, if any, routes incoming trafic correctly.
// TODO: verify if this step is necessary, since we'll try and connect anyways below.
StunClient::SendHolePunchingMessages(*enetClient, m_ServerAddress, m_ServerPort);
}
if (!g_NetClient->SetupConnection(enetClient))
{
PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", static_cast(NDR_UNKNOWN));
return false;
}
return true;
}
void CNetClient::SetAndOwnSession(CNetClientSession* session)
{
delete m_Session;
m_Session = session;
}
void CNetClient::DestroyConnection()
{
if (m_Session)
m_Session->Shutdown();
if (m_PollingThread.joinable())
// Use detach() over join() because we don't want to wait for the session
// (which may be polling or trying to send messages).
m_PollingThread.detach();
// The polling thread will cleanup the session on its own,
// mark it as nullptr here so we know we're done using it.
m_Session = nullptr;
}
void CNetClient::Poll()
{
if (!m_Session)
return;
PROFILE3("NetClient::poll");
CheckServerConnection();
m_Session->ProcessPolledMessages();
}
void CNetClient::CheckServerConnection()
{
// Trigger local warnings if the connection to the server is bad.
// At most once per second.
std::time_t now = std::time(nullptr);
if (now <= m_LastConnectionCheck)
return;
m_LastConnectionCheck = now;
// Report if we are losing the connection to the server
u32 lastReceived = m_Session->GetLastReceivedTime();
if (lastReceived > NETWORK_WARNING_TIMEOUT)
{
PushGuiMessage(
"type", "netwarn",
"warntype", "server-timeout",
"lastReceivedTime", lastReceived);
return;
}
// Report if we have a bad ping to the server.
u32 meanRTT = m_Session->GetMeanRTT();
if (meanRTT > NETWORK_BAD_PING)
{
PushGuiMessage(
"type", "netwarn",
"warntype", "server-latency",
"meanRTT", meanRTT);
}
}
void CNetClient::GuiPoll(JS::MutableHandleValue ret)
{
if (m_GuiMessageQueue.empty())
{
ret.setUndefined();
return;
}
ret.set(m_GuiMessageQueue.front());
m_GuiMessageQueue.pop_front();
}
std::string CNetClient::TestReadGuiMessages()
{
ScriptRequest rq(GetScriptInterface());
std::string r;
JS::RootedValue msg(rq.cx);
while (true)
{
GuiPoll(&msg);
if (msg.isUndefined())
break;
r += Script::ToString(rq, &msg) + "\n";
}
return r;
}
const ScriptInterface& CNetClient::GetScriptInterface()
{
return m_Game->GetSimulation2()->GetScriptInterface();
}
void CNetClient::PostPlayerAssignmentsToScript()
{
ScriptRequest rq(GetScriptInterface());
JS::RootedValue newAssignments(rq.cx);
Script::CreateObject(rq, &newAssignments);
for (const std::pair& p : m_PlayerAssignments)
{
JS::RootedValue assignment(rq.cx);
Script::CreateObject(
rq,
&assignment,
"name", p.second.m_Name,
"player", p.second.m_PlayerID,
"status", p.second.m_Status);
Script::SetProperty(rq, newAssignments, p.first.c_str(), assignment);
}
PushGuiMessage(
"type", "players",
"newAssignments", newAssignments);
}
bool CNetClient::SendMessage(const CNetMessage* message)
{
if (!m_Session)
return false;
return m_Session->SendMessage(message);
}
void CNetClient::HandleConnect()
{
Update((uint)NMT_CONNECT_COMPLETE, NULL);
}
void CNetClient::HandleDisconnect(u32 reason)
{
PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", reason);
DestroyConnection();
// Update the state immediately to UNCONNECTED (don't bother with FSM transitions since
// we'd need one for every single state, and we don't need to use per-state actions)
SetCurrState(NCS_UNCONNECTED);
}
void CNetClient::SendGameSetupMessage(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface)
{
CGameSetupMessage gameSetup(scriptInterface);
gameSetup.m_Data = attrs;
SendMessage(&gameSetup);
}
void CNetClient::SendAssignPlayerMessage(const int playerID, const CStr& guid)
{
CAssignPlayerMessage assignPlayer;
assignPlayer.m_PlayerID = playerID;
assignPlayer.m_GUID = guid;
SendMessage(&assignPlayer);
}
void CNetClient::SendChatMessage(const std::wstring& text)
{
CChatMessage chat;
chat.m_Message = text;
SendMessage(&chat);
}
void CNetClient::SendReadyMessage(const int status)
{
CReadyMessage readyStatus;
readyStatus.m_Status = status;
SendMessage(&readyStatus);
}
void CNetClient::SendClearAllReadyMessage()
{
CClearAllReadyMessage clearAllReady;
SendMessage(&clearAllReady);
}
void CNetClient::SendStartGameMessage(const CStr& initAttribs)
{
CGameStartMessage gameStart;
gameStart.m_InitAttributes = initAttribs;
SendMessage(&gameStart);
}
void CNetClient::SendRejoinedMessage()
{
CRejoinedMessage rejoinedMessage;
SendMessage(&rejoinedMessage);
}
void CNetClient::SendKickPlayerMessage(const CStrW& playerName, bool ban)
{
CKickedMessage kickPlayer;
kickPlayer.m_Name = playerName;
kickPlayer.m_Ban = ban;
SendMessage(&kickPlayer);
}
void CNetClient::SendPausedMessage(bool pause)
{
CClientPausedMessage pausedMessage;
pausedMessage.m_Pause = pause;
SendMessage(&pausedMessage);
}
bool CNetClient::HandleMessage(CNetMessage* message)
{
// Handle non-FSM messages first
Status status = m_Session->GetFileTransferer().HandleMessageReceive(*message);
if (status == INFO::OK)
return true;
if (status != INFO::SKIPPED)
return false;
if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
{
CFileTransferRequestMessage* reqMessage = static_cast(message);
// TODO: we should support different transfer request types, instead of assuming
// it's always requesting the simulation state
std::stringstream stream;
LOGMESSAGERENDER("Serializing game at turn %u for rejoining player", m_ClientTurnManager->GetCurrentTurn());
u32 turn = to_le32(m_ClientTurnManager->GetCurrentTurn());
stream.write((char*)&turn, sizeof(turn));
bool ok = m_Game->GetSimulation2()->SerializeState(stream);
ENSURE(ok);
// Compress the content with zlib to save bandwidth
// (TODO: if this is still too large, compressing with e.g. LZMA works much better)
std::string compressed;
CompressZLib(stream.str(), compressed, true);
m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, compressed);
return true;
}
// Update FSM
bool ok = Update(message->GetType(), message);
if (!ok)
LOGERROR("Net client: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)GetCurrState());
return ok;
}
void CNetClient::LoadFinished()
{
if (!m_JoinSyncBuffer.empty())
{
// We're rejoining a game, and just finished loading the initial map,
// so deserialize the saved game state now
std::string state;
DecompressZLib(m_JoinSyncBuffer, state, true);
std::stringstream stream(state);
u32 turn;
stream.read((char*)&turn, sizeof(turn));
turn = to_le32(turn);
LOGMESSAGE("Rejoining client deserializing state at turn %u\n", turn);
bool ok = m_Game->GetSimulation2()->DeserializeState(stream);
ENSURE(ok);
m_ClientTurnManager->ResetState(turn, turn);
PushGuiMessage(
"type", "netstatus",
"status", "join_syncing");
}
else
{
// Connecting at the start of a game, so we'll wait for other players to finish loading
PushGuiMessage(
"type", "netstatus",
"status", "waiting_for_players");
}
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = m_ClientTurnManager->GetCurrentTurn();
SendMessage(&loaded);
}
void CNetClient::SendAuthenticateMessage()
{
CAuthenticateMessage authenticate;
authenticate.m_Name = m_UserName;
authenticate.m_Password = m_Password;
authenticate.m_ControllerSecret = m_ControllerSecret;
SendMessage(&authenticate);
}
bool CNetClient::OnConnect(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE);
CNetClient* client = static_cast(context);
client->PushGuiMessage(
"type", "netstatus",
"status", "connected");
return true;
}
bool CNetClient::OnHandshake(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE);
CNetClient* client = static_cast(context);
CCliHandshakeMessage handshake;
handshake.m_MagicResponse = PS_PROTOCOL_MAGIC_RESPONSE;
handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
client->SendMessage(&handshake);
return true;
}
bool CNetClient::OnHandshakeResponse(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE_RESPONSE);
CNetClient* client = static_cast(context);
CSrvHandshakeResponseMessage* message = static_cast(event->GetParamRef());
client->m_GUID = message->m_GUID;
if (message->m_Flags & PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH)
{
if (g_XmppClient && !client->m_HostJID.empty())
g_XmppClient->SendIqLobbyAuth(client->m_HostJID, client->m_GUID);
else
{
client->PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", static_cast(NDR_LOBBY_AUTH_FAILED));
LOGMESSAGE("Net client: Couldn't send lobby auth xmpp message");
}
return true;
}
client->SendAuthenticateMessage();
return true;
}
bool CNetClient::OnAuthenticateRequest(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
CNetClient* client = static_cast(context);
client->SendAuthenticateMessage();
return true;
}
bool CNetClient::OnAuthenticate(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE_RESULT);
CNetClient* client = static_cast(context);
CAuthenticateResultMessage* message = static_cast(event->GetParamRef());
LOGMESSAGE("Net: Authentication result: host=%u, %s", message->m_HostID, utf8_from_wstring(message->m_Message));
client->m_HostID = message->m_HostID;
client->m_Rejoin = message->m_Code == ARC_OK_REJOINING;
client->m_IsController = message->m_IsController;
client->PushGuiMessage(
"type", "netstatus",
"status", "authenticated",
"rejoining", client->m_Rejoin);
return true;
}
bool CNetClient::OnChat(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CHAT);
CNetClient* client = static_cast(context);
CChatMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "chat",
"guid", message->m_GUID,
"text", message->m_Message);
return true;
}
bool CNetClient::OnReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_READY);
CNetClient* client = static_cast(context);
CReadyMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "ready",
"guid", message->m_GUID,
"status", message->m_Status);
return true;
}
bool CNetClient::OnGameSetup(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
CNetClient* client = static_cast(context);
CGameSetupMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "gamesetup",
"data", message->m_Data);
return true;
}
bool CNetClient::OnPlayerAssignment(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_PLAYER_ASSIGNMENT);
CNetClient* client = static_cast(context);
CPlayerAssignmentMessage* message = static_cast(event->GetParamRef());
// Unpack the message
PlayerAssignmentMap newPlayerAssignments;
for (size_t i = 0; i < message->m_Hosts.size(); ++i)
{
PlayerAssignment assignment;
assignment.m_Enabled = true;
assignment.m_Name = message->m_Hosts[i].m_Name;
assignment.m_PlayerID = message->m_Hosts[i].m_PlayerID;
assignment.m_Status = message->m_Hosts[i].m_Status;
newPlayerAssignments[message->m_Hosts[i].m_GUID] = assignment;
}
client->m_PlayerAssignments.swap(newPlayerAssignments);
client->PostPlayerAssignmentsToScript();
return true;
}
// This is called either when the host clicks the StartGame button or
// if this client rejoins and finishes the download of the simstate.
bool CNetClient::OnGameStart(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_START);
CNetClient* client = static_cast(context);
CGameStartMessage* message = static_cast(event->GetParamRef());
// Find the player assigned to our GUID
int player = -1;
if (client->m_PlayerAssignments.find(client->m_GUID) != client->m_PlayerAssignments.end())
player = client->m_PlayerAssignments[client->m_GUID].m_PlayerID;
client->m_ClientTurnManager = new CNetClientTurnManager(
*client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger());
// Parse init attributes.
const ScriptInterface& scriptInterface = client->m_Game->GetSimulation2()->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue initAttribs(rq.cx);
Script::ParseJSON(rq, message->m_InitAttributes, &initAttribs);
client->m_Game->SetPlayerID(player);
client->m_Game->StartGame(&initAttribs, "");
client->PushGuiMessage("type", "start",
"initAttributes", initAttribs);
return true;
}
bool CNetClient::OnJoinSyncStart(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START);
CNetClient* client = static_cast(context);
CJoinSyncStartMessage* joinSyncStartMessage = (CJoinSyncStartMessage*)event->GetParamRef();
// The server wants us to start downloading the game state from it, so do so
client->m_Session->GetFileTransferer().StartTask(
std::shared_ptr(new CNetFileReceiveTask_ClientRejoin(*client, joinSyncStartMessage->m_InitAttributes))
);
return true;
}
bool CNetClient::OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH);
CNetClient* client = static_cast(context);
CEndCommandBatchMessage* endMessage = (CEndCommandBatchMessage*)event->GetParamRef();
client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength);
// Execute all the received commands for the latest turn
client->m_ClientTurnManager->UpdateFastForward();
return true;
}
bool CNetClient::OnRejoined(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_REJOINED);
CNetClient* client = static_cast(context);
CRejoinedMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "rejoined",
"guid", message->m_GUID);
return true;
}
bool CNetClient::OnKicked(void *context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_KICKED);
CNetClient* client = static_cast(context);
CKickedMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"username", message->m_Name,
"type", "kicked",
"banned", message->m_Ban != 0);
return true;
}
bool CNetClient::OnClientTimeout(void *context, CFsmEvent* event)
{
// Report the timeout of some other client
ENSURE(event->GetType() == (uint)NMT_CLIENT_TIMEOUT);
CNetClient* client = static_cast(context);
CClientTimeoutMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "netwarn",
"warntype", "client-timeout",
"guid", message->m_GUID,
"lastReceivedTime", message->m_LastReceivedTime);
return true;
}
bool CNetClient::OnClientPerformance(void *context, CFsmEvent* event)
{
// Performance statistics for one or multiple clients
ENSURE(event->GetType() == (uint)NMT_CLIENT_PERFORMANCE);
CNetClient* client = static_cast(context);
CClientPerformanceMessage* message = static_cast(event->GetParamRef());
// Display warnings for other clients with bad ping
for (size_t i = 0; i < message->m_Clients.size(); ++i)
{
if (message->m_Clients[i].m_MeanRTT < NETWORK_BAD_PING || message->m_Clients[i].m_GUID == client->m_GUID)
continue;
client->PushGuiMessage(
"type", "netwarn",
"warntype", "client-latency",
"guid", message->m_Clients[i].m_GUID,
"meanRTT", message->m_Clients[i].m_MeanRTT);
}
return true;
}
bool CNetClient::OnClientsLoading(void *context, CFsmEvent *event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENTS_LOADING);
CNetClient* client = static_cast(context);
CClientsLoadingMessage* message = static_cast(event->GetParamRef());
std::vector guids;
guids.reserve(message->m_Clients.size());
for (const CClientsLoadingMessage::S_m_Clients& mClient : message->m_Clients)
guids.push_back(mClient.m_GUID);
client->PushGuiMessage(
"type", "clients-loading",
"guids", guids);
return true;
}
bool CNetClient::OnClientPaused(void *context, CFsmEvent *event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
CNetClient* client = static_cast(context);
CClientPausedMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "paused",
"pause", message->m_Pause != 0,
"guid", message->m_GUID);
return true;
}
bool CNetClient::OnLoadedGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetClient* client = static_cast(context);
// All players have loaded the game - start running the turn manager
// so that the game begins
client->m_Game->SetTurnManager(client->m_ClientTurnManager);
client->PushGuiMessage(
"type", "netstatus",
"status", "active");
// If we have rejoined an in progress game, send the rejoined message to the server.
if (client->m_Rejoin)
client->SendRejoinedMessage();
return true;
}
bool CNetClient::OnInGame(void *context, CFsmEvent* event)
{
// TODO: should split each of these cases into a separate method
CNetClient* client = static_cast(context);
CNetMessage* message = static_cast(event->GetParamRef());
if (message)
{
if (message->GetType() == NMT_SIMULATION_COMMAND)
{
CSimulationMessage* simMessage = static_cast (message);
client->m_ClientTurnManager->OnSimulationMessage(simMessage);
}
else if (message->GetType() == NMT_SYNC_ERROR)
{
CSyncErrorMessage* syncMessage = static_cast (message);
client->m_ClientTurnManager->OnSyncError(syncMessage->m_Turn, syncMessage->m_HashExpected, syncMessage->m_PlayerNames);
}
else if (message->GetType() == NMT_END_COMMAND_BATCH)
{
CEndCommandBatchMessage* endMessage = static_cast (message);
client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength);
}
}
return true;
}
Index: ps/trunk/source/network/NetEnet.cpp
===================================================================
--- ps/trunk/source/network/NetEnet.cpp (nonexistent)
+++ ps/trunk/source/network/NetEnet.cpp (revision 27599)
@@ -0,0 +1,62 @@
+/* Copyright (C) 2023 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 "NetEnet.h"
+
+#include "ps/ConfigDB.h"
+#include "ps/containers/Span.h"
+
+namespace PS
+{
+
+namespace Enet
+{
+
+// ENet protocol MTU
+// ENet by default uses 1400 as ENet packet fragment max size, adding ICMP and
+// IPv4 headers this may exceed the MTU for some VPN solutions [1], so provide
+// a lower default.
+// MTU negotiation server-side needs [2], which was merged after enet-1.3.17,
+// so the user configured value may be ignored on older versions.
+// [1] https://github.com/lsalzman/enet/issues/132
+// [2] https://github.com/lsalzman/enet/pull/222
+constexpr enet_uint32 HOST_DEFAULT_MTU = 1372;
+
+ENetHost* CreateHost(const ENetAddress* address, size_t peerCount, size_t channelLimit)
+{
+ // TODO: Maybe allow user to set rate limits?
+
+ ENetHost* host = enet_host_create(address, peerCount, channelLimit, 0, 0);
+ if (!host)
+ return nullptr;
+
+ // Public ENet API doesn't offer a means to change MTU, so do it in a
+ // way least likely to break with ENet updates.
+ enet_uint32 mtu = HOST_DEFAULT_MTU;
+ CFG_GET_VAL("network.enetmtu", mtu);
+ host->mtu = mtu;
+ for (ENetPeer& p : PS::span{host->peers, host->peerCount})
+ enet_peer_reset(&p);
+
+ return host;
+}
+
+} // namespace Enet
+
+} // namespace PS
Property changes on: ps/trunk/source/network/NetEnet.cpp
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/source/network/NetEnet.h
===================================================================
--- ps/trunk/source/network/NetEnet.h (nonexistent)
+++ ps/trunk/source/network/NetEnet.h (revision 27599)
@@ -0,0 +1,44 @@
+/* Copyright (C) 2023 Wildfire Games.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef INCLUDED_NETENET
+#define INCLUDED_NETENET
+
+#include "lib/external_libraries/enet.h"
+
+namespace PS
+{
+
+namespace Enet
+{
+
+/**
+ * Wrapper for enet_host_create setting default values and custom mtu, taking
+ * care of user configuration.
+ */
+ENetHost* CreateHost(const ENetAddress* address, size_t peerCount, size_t channelLimit);
+
+} // namespace Enet
+
+} // namespace PS
+
+#endif // #ifndef INCLUDED_NETENET
Property changes on: ps/trunk/source/network/NetEnet.h
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/source/network/NetServer.cpp
===================================================================
--- ps/trunk/source/network/NetServer.cpp (revision 27598)
+++ ps/trunk/source/network/NetServer.cpp (revision 27599)
@@ -1,1748 +1,1749 @@
-/* Copyright (C) 2022 Wildfire Games.
+/* Copyright (C) 2023 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 "NetEnet.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/Hashing.h"
#include "ps/Profile.h"
#include "ps/Threading.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/JSON.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 = Script::StringifyJSON(ScriptRequest(m_Server.GetScriptInterface()), &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) :
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::CheckPassword(const std::string& password, const std::string& salt) const
{
return HashCryptographically(m_Password, salt) == password;
}
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);
+ m_Host = PS::Enet::CreateHost(&addr, MAX_CLIENTS, CHANNEL_COUNT);
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.
std::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;
// 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);
Script::ParseJSON(rq, 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: 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. Don't do this in pregame however,
// as that ID might be invalid for various reasons.
i32 playerID = -1;
if (m_State != SERVER_STATE_UNCONNECTED && m_State != SERVER_STATE_PREGAME)
{
// 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;
}
}
}
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.
// NB: m_Name must match the client's salt, @see CNetClient::SetGamePassword
if (!server.CheckPassword(message->m_Password, message->m_Name.ToUTF8()))
{
// 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(
std::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);
Script::GetProperty(rq, server.m_InitAttributes, "settings", &settings);
if (Script::HasProperty(rq, settings, "CheatsEnabled"))
Script::GetProperty(rq, 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
// 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)
{
// 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.
Script::ParseJSON(ScriptRequest(m_ScriptInterface), 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) :
m_Worker(new CNetServerWorker(useLobbyAuth)),
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);
}
CStr CNetServer::GetPublicIp() const
{
return m_PublicIp;
}
u16 CNetServer::GetPublicPort() const
{
return m_PublicPort;
}
u16 CNetServer::GetLocalPort() const
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
if (!m_Worker->m_Host)
return 0;
return m_Worker->m_Host->address.port;
}
void CNetServer::SetConnectionData(const CStr& ip, const u16 port)
{
m_PublicIp = ip;
m_PublicPort = port;
m_UseSTUN = false;
}
bool CNetServer::SetConnectionDataViaSTUN()
{
m_UseSTUN = true;
std::lock_guard lock(m_Worker->m_WorkerMutex);
if (!m_Worker->m_Host)
return false;
return StunClient::FindPublicIP(*m_Worker->m_Host, m_PublicIp, m_PublicPort);
}
bool CNetServer::CheckPasswordAndIncrement(const std::string& username, const std::string& password, const std::string& salt)
{
std::unordered_map::iterator it = m_FailedAttempts.find(username);
if (m_Worker->CheckPassword(password, salt))
{
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 ScriptRequest& rq)
{
// Pass the attributes as JSON, since that's the easiest safe
// cross-thread way of passing script data
std::string attrsJSON = Script::StringifyJSON(rq, 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/NetSession.cpp
===================================================================
--- ps/trunk/source/network/NetSession.cpp (revision 27598)
+++ ps/trunk/source/network/NetSession.cpp (revision 27599)
@@ -1,283 +1,280 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2023 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "NetSession.h"
#include "NetClient.h"
+#include "NetEnet.h"
#include "NetMessage.h"
#include "NetServer.h"
#include "NetStats.h"
#include "ps/CLogger.h"
#include "ps/Profile.h"
constexpr int NETCLIENT_POLL_TIMEOUT = 50;
constexpr int CHANNEL_COUNT = 1;
CNetClientSession::CNetClientSession(CNetClient& client) :
m_Client(client), m_FileTransferer(this), m_Host(nullptr), m_Server(nullptr),
m_Stats(nullptr), m_IncomingMessages(16), m_OutgoingMessages(16),
m_LoopRunning(false), m_ShouldShutdown(false), m_MeanRTT(0), m_LastReceivedTime(0)
{
}
CNetClientSession::~CNetClientSession()
{
ENSURE(!m_LoopRunning);
delete m_Stats;
if (m_Host && m_Server)
{
// Disconnect immediately (we can't wait for acks)
enet_peer_disconnect_now(m_Server, NDR_SERVER_SHUTDOWN);
enet_host_destroy(m_Host);
m_Host = NULL;
m_Server = NULL;
}
}
bool CNetClientSession::Connect(const CStr& server, const u16 port, ENetHost* enetClient)
{
ENSURE(!m_LoopRunning);
ENSURE(!m_Host);
ENSURE(!m_Server);
- // Create ENet host
- ENetHost* host;
- if (enetClient != nullptr)
- host = enetClient;
- else
- host = enet_host_create(NULL, 1, CHANNEL_COUNT, 0, 0);
+ // Create ENet host if necessary.
+ ENetHost* host = enetClient != nullptr ? enetClient : PS::Enet::CreateHost(nullptr, 1, CHANNEL_COUNT);
if (!host)
return false;
// Bind to specified host
ENetAddress addr;
addr.port = port;
if (enet_address_set_host(&addr, server.c_str()) < 0)
return false;
// Initiate connection to server
ENetPeer* peer = enet_host_connect(host, &addr, CHANNEL_COUNT, 0);
if (!peer)
return false;
m_Host = host;
m_Server = peer;
m_Stats = new CNetStatsTable(m_Server);
if (CProfileViewer::IsInitialised())
g_ProfileViewer.AddRootTable(m_Stats);
return true;
}
void CNetClientSession::RunNetLoop(CNetClientSession* session)
{
ENSURE(!session->m_LoopRunning);
session->m_LoopRunning = true;
debug_SetThreadName("NetClientSession loop");
while (!session->m_ShouldShutdown)
{
ENSURE(session->m_Host && session->m_Server);
session->m_FileTransferer.Poll();
session->Poll();
session->Flush();
session->m_LastReceivedTime = enet_time_get() - session->m_Server->lastReceiveTime;
session->m_MeanRTT = session->m_Server->roundTripTime;
}
session->m_LoopRunning = false;
// Deleting the session is handled in this thread as it might outlive the CNetClient.
SAFE_DELETE(session);
}
void CNetClientSession::Shutdown()
{
m_ShouldShutdown = true;
}
void CNetClientSession::Poll()
{
ENetEvent event;
// Use the timeout to make the thread wait and save CPU time.
if (enet_host_service(m_Host, &event, NETCLIENT_POLL_TIMEOUT) <= 0)
return;
if (event.type == ENET_EVENT_TYPE_CONNECT)
{
ENSURE(event.peer == m_Server);
// Report the server address immediately.
char hostname[256] = "(error)";
enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
LOGMESSAGE("Net client: Connected to %s:%u", hostname, (unsigned int)event.peer->address.port);
m_Connected = true;
m_IncomingMessages.push(event);
}
else if (event.type == ENET_EVENT_TYPE_DISCONNECT)
{
ENSURE(event.peer == m_Server);
// Report immediately.
LOGMESSAGE("Net client: Disconnected");
m_Connected = false;
m_IncomingMessages.push(event);
}
else if (event.type == ENET_EVENT_TYPE_RECEIVE)
m_IncomingMessages.push(event);
}
void CNetClientSession::Flush()
{
ENetPacket* packet;
while (m_OutgoingMessages.pop(packet))
if (enet_peer_send(m_Server, CNetHost::DEFAULT_CHANNEL, packet) < 0)
{
// Report the error, but do so silently if we know we are disconnected.
if (m_Connected)
LOGERROR("NetClient: Failed to send packet to server");
else
LOGMESSAGE("NetClient: Failed to send packet to server");
}
enet_host_flush(m_Host);
}
void CNetClientSession::ProcessPolledMessages()
{
ENetEvent event;
while(m_IncomingMessages.pop(event))
{
if (event.type == ENET_EVENT_TYPE_CONNECT)
m_Client.HandleConnect();
else if (event.type == ENET_EVENT_TYPE_DISCONNECT)
{
// This deletes the session, so we must break;
m_Client.HandleDisconnect(event.data);
break;
}
else if (event.type == ENET_EVENT_TYPE_RECEIVE)
{
CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, m_Client.GetScriptInterface());
if (msg)
{
LOGMESSAGE("Net client: Received message %s of size %lu from server", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength());
m_Client.HandleMessage(msg);
}
// Thread-safe
enet_packet_destroy(event.packet);
}
}
}
bool CNetClientSession::SendMessage(const CNetMessage* message)
{
ENSURE(m_Host && m_Server);
// Thread-safe.
ENetPacket* packet = CNetHost::CreatePacket(message);
if (!packet)
return false;
if (!m_OutgoingMessages.push(packet))
{
LOGERROR("NetClient: Failed to push message on the outgoing queue.");
return false;
}
return true;
}
u32 CNetClientSession::GetLastReceivedTime() const
{
if (!m_Server)
return 0;
return m_LastReceivedTime;
}
u32 CNetClientSession::GetMeanRTT() const
{
if (!m_Server)
return 0;
return m_MeanRTT;
}
CNetServerSession::CNetServerSession(CNetServerWorker& server, ENetPeer* peer) :
m_Server(server), m_FileTransferer(this), m_Peer(peer), m_HostID(0), m_GUID(), m_UserName()
{
}
u32 CNetServerSession::GetIPAddress() const
{
return m_Peer->address.host;
}
u32 CNetServerSession::GetLastReceivedTime() const
{
if (!m_Peer)
return 0;
return enet_time_get() - m_Peer->lastReceiveTime;
}
u32 CNetServerSession::GetMeanRTT() const
{
if (!m_Peer)
return 0;
return m_Peer->roundTripTime;
}
void CNetServerSession::Disconnect(NetDisconnectReason reason)
{
if (reason == NDR_UNKNOWN)
LOGWARNING("Disconnecting client without communicating the disconnect reason!");
Update((uint)NMT_CONNECTION_LOST, NULL);
enet_peer_disconnect(m_Peer, static_cast(reason));
}
void CNetServerSession::DisconnectNow(NetDisconnectReason reason)
{
if (reason == NDR_UNKNOWN)
LOGWARNING("Disconnecting client without communicating the disconnect reason!");
enet_peer_disconnect_now(m_Peer, static_cast(reason));
}
bool CNetServerSession::SendMessage(const CNetMessage* message)
{
return m_Server.SendMessage(m_Peer, message);
}