Index: ps/trunk/binaries/data/config/default.cfg =================================================================== --- ps/trunk/binaries/data/config/default.cfg (revision 24517) +++ ps/trunk/binaries/data/config/default.cfg (revision 24518) @@ -1,539 +1,538 @@ ; Global Configuration Settings ; ; ************************************************************** ; * DO NOT EDIT THIS FILE if you want personal customisations: * ; * create a text file called "local.cfg" instead, and copy * ; * the lines from this file that you want to change. * ; * * ; * If a setting is part of a section (for instance [hotkey]) * ; * you need to append the section name at the beginning of * ; * your custom line (for instance you need to write * ; * "hotkey.pause = Space" if you want to change the pausing * ; * hotkey to the spacebar). * ; * * ; * On Linux, create: * ; * $XDG_CONFIG_HOME/0ad/config/local.cfg * ; * (Note: $XDG_CONFIG_HOME defaults to ~/.config) * ; * * ; * On OS X, create: * ; * ~/Library/Application\ Support/0ad/config/local.cfg * ; * * ; * On Windows, create: * ; * %appdata%\0ad\config\local.cfg * ; * * ; ************************************************************** ; Enable/disable windowed mode by default. (Use Alt+Enter to toggle in the game.) windowed = false ; Show detailed tooltips (Unit stats) showdetailedtooltips = false ; Pause the game on window focus loss (Only applicable to single player mode) pauseonfocusloss = true ; Persist settings after leaving the game setup screen persistmatchsettings = true ; Default player name to use in multiplayer ; playername = "anonymous" ; Default server name or IP to use in multiplayer multiplayerserver = "127.0.0.1" ; Force a particular resolution. (If these are 0, the default is ; to keep the current desktop resolution in fullscreen mode or to ; use 1024x768 in windowed mode.) xres = 0 yres = 0 ; Force a non-standard bit depth (if 0 then use the current desktop bit depth) bpp = 0 ; Preferred display (for multidisplay setups, only works with SDL 2.0) display = 0 ; Emulate right-click with Ctrl+Click on Mac mice macmouse = false ; System settings: ; if false, actors won't be rendered but anything entity will be. renderactors = true watereffects=true ; When disabled, force usage of the fixed pipeline water. This is faster, but really, really ugly. waterfancyeffects = false waterrealdepth = true waterrefraction = true waterreflection = true shadowsonwater = false shadows = true shadowquality = 0 ; Shadow map resolution. (-2 - Very Low, -1 - Low, 0 - Medium, 1 - High, 2 - Very High) ; High values can crash the game when using a graphics card with low memory! shadowpcf = true shadowsfixed = false ; When enabled shadows are rendered only on the shadowsfixeddistance = 300.0 ; fixed distance and without swimming effect. vsync = false particles = true fog = true silhouettes = true showsky = true novbo = false ; Disable hardware cursors nohwcursor = false ; Specify the render path. This can be one of: ; default Automatically select one of the below, depending on system capabilities ; fixed Only use OpenGL fixed function pipeline ; shader Use vertex/fragment shaders for transform and lighting where possible ; Using 'fixed' instead of 'default' may work around some graphics-related problems, ; but will reduce performance and features when a modern graphics card is available. renderpath = default ;;;;; EXPERIMENTAL ;;;;; ; Prefer GLSL shaders over ARB shaders. Allows fancier graphical effects. preferglsl = false ; Experimental probably-non-working GPU skinning support; requires preferglsl; use at own risk gpuskinning = false ; Use smooth LOS interpolation smoothlos = false ; Use screen-space postprocessing filters (HDR, bloom, DOF, etc). Incompatible with fixed renderpath. postproc = false ; Use anti-aliasing techniques. antialiasing = "disabled" ; Use sharpening techniques. sharpening = "disabled" sharpness = 0.3 ; Quality level of shader effects (set to 10 to display all effects) materialmgr.quality = 2.0 ; Maximum distance to display parallax effect. Set to 0 to disable parallax. materialmgr.PARALLAX_DIST.max = 150 ; Maximum distance to display high quality parallax effect. materialmgr.PARALLAX_HQ_DIST.max = 75 ; Maximum distance to display very high quality parallax effect. Set to 30 to enable. materialmgr.PARALLAX_VHQ_DIST.max = 0 ;;;;;;;;;;;;;;;;;;;;;;;; ; Replace alpha-blending with alpha-testing, for performance experiments forcealphatest = false ; Color of the sky (in "r g b" format) skycolor = "0 0 0" [adaptivefps] session = 60 ; Throttle FPS in running games (prevents 100% CPU workload). menu = 60 ; Throttle FPS in menus only. [profiler2] server = "127.0.0.1" server.port = "8000" ; Use a free port on your machine. server.threads = "6" ; Enough for the browser's parallel connection limit [hotkey] ; Each one of the specified keys will trigger the action on the left ; for multiple-key combinations, separate keys with '+'. ; See keys.txt for the list of key names. ; > SYSTEM SETTINGS exit = "Ctrl+Break", "Super+Q", "Alt+F4" ; Exit to desktop cancel = Escape ; Close or cancel the current dialog box/popup confirm = Return ; Confirm the current command pause = Pause ; Pause/unpause game screenshot = F2 ; Take PNG screenshot bigscreenshot = "Shift+F2" ; Take large BMP screenshot togglefullscreen = "Alt+Return" ; Toggle fullscreen/windowed mode screenshot.watermark = "Alt+K" ; Toggle product/company watermark for official screenshots wireframe = "Alt+Shift+W" ; Toggle wireframe mode silhouettes = "Alt+Shift+S" ; Toggle unit silhouettes showsky = "Alt+Z" ; Toggle sky ; > DIALOG HOTKEYS summary = "Ctrl+Tab" ; Toggle in-game summary lobby = "Alt+L" ; Show the multiplayer lobby in a dialog window. structree = "Alt+Shift+T" ; Show structure tree civinfo = "Alt+Shift+H" ; Show civilization info ; > CLIPBOARD CONTROLS copy = "Ctrl+C" ; Copy to clipboard paste = "Ctrl+V" ; Paste from clipboard cut = "Ctrl+X" ; Cut selected text and copy to the clipboard ; > CONSOLE SETTINGS console.toggle = BackQuote, F9 ; Open/close console ; > OVERLAY KEYS fps.toggle = "Alt+F" ; Toggle frame counter realtime.toggle = "Alt+T" ; Toggle current display of computer time timeelapsedcounter.toggle = "F12" ; Toggle time elapsed counter ceasefirecounter.toggle = unused ; 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 = unused ; Focus the camera on the rally point of the selected building zoom.in = Plus, NumPlus ; Zoom camera in (continuous control) zoom.out = Minus, NumMinus ; Zoom camera out (continuous control) zoom.wheel.in = WheelUp ; Zoom camera in (stepped control) zoom.wheel.out = WheelDown ; Zoom camera out (stepped control) rotate.up = "Ctrl+UpArrow", "Ctrl+W" ; Rotate camera to look upwards rotate.down = "Ctrl+DownArrow", "Ctrl+S" ; Rotate camera to look downwards rotate.cw = "Ctrl+LeftArrow", "Ctrl+A", Q ; Rotate camera clockwise around terrain rotate.ccw = "Ctrl+RightArrow", "Ctrl+D", E ; Rotate camera anticlockwise around terrain rotate.wheel.cw = "Shift+WheelUp", MouseX1 ; Rotate camera clockwise around terrain (stepped control) rotate.wheel.ccw = "Shift+WheelDown", MouseX2 ; Rotate camera anticlockwise around terrain (stepped control) pan = MouseMiddle ; Enable scrolling by moving mouse left = A, LeftArrow ; Scroll or rotate left right = D, RightArrow ; Scroll or rotate right up = W, UpArrow ; Scroll or rotate up/forwards down = S, DownArrow ; Scroll or rotate down/backwards scroll.speed.increase = "Ctrl+Shift+S" ; Increase scroll speed scroll.speed.decrease = "Ctrl+Alt+S" ; Decrease scroll speed rotate.speed.increase = "Ctrl+Shift+R" ; Increase rotation speed rotate.speed.decrease = "Ctrl+Alt+R" ; Decrease rotation speed zoom.speed.increase = "Ctrl+Shift+Z" ; Increase zoom speed zoom.speed.decrease = "Ctrl+Alt+Z" ; Decrease zoom speed [hotkey.camera.jump] 1 = F5 ; Jump to position N 2 = F6 3 = F7 4 = F8 ;5 = ;6 = ;7 = ;8 = ;9 = ;10 = [hotkey.camera.jump.set] 1 = "Ctrl+F5" ; Set jump position N 2 = "Ctrl+F6" 3 = "Ctrl+F7" 4 = "Ctrl+F8" ;5 = ;6 = ;7 = ;8 = ;9 = ;10 = [hotkey.profile] toggle = "F11" ; Enable/disable real-time profiler save = "Shift+F11" ; Save current profiler data to logs/profile.txt [hotkey.profile2] toggle = "Ctrl+F11" ; Enable/disable HTTP/GPU modes for new profiler [hotkey.selection] cancel = Esc ; Un-select all units and cancel building placement add = Shift ; Add units to selection militaryonly = Alt ; Add only military units to the selection nonmilitaryonly = "Alt+Y" ; Add only non-military units to the selection idleonly = "I" ; Select only idle units woundedonly = "O" ; Select only wounded units remove = Ctrl ; Remove units from selection idleworker = Period, NumDecimal ; Select next idle worker idlewarrior = Slash, NumDivide ; Select next idle warrior idleunit = BackSlash ; Select next idle unit offscreen = Alt ; Include offscreen units in selection [hotkey.selection.group.add] 0 = "Shift+0", "Shift+Num0" 1 = "Shift+1", "Shift+Num1" 2 = "Shift+2", "Shift+Num2" 3 = "Shift+3", "Shift+Num3" 4 = "Shift+4", "Shift+Num4" 5 = "Shift+5", "Shift+Num5" 6 = "Shift+6", "Shift+Num6" 7 = "Shift+7", "Shift+Num7" 8 = "Shift+8", "Shift+Num8" 9 = "Shift+9", "Shift+Num9" [hotkey.selection.group.save] 0 = "Ctrl+0", "Ctrl+Num0" 1 = "Ctrl+1", "Ctrl+Num1" 2 = "Ctrl+2", "Ctrl+Num2" 3 = "Ctrl+3", "Ctrl+Num3" 4 = "Ctrl+4", "Ctrl+Num4" 5 = "Ctrl+5", "Ctrl+Num5" 6 = "Ctrl+6", "Ctrl+Num6" 7 = "Ctrl+7", "Ctrl+Num7" 8 = "Ctrl+8", "Ctrl+Num8" 9 = "Ctrl+9", "Ctrl+Num9" [hotkey.selection.group.select] 0 = 0, Num0 1 = 1, Num1 2 = 2, Num2 3 = 3, Num3 4 = 4, Num4 5 = 5, Num5 6 = 6, Num6 7 = 7, Num7 8 = 8, Num8 9 = 9, Num9 [hotkey.gamesetup] mapbrowser.open = "M" [hotkey.session] kill = Delete, Backspace ; Destroy selected units stop = "H" ; Stop the current action backtowork = "Y" ; The unit will go back to work unload = "U" ; Unload garrisoned units when a building/mechanical unit is selected move = unused ; Modifier to move to a point instead of another action (e.g. gather) attack = Ctrl ; Modifier to attack instead of another action (e.g. capture) attackmove = Ctrl ; Modifier to attackmove when clicking on a point attackmoveUnit = "Ctrl+Q" ; Modifier to attackmove targeting only units when clicking on a point (should contain the attackmove keys) garrison = Ctrl ; Modifier to garrison when clicking on building autorallypoint = Ctrl ; Modifier to set the rally point on the building itself guard = "G" ; Modifier to escort/guard when clicking on unit/building patrol = "P" ; Modifier to patrol a unit repair = "J" ; Modifier to repair when clicking on building/mechanical unit queue = Shift ; Modifier to queue unit orders instead of replacing orderone = Alt ; Modifier to order only one entity in selection. batchtrain = Shift ; Modifier to train units in batches massbarter = Shift ; Modifier to barter bunch of resources masstribute = Shift ; Modifier to tribute bunch of resources noconfirmation = Shift ; Do not ask confirmation when deleting a building/unit fulltradeswap = Shift ; Modifier to put the desired trade resource to 100% unloadtype = Shift ; Modifier to unload all units of type deselectgroup = Ctrl ; Modifier to deselect units when clicking group icon, instead of selecting rotate.cw = RightBracket ; Rotate building placement preview clockwise rotate.ccw = LeftBracket ; Rotate building placement preview anticlockwise snaptoedges = Ctrl ; Modifier to align new structures with nearby existing structure ; Overlays showstatusbars = Tab ; Toggle display of status bars devcommands.toggle = "Alt+D" ; Toggle developer commands panel highlightguarding = PageDown ; Toggle highlight of guarding units highlightguarded = PageUp ; Toggle highlight of guarded units diplomacycolors = "Alt+X" ; Toggle diplomacy colors toggleattackrange = "Alt+C" ; Toggle display of attack range overlays of selected defensive structures toggleaurasrange = "Alt+V" ; Toggle display of aura range overlays of selected units and structures togglehealrange = "Alt+B" ; Toggle display of heal range overlays of selected units [hotkey.session.gui] toggle = "Alt+G" ; Toggle visibility of session GUI menu.toggle = "F10" ; Toggle in-game menu diplomacy.toggle = "Ctrl+H" ; Toggle in-game diplomacy page barter.toggle = "Ctrl+B" ; Toggle in-game barter/trade page objectives.toggle = "Ctrl+O" ; Toggle in-game objectives page tutorial.toggle = "Ctrl+P" ; Toggle in-game tutorial panel [hotkey.session.savedgames] delete = Delete, Backspace ; Delete the selected saved game asking confirmation noconfirmation = Shift ; Do not ask confirmation when deleting a game [hotkey.session.queueunit] ; > UNIT TRAINING 1 = "Z" ; add first unit type to queue 2 = "X" ; add second unit type to queue 3 = "C" ; add third unit type to queue 4 = "V" ; add fourth unit type to queue 5 = "B" ; add fivth unit type to queue 6 = "N" ; add sixth unit type to queue 7 = "M" ; add seventh unit type to queue 8 = Comma ; add eighth unit type to queue [hotkey.session.timewarp] fastforward = Space ; If timewarp mode enabled, speed up the game rewind = "Shift+Backspace" ; If timewarp mode enabled, go back to earlier point in the game [hotkey.tab] next = "Tab", "Alt+S" ; Show the next tab prev = "Shift+Tab", "Alt+W" ; Show the previous tab [hotkey.text] ; > GUI TEXTBOX HOTKEYS delete.left = "Ctrl+Backspace" ; Delete word to the left of cursor delete.right = "Ctrl+Del" ; Delete word to the right of cursor move.left = "Ctrl+LeftArrow" ; Move cursor to start of word to the left of cursor move.right = "Ctrl+RightArrow" ; Move cursor to start of word to the right of cursor [gui] cursorblinkrate = 0.5 ; Cursor blink rate in seconds (0.0 to disable blinking) scale = 1.0 ; GUI scaling factor, for improved compatibility with 4K displays [gui.gamesetup] enabletips = true ; Enable/Disable tips during gamesetup (for newcomers) assignplayers = everyone ; Whether to assign joining clients to free playerslots. Possible values: everyone, buddies, disabled. aidifficulty = 3 ; Difficulty level, from 0 (easiest) to 5 (hardest) aibehavior = "random" ; Default behavior of the AI (random, balanced, aggressive or defensive) settingsslide = true ; Enable/Disable settings panel slide [gui.loadingscreen] progressdescription = false ; Whether to display the progress percent or a textual description [gui.session] camerajump.threshold = 40 ; How close do we have to be to the actual location in order to jump back to the previous one? timeelapsedcounter = false ; Show the game duration in the top right corner ceasefirecounter = false ; Show the remaining ceasefire time in the top right corner batchtrainingsize = 5 ; Number of units to be trained per batch by default (when pressing the hotkey) scrollbatchratio = 1 ; Number of times you have to scroll to increase/decrease the batchsize by 1 woundedunithotkeythreshold = 33 ; The wounded unit hotkey considers the selected units as wounded if their health percentage falls below this number attackrange = true ; Display attack range overlays of selected defensive structures aurasrange = true ; Display aura range overlays of selected units and structures healrange = true ; Display heal range overlays of selected units rankabovestatusbar = true ; Show rank icons above status bars experiencestatusbar = true ; Show an experience status bar above each selected unit respoptooltipsort = 0 ; Sorting players in the resources and population tooltip by value (0 - no sort, -1 - ascending, 1 - descending) snaptoedges = "disabled" ; Possible values: disabled, enabled. snaptoedgesdistancethreshold = 15 ; On which distance we don't snap to edges disjointcontrolgroups = "true" ; Whether control groups are disjoint sets or entities can be in multiple control groups at the same time. defaultformation = "special/formations/box" ; For walking orders, automatically put units into this formation if they don't have one already. [gui.session.minimap] blinkduration = 1.7 ; The blink duration while pinging pingduration = 50.0 ; The duration for which an entity will be pinged after an attack notification [gui.session.notifications] attack = true ; Show a chat notification if you are attacked by another player tribute = true ; Show a chat notification if an ally tributes resources to another team member if teams are locked, and all tributes in observer mode barter = true ; Show a chat notification to observers when a player bartered resources phase = completed ; Show a chat notification if you or an ally have started, aborted or completed a new phase, and phases of all players in observer mode. Possible values: none, completed, all. [gui.splashscreen] enable = true ; Enable/disable the splashscreen version = 0 ; Splashscreen version (date of last modification). By default, 0 to force splashscreen to appear at first launch [gui.session.diplomacycolors] self = "21 55 149" ; Color of your units when diplomacy colors are enabled ally = "86 180 31" ; Color of allies when diplomacy colors are enabled neutral = "231 200 5" ; Color of neutral players when diplomacy colors are enabled enemy = "150 20 20" ; Color of enemies when diplomacy colors are enabled [joystick] ; EXPERIMENTAL: joystick/gamepad settings enable = false deadzone = 8192 [joystick.camera] pan.x = 0 pan.y = 1 rotate.x = 3 rotate.y = 2 zoom.in = 5 zoom.out = 4 [chat] timestamp = true ; Show at which time chat messages have been sent [chat.session] extended = true ; Whether to display the chat history [lobby] history = 0 ; Number of past messages to display on join room = "arena24" ; 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 = "wfgbot24" ; Name of the server-side XMPP-account that manage games echelon = "echelon24" ; 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 = "RWTsHxQMrRq4xwHisyBa2rNQfAedcINzbTT83jeX4/ZcfVxqLfWB4y8w" ; 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 -gamestarttimeout = 60000 ; Don't disconnect clients timing out in the loading screen and rejoin process before exceeding this timeout. [overlay] fps = "false" ; Show frames per second in top right corner realtime = "false" ; Show current system time in top right corner netwarnings = "true" ; Show warnings if the network connection is bad [profiler2] autoenable = false ; Enable HTTP server output at startup (default off for security/performance) gpu.arb.enable = true ; Allow GL_ARB_timer_query timing mode when available gpu.ext.enable = true ; Allow GL_EXT_timer_query timing mode when available gpu.intel.enable = true ; Allow GL_INTEL_performance_queries timing mode when available [rlinterface] address = "127.0.0.1:6000" [sound] mastergain = 0.9 musicgain = 0.2 ambientgain = 0.6 actiongain = 0.7 uigain = 0.7 [sound.notify] nick = true ; Play a sound when someone mentions your name in the lobby or game gamesetup.join = false ; Play a sound when a new client joins the game setup [tinygettext] debug = false ; Print error messages each time a translation for an English string is not found. [userreport] ; Opt-in online user reporting system url_upload = "https://feedback.wildfiregames.com/report/upload/v1/" ; URL where UserReports are uploaded to url_publication = "https://feedback.wildfiregames.com/" ; URL where UserReports were analyzed and published url_terms = "https://trac.wildfiregames.com/browser/ps/trunk/binaries/data/mods/public/gui/userreport/Terms_and_Conditions.txt"; Allows the user to save the text and print the terms terms = "0" ; Version (hash) of the UserReporter Terms that the user has accepted [view] ; Camera control settings scroll.speed = 120.0 scroll.speed.modifier = 1.05 ; Multiplier for changing scroll speed rotate.x.speed = 1.2 rotate.x.min = 28.0 rotate.x.max = 60.0 rotate.x.default = 35.0 rotate.y.speed = 2.0 rotate.y.speed.wheel = 0.45 rotate.y.default = 0.0 rotate.speed.modifier = 1.05 ; Multiplier for changing rotation speed drag.speed = 0.5 zoom.speed = 256.0 zoom.speed.wheel = 32.0 zoom.min = 50.0 zoom.max = 200.0 zoom.default = 120.0 zoom.speed.modifier = 1.05 ; Multiplier for changing zoom speed pos.smoothness = 0.1 zoom.smoothness = 0.4 rotate.x.smoothness = 0.5 rotate.y.smoothness = 0.3 near = 2.0 ; Near plane distance far = 4096.0 ; Far plane distance fov = 45.0 ; Field of view (degrees), lower is narrow, higher is wide height.smoothness = 0.5 height.min = 16 Index: ps/trunk/build/premake/premake5.lua =================================================================== --- ps/trunk/build/premake/premake5.lua (revision 24517) +++ ps/trunk/build/premake/premake5.lua (revision 24518) @@ -1,1460 +1,1460 @@ newoption { trigger = "android", description = "Use non-working Android cross-compiling mode" } newoption { trigger = "atlas", description = "Include Atlas scenario editor projects" } newoption { trigger = "coverage", description = "Enable code coverage data collection (GCC only)" } newoption { trigger = "gles", description = "Use non-working OpenGL ES 2.0 mode" } newoption { trigger = "icc", description = "Use Intel C++ Compiler (Linux only; should use either \"--cc icc\" or --without-pch too, and then set CXX=icpc before calling make)" } newoption { trigger = "jenkins-tests", description = "Configure CxxTest to use the XmlPrinter runner which produces Jenkins-compatible output" } newoption { trigger = "minimal-flags", description = "Only set compiler/linker flags that are really needed. Has no effect on Windows builds" } newoption { trigger = "outpath", description = "Location for generated project files" } newoption { trigger = "with-system-mozjs", description = "Search standard paths for libmozjs60, instead of using bundled copy" } newoption { trigger = "with-system-nvtt", description = "Search standard paths for nvidia-texture-tools library, instead of using bundled copy" } newoption { trigger = "without-audio", description = "Disable use of OpenAL/Ogg/Vorbis APIs" } newoption { trigger = "without-lobby", description = "Disable the use of gloox and the multiplayer lobby" } newoption { trigger = "without-miniupnpc", description = "Disable use of miniupnpc for port forwarding" } newoption { trigger = "without-nvtt", description = "Disable use of NVTT" } newoption { trigger = "without-pch", description = "Disable generation and usage of precompiled headers" } newoption { trigger = "without-tests", description = "Disable generation of test projects" } -- Linux/BSD specific options newoption { trigger = "prefer-local-libs", description = "Prefer locally built libs. Any local libraries used must also be listed within a file within /etc/ld.so.conf.d so the dynamic linker can find them at runtime." } -- OS X specific options newoption { trigger = "macosx-version-min", description = "Set minimum required version of the OS X API, the build will possibly fail if an older SDK is used, while newer API functions will be weakly linked (i.e. resolved at runtime)" } newoption { trigger = "sysroot", description = "Set compiler system root path, used for building against a non-system SDK. For example /usr/local becomes SYSROOT/user/local" } -- Windows specific options newoption { trigger = "build-shared-glooxwrapper", description = "Rebuild glooxwrapper DLL for Windows. Requires the same compiler version that gloox was built with" } newoption { trigger = "use-shared-glooxwrapper", description = "Use prebuilt glooxwrapper DLL for Windows" } newoption { trigger = "large-address-aware", description = "Make the executable large address aware. Do not use for development, in order to spot memory issues easily" } -- Install options newoption { trigger = "bindir", description = "Directory for executables (typically '/usr/games'); default is to be relocatable" } newoption { trigger = "datadir", description = "Directory for data files (typically '/usr/share/games/0ad'); default is ../data/ relative to executable" } newoption { trigger = "libdir", description = "Directory for libraries (typically '/usr/lib/games/0ad'); default is ./ relative to executable" } -- Root directory of project checkout relative to this .lua file rootdir = "../.." dofile("extern_libs5.lua") -- detect compiler for non-Windows if os.istarget("macosx") then cc = "clang" elseif os.istarget("linux") and _OPTIONS["icc"] then cc = "icc" elseif not os.istarget("windows") then cc = os.getenv("CC") if cc == nil or cc == "" then local hasgcc = os.execute("which gcc > .gccpath") local f = io.open(".gccpath", "r") local gccpath = f:read("*line") f:close() os.execute("rm .gccpath") if gccpath == nil then cc = "clang" else cc = "gcc" end end end -- detect CPU architecture (simplistic, currently only supports x86, amd64 and ARM) arch = "x86" if _OPTIONS["android"] then arch = "arm" elseif os.istarget("windows") then if os.getenv("PROCESSOR_ARCHITECTURE") == "amd64" or os.getenv("PROCESSOR_ARCHITEW6432") == "amd64" then arch = "amd64" end else arch = os.getenv("HOSTTYPE") if arch == "x86_64" or arch == "amd64" then arch = "amd64" else os.execute(cc .. " -dumpmachine > .gccmachine.tmp") local f = io.open(".gccmachine.tmp", "r") local machine = f:read("*line") f:close() if string.find(machine, "x86_64") == 1 or string.find(machine, "amd64") == 1 then arch = "amd64" elseif string.find(machine, "i.86") == 1 then arch = "x86" elseif string.find(machine, "arm") == 1 then arch = "arm" elseif string.find(machine, "aarch64") == 1 then arch = "aarch64" elseif string.find(machine, "e2k") == 1 then arch = "e2k" else print("WARNING: Cannot determine architecture from GCC, assuming x86") end end end -- Test whether we need to link libexecinfo. -- This is mostly the case on musl systems, as well as on BSD systems : only glibc provides the -- backtrace symbols we require in the libc, for other libcs we use the libexecinfo library. local link_execinfo = false if os.istarget("bsd") then link_execinfo = true elseif os.istarget("linux") then local _, link_errorCode = os.outputof(cc .. " ./tests/execinfo.c -o /dev/null") if link_errorCode ~= 0 then link_execinfo = true end end -- Set up the Workspace workspace "pyrogenesis" targetdir(rootdir.."/binaries/system") libdirs(rootdir.."/binaries/system") if not _OPTIONS["outpath"] then error("You must specify the 'outpath' parameter") end location(_OPTIONS["outpath"]) configurations { "Release", "Debug" } source_root = rootdir.."/source/" -- default for most projects - overridden by local in others -- Rationale: projects should not have any additional include paths except for -- those required by external libraries. Instead, we should always write the -- full relative path, e.g. #include "maths/Vector3d.h". This avoids confusion -- ("which file is meant?") and avoids enormous include path lists. -- projects: engine static libs, main exe, atlas, atlas frontends, test. -------------------------------------------------------------------------------- -- project helper functions -------------------------------------------------------------------------------- function project_set_target(project_name) -- Note: On Windows, ".exe" is added on the end, on unices the name is used directly local obj_dir_prefix = _OPTIONS["outpath"].."/obj/"..project_name.."_" filter "Debug" objdir(obj_dir_prefix.."Debug") targetsuffix("_dbg") filter "Release" objdir(obj_dir_prefix.."Release") filter { } end function project_set_build_flags() editandcontinue "Off" if not _OPTIONS["minimal-flags"] then symbols "On" end if cc ~= "icc" and (os.istarget("windows") or not _OPTIONS["minimal-flags"]) then -- adds the -Wall compiler flag warnings "Extra" -- this causes far too many warnings/remarks on ICC end -- disable Windows debug heap, since it makes malloc/free hugely slower when -- running inside a debugger if os.istarget("windows") then debugenvs { "_NO_DEBUG_HEAP=1" } end filter "Debug" defines { "DEBUG" } filter "Release" if os.istarget("windows") or not _OPTIONS["minimal-flags"] then optimize "Speed" end defines { "NDEBUG", "CONFIG_FINAL=1" } filter { } if _OPTIONS["gles"] then defines { "CONFIG2_GLES=1" } end if _OPTIONS["without-audio"] then defines { "CONFIG2_AUDIO=0" } end if _OPTIONS["without-nvtt"] then defines { "CONFIG2_NVTT=0" } end if _OPTIONS["without-lobby"] then defines { "CONFIG2_LOBBY=0" } end if _OPTIONS["without-miniupnpc"] then defines { "CONFIG2_MINIUPNPC=0" } end -- required for the lowlevel library. must be set from all projects that use it, otherwise it assumes it is -- being used as a DLL (which is currently not the case in 0ad) defines { "LIB_STATIC_LINK" } -- Enable C++17 standard. filter "action:vs*" buildoptions { "/std:c++17" } filter "action:not vs*" buildoptions { "-std=c++17" } filter {} -- various platform-specific build flags if os.istarget("windows") then flags { "MultiProcessorCompile" } -- Since KB4088875 Windows 7 has a soft requirement for SSE2. -- Windows 8+ and Firefox ESR52 make it hard requirement. -- Finally since VS2012 it's enabled implicitely when not set. vectorextensions "SSE2" -- use native wchar_t type (not typedef to unsigned short) nativewchar "on" else -- *nix -- TODO, FIXME: This check is incorrect because it means that some additional flags will be added inside the "else" branch if the -- compiler is ICC and minimal-flags is specified (ticket: #2994) if cc == "icc" and not _OPTIONS["minimal-flags"] then buildoptions { "-w1", -- "-Wabi", -- "-Wp64", -- complains about OBJECT_TO_JSVAL which is annoying "-Wpointer-arith", "-Wreturn-type", -- "-Wshadow", "-Wuninitialized", "-Wunknown-pragmas", "-Wunused-function", "-wd1292" -- avoid lots of 'attribute "__nonnull__" ignored' } filter "Debug" buildoptions { "-O0" } -- ICC defaults to -O2 filter { } if os.istarget("macosx") then linkoptions { "-multiply_defined","suppress" } end else -- exclude most non-essential build options for minimal-flags if not _OPTIONS["minimal-flags"] then buildoptions { -- enable most of the standard warnings "-Wno-switch", -- enumeration value not handled in switch (this is sometimes useful, but results in lots of noise) "-Wno-reorder", -- order of initialization list in constructors (lots of noise) "-Wno-invalid-offsetof", -- offsetof on non-POD types (see comment in renderer/PatchRData.cpp) "-Wextra", "-Wno-missing-field-initializers", -- (this is common in external headers we can't fix) -- add some other useful warnings that need to be enabled explicitly "-Wunused-parameter", "-Wredundant-decls", -- (useful for finding some multiply-included header files) -- "-Wformat=2", -- (useful sometimes, but a bit noisy, so skip it by default) -- "-Wcast-qual", -- (useful for checking const-correctness, but a bit noisy, so skip it by default) "-Wnon-virtual-dtor", -- (sometimes noisy but finds real bugs) "-Wundef", -- (useful for finding macro name typos) -- enable security features (stack checking etc) that shouldn't have -- a significant effect on performance and can catch bugs "-fstack-protector-all", "-U_FORTIFY_SOURCE", -- (avoid redefinition warning if already defined) "-D_FORTIFY_SOURCE=2", -- always enable strict aliasing (useful in debug builds because of the warnings) "-fstrict-aliasing", -- don't omit frame pointers (for now), because performance will be impacted -- negatively by the way this breaks profilers more than it will be impacted -- positively by the optimisation "-fno-omit-frame-pointer" } if not _OPTIONS["without-pch"] then buildoptions { -- do something (?) so that ccache can handle compilation with PCH enabled -- (ccache 3.1+ also requires CCACHE_SLOPPINESS=time_macros for this to work) "-fpch-preprocess" } end if os.istarget("linux") or os.istarget("bsd") then buildoptions { "-fPIC" } linkoptions { "-Wl,--no-undefined", "-Wl,--as-needed", "-Wl,-z,relro" } end if arch == "x86" then buildoptions { -- To support intrinsics like __sync_bool_compare_and_swap on x86 -- we need to set -march to something that supports them (i686). -- We use pentium3 to also enable other features like mmx and sse, -- while tuning for generic to have good performance on every -- supported CPU. -- Note that all these features are already supported on amd64. "-march=pentium3 -mtune=generic" } end end if arch == "arm" then -- disable warnings about va_list ABI change and use -- compile-time flags for futher configuration. buildoptions { "-Wno-psabi" } if _OPTIONS["android"] then -- Android uses softfp, so we should too. buildoptions { "-mfloat-abi=softfp" } end end if _OPTIONS["coverage"] then buildoptions { "-fprofile-arcs", "-ftest-coverage" } links { "gcov" } end -- MacOS 10.12 only supports processors with SSE 4.1, so enable that. if os.istarget("macosx") then buildoptions { "-msse4.1" } end -- Check if SDK path should be used if _OPTIONS["sysroot"] then buildoptions { "-isysroot " .. _OPTIONS["sysroot"] } linkoptions { "-Wl,-syslibroot," .. _OPTIONS["sysroot"] } end -- On OS X, sometimes we need to specify the minimum API version to use if _OPTIONS["macosx-version-min"] then buildoptions { "-mmacosx-version-min=" .. _OPTIONS["macosx-version-min"] } -- clang and llvm-gcc look at mmacosx-version-min to determine link target -- and CRT version, and use it to set the macosx_version_min linker flag linkoptions { "-mmacosx-version-min=" .. _OPTIONS["macosx-version-min"] } end -- Only libc++ is supported on MacOS if os.istarget("macosx") then buildoptions { "-stdlib=libc++" } linkoptions { "-stdlib=libc++" } end end buildoptions { -- Hide symbols in dynamic shared objects by default, for efficiency and for equivalence with -- Windows - they should be exported explicitly with __attribute__ ((visibility ("default"))) "-fvisibility=hidden" } if _OPTIONS["bindir"] then defines { "INSTALLED_BINDIR=" .. _OPTIONS["bindir"] } end if _OPTIONS["datadir"] then defines { "INSTALLED_DATADIR=" .. _OPTIONS["datadir"] } end if _OPTIONS["libdir"] then defines { "INSTALLED_LIBDIR=" .. _OPTIONS["libdir"] } end if os.istarget("linux") or os.istarget("bsd") then if _OPTIONS["prefer-local-libs"] then libdirs { "/usr/local/lib" } end -- To use our local shared libraries, they need to be found in the -- runtime dynamic linker path. Add their path to -rpath. if _OPTIONS["libdir"] then linkoptions {"-Wl,-rpath," .. _OPTIONS["libdir"] } else -- On FreeBSD we need to allow use of $ORIGIN if os.istarget("bsd") then linkoptions { "-Wl,-z,origin" } end -- Adding the executable path and taking care of correct escaping if _ACTION == "gmake" then linkoptions { "-Wl,-rpath,'$$ORIGIN'" } elseif _ACTION == "codeblocks" then linkoptions { "-Wl,-R\\\\$$$ORIGIN" } end end end end end -- create a project and set the attributes that are common to all projects. function project_create(project_name, target_type) project(project_name) language "C++" kind(target_type) filter "action:vs2017" toolset "v141_xp" filter {} filter "action:vs*" buildoptions "/utf-8" filter {} project_set_target(project_name) project_set_build_flags() end -- OSX creates a .app bundle if the project type of the main application is set to "WindowedApp". -- We don't want this because this bundle would be broken (it lacks all the resources and external dependencies, Info.plist etc...) -- Windows opens a console in the background if it's set to ConsoleApp, which is not what we want. -- I didn't check if this setting matters for linux, but WindowedApp works there. function get_main_project_target_type() if _OPTIONS["android"] then return "SharedLib" elseif os.istarget("macosx") then return "ConsoleApp" else return "WindowedApp" end end -- source_root: rel_source_dirs and rel_include_dirs are relative to this directory -- rel_source_dirs: A table of subdirectories. All source files in these directories are added. -- rel_include_dirs: A table of subdirectories to be included. -- extra_params: table including zero or more of the following: -- * no_pch: If specified, no precompiled headers are used for this project. -- * pch_dir: If specified, this directory will be used for precompiled headers instead of the default -- /pch//. -- * extra_files: table of filenames (relative to source_root) to add to project -- * extra_links: table of library names to add to link step function project_add_contents(source_root, rel_source_dirs, rel_include_dirs, extra_params) for i,v in pairs(rel_source_dirs) do local prefix = source_root..v.."/" files { prefix.."*.cpp", prefix.."*.h", prefix.."*.inl", prefix.."*.js", prefix.."*.asm", prefix.."*.mm" } end -- Put the project-specific PCH directory at the start of the -- include path, so '#include "precompiled.h"' will look in -- there first local pch_dir if not extra_params["pch_dir"] then pch_dir = source_root .. "pch/" .. project().name .. "/" else pch_dir = extra_params["pch_dir"] end includedirs { pch_dir } -- Precompiled Headers -- rationale: we need one PCH per static lib, since one global header would -- increase dependencies. To that end, we can either include them as -- "projectdir/precompiled.h", or add "source/PCH/projectdir" to the -- include path and put the PCH there. The latter is better because -- many projects contain several dirs and it's unclear where there the -- PCH should be stored. This way is also a bit easier to use in that -- source files always include "precompiled.h". -- Notes: -- * Visual Assist manages to use the project include path and can -- correctly open these files from the IDE. -- * precompiled.cpp (needed to "Create" the PCH) also goes in -- the abovementioned dir. if (not _OPTIONS["without-pch"] and not extra_params["no_pch"]) then filter "action:vs*" pchheader("precompiled.h") filter "action:xcode*" pchheader("../"..pch_dir.."precompiled.h") filter { "action:not vs*", "action:not xcode*" } pchheader(pch_dir.."precompiled.h") filter {} pchsource(pch_dir.."precompiled.cpp") defines { "CONFIG_ENABLE_PCH=1" } files { pch_dir.."precompiled.h", pch_dir.."precompiled.cpp" } else defines { "CONFIG_ENABLE_PCH=0" } flags { "NoPCH" } end -- next is source root dir, for absolute (nonrelative) includes -- (e.g. "lib/precompiled.h") includedirs { source_root } for i,v in pairs(rel_include_dirs) do includedirs { source_root .. v } end if extra_params["extra_files"] then for i,v in pairs(extra_params["extra_files"]) do -- .rc files are only needed on Windows if path.getextension(v) ~= ".rc" or os.istarget("windows") then files { source_root .. v } end end end if extra_params["extra_links"] then links { extra_params["extra_links"] } end end -- Add command-line options to set up the manifest dependencies for Windows -- (See lib/sysdep/os/win/manifest.cpp) function project_add_manifest() linkoptions { "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='X86' publicKeyToken='6595b64144ccf1df'\"" } end -------------------------------------------------------------------------------- -- engine static libraries -------------------------------------------------------------------------------- -- the engine is split up into several static libraries. this eases separate -- distribution of those components, reduces dependencies a bit, and can -- also speed up builds. -- more to the point, it is necessary to efficiently support a separate -- test executable that also includes much of the game code. -- names of all static libs created. automatically added to the -- main app project later (see explanation at end of this file) static_lib_names = {} static_lib_names_debug = {} static_lib_names_release = {} -- set up one of the static libraries into which the main engine code is split. -- extra_params: -- no_default_link: If specified, linking won't be done by default. -- For the rest of extra_params, see project_add_contents(). -- note: rel_source_dirs and rel_include_dirs are relative to global source_root. function setup_static_lib_project (project_name, rel_source_dirs, extern_libs, extra_params) local target_type = "StaticLib" project_create(project_name, target_type) project_add_contents(source_root, rel_source_dirs, {}, extra_params) project_add_extern_libs(extern_libs, target_type) if not extra_params["no_default_link"] then table.insert(static_lib_names, project_name) end -- Deactivate Run Time Type Information. Performance of dynamic_cast is very poor. -- The exception to this principle is Atlas UI, which is not a static library. rtti "off" if os.istarget("macosx") and _OPTIONS["macosx-version-min"] then xcodebuildsettings { MACOSX_DEPLOYMENT_TARGET = _OPTIONS["macosx-version-min"] } end end function setup_third_party_static_lib_project (project_name, rel_source_dirs, extern_libs, extra_params) setup_static_lib_project(project_name, rel_source_dirs, extern_libs, extra_params) includedirs { source_root .. "third_party/" .. project_name .. "/include/" } end function setup_shared_lib_project (project_name, rel_source_dirs, extern_libs, extra_params) local target_type = "SharedLib" project_create(project_name, target_type) project_add_contents(source_root, rel_source_dirs, {}, extra_params) project_add_extern_libs(extern_libs, target_type) if not extra_params["no_default_link"] then table.insert(static_lib_names, project_name) end if os.istarget("windows") then links { "delayimp" } elseif os.istarget("macosx") and _OPTIONS["macosx-version-min"] then xcodebuildsettings { MACOSX_DEPLOYMENT_TARGET = _OPTIONS["macosx-version-min"] } end end -- this is where the source tree is chopped up into static libs. -- can be changed very easily; just copy+paste a new setup_static_lib_project, -- or remove existing ones. static libs are automagically added to -- main_exe link step. function setup_all_libs () -- relative to global source_root. local source_dirs = {} -- names of external libraries used (see libraries_dir comment) local extern_libs = {} source_dirs = { "network", } extern_libs = { "spidermonkey", "enet", - "boost", -- dragged in via server->simulation.h->random + "boost", -- dragged in via server->simulation.h->random and NetSession.h->lockfree "fmt", } if not _OPTIONS["without-miniupnpc"] then table.insert(extern_libs, "miniupnpc") end setup_static_lib_project("network", source_dirs, extern_libs, {}) source_dirs = { "rlinterface", } extern_libs = { "boost", -- dragged in via simulation.h and scriptinterface.h "fmt", "spidermonkey", } setup_static_lib_project("rlinterface", source_dirs, extern_libs, { no_pch = 1 }) source_dirs = { "third_party/tinygettext/src", } extern_libs = { "iconv", "boost", "fmt", } setup_third_party_static_lib_project("tinygettext", source_dirs, extern_libs, { } ) -- it's an external library and we don't want to modify its source to fix warnings, so we just disable them to avoid noise in the compile output filter "action:vs*" buildoptions { "/wd4127", "/wd4309", "/wd4800", "/wd4100", "/wd4996", "/wd4099", "/wd4503" } filter {} if not _OPTIONS["without-lobby"] then source_dirs = { "lobby", "lobby/scripting", "i18n", "third_party/encryption" } extern_libs = { "spidermonkey", "boost", "enet", "gloox", "icu", "iconv", "libsodium", "tinygettext", "fmt", } setup_static_lib_project("lobby", source_dirs, extern_libs, {}) if _OPTIONS["use-shared-glooxwrapper"] and not _OPTIONS["build-shared-glooxwrapper"] then table.insert(static_lib_names_debug, "glooxwrapper_dbg") table.insert(static_lib_names_release, "glooxwrapper") else source_dirs = { "lobby/glooxwrapper", } extern_libs = { "boost", "gloox", "fmt", } if _OPTIONS["build-shared-glooxwrapper"] then setup_shared_lib_project("glooxwrapper", source_dirs, extern_libs, {}) else setup_static_lib_project("glooxwrapper", source_dirs, extern_libs, {}) end end else source_dirs = { "lobby/scripting", "third_party/encryption" } extern_libs = { "spidermonkey", "boost", "libsodium", "fmt", } setup_static_lib_project("lobby", source_dirs, extern_libs, {}) files { source_root.."lobby/Globals.cpp" } end source_dirs = { "simulation2", "simulation2/components", "simulation2/helpers", "simulation2/scripting", "simulation2/serialization", "simulation2/system", "simulation2/testcomponents", } extern_libs = { "boost", "opengl", "spidermonkey", "fmt", } setup_static_lib_project("simulation2", source_dirs, extern_libs, {}) source_dirs = { "scriptinterface", "scriptinterface/third_party" } extern_libs = { "boost", "spidermonkey", "valgrind", "sdl", "fmt", } setup_static_lib_project("scriptinterface", source_dirs, extern_libs, {}) source_dirs = { "ps", "ps/scripting", "network/scripting", "ps/GameSetup", "ps/XML", "soundmanager", "soundmanager/data", "soundmanager/items", "soundmanager/scripting", "maths", "maths/scripting", "i18n", "i18n/scripting", } extern_libs = { "spidermonkey", "sdl", -- key definitions "libxml2", "opengl", "zlib", "boost", "enet", "libcurl", "tinygettext", "icu", "iconv", "libsodium", "fmt", } if not _OPTIONS["without-audio"] then table.insert(extern_libs, "openal") table.insert(extern_libs, "vorbis") end setup_static_lib_project("engine", source_dirs, extern_libs, {}) source_dirs = { "graphics", "graphics/scripting", "renderer", "renderer/scripting", "third_party/mikktspace", "third_party/ogre3d_preprocessor" } extern_libs = { "opengl", "sdl", -- key definitions "spidermonkey", -- for graphics/scripting "boost", "fmt", } if not _OPTIONS["without-nvtt"] then table.insert(extern_libs, "nvtt") end setup_static_lib_project("graphics", source_dirs, extern_libs, {}) source_dirs = { "tools/atlas/GameInterface", "tools/atlas/GameInterface/Handlers" } extern_libs = { "boost", "sdl", -- key definitions "opengl", "spidermonkey", "fmt", } setup_static_lib_project("atlas", source_dirs, extern_libs, {}) source_dirs = { "gui", "gui/ObjectTypes", "gui/ObjectBases", "gui/Scripting", "gui/SettingTypes", "i18n" } extern_libs = { "spidermonkey", "sdl", -- key definitions "opengl", "boost", "enet", "tinygettext", "icu", "iconv", "fmt", } if not _OPTIONS["without-audio"] then table.insert(extern_libs, "openal") end setup_static_lib_project("gui", source_dirs, extern_libs, {}) source_dirs = { "lib", "lib/adts", "lib/allocators", "lib/external_libraries", "lib/file", "lib/file/archive", "lib/file/common", "lib/file/io", "lib/file/vfs", "lib/pch", "lib/posix", "lib/res", "lib/res/graphics", "lib/sysdep", "lib/tex" } extern_libs = { "boost", "sdl", "openal", "opengl", "libpng", "zlib", "valgrind", "cxxtest", "fmt", } -- CPU architecture-specific if arch == "amd64" then table.insert(source_dirs, "lib/sysdep/arch/amd64"); table.insert(source_dirs, "lib/sysdep/arch/x86_x64"); elseif arch == "x86" then table.insert(source_dirs, "lib/sysdep/arch/ia32"); table.insert(source_dirs, "lib/sysdep/arch/x86_x64"); elseif arch == "arm" then table.insert(source_dirs, "lib/sysdep/arch/arm"); elseif arch == "aarch64" then table.insert(source_dirs, "lib/sysdep/arch/aarch64"); elseif arch == "e2k" then table.insert(source_dirs, "lib/sysdep/arch/e2k"); end -- OS-specific sysdep_dirs = { linux = { "lib/sysdep/os/linux", "lib/sysdep/os/unix" }, -- note: RC file must be added to main_exe project. -- note: don't add "lib/sysdep/os/win/aken.cpp" because that must be compiled with the DDK. windows = { "lib/sysdep/os/win", "lib/sysdep/os/win/wposix", "lib/sysdep/os/win/whrt" }, macosx = { "lib/sysdep/os/osx", "lib/sysdep/os/unix" }, bsd = { "lib/sysdep/os/bsd", "lib/sysdep/os/unix", "lib/sysdep/os/unix/x" }, } for i,v in pairs(sysdep_dirs[os.target()]) do table.insert(source_dirs, v); end if os.istarget("linux") then if _OPTIONS["android"] then table.insert(source_dirs, "lib/sysdep/os/android") else table.insert(source_dirs, "lib/sysdep/os/unix/x") end end -- On OSX, disable precompiled headers because C++ files and Objective-C++ files are -- mixed in this project. To fix that, we would need per-file basis configuration which -- is not yet supported by the gmake action in premake. We should look into using gmake2. extra_params = {} if os.istarget("macosx") then extra_params = { no_pch = 1 } end -- runtime-library-specific if _ACTION == "vs2017" then table.insert(source_dirs, "lib/sysdep/rtl/msc"); else table.insert(source_dirs, "lib/sysdep/rtl/gcc"); end setup_static_lib_project("lowlevel", source_dirs, extern_libs, extra_params) -- Third-party libraries that are built as part of the main project, -- not built externally and then linked source_dirs = { "third_party/mongoose", } extern_libs = { } setup_static_lib_project("mongoose", source_dirs, extern_libs, { no_pch = 1 }) -- CxxTest mock function support extern_libs = { "boost", "cxxtest", } -- 'real' implementations, to be linked against the main executable -- (files are added manually and not with setup_static_lib_project -- because not all files in the directory are included) setup_static_lib_project("mocks_real", {}, extern_libs, { no_default_link = 1, no_pch = 1 }) files { "mocks/*.h", source_root.."mocks/*_real.cpp" } -- 'test' implementations, to be linked against the test executable setup_static_lib_project("mocks_test", {}, extern_libs, { no_default_link = 1, no_pch = 1 }) files { source_root.."mocks/*.h", source_root.."mocks/*_test.cpp" } end -------------------------------------------------------------------------------- -- main EXE -------------------------------------------------------------------------------- -- used for main EXE as well as test used_extern_libs = { "opengl", "sdl", "libpng", "zlib", "spidermonkey", "libxml2", "boost", "cxxtest", "comsuppw", "enet", "libcurl", "tinygettext", "icu", "iconv", "libsodium", "fmt", "valgrind", } if not os.istarget("windows") and not _OPTIONS["android"] and not os.istarget("macosx") then -- X11 should only be linked on *nix table.insert(used_extern_libs, "x11") end if not _OPTIONS["without-audio"] then table.insert(used_extern_libs, "openal") table.insert(used_extern_libs, "vorbis") end if not _OPTIONS["without-nvtt"] then table.insert(used_extern_libs, "nvtt") end if not _OPTIONS["without-lobby"] then table.insert(used_extern_libs, "gloox") end if not _OPTIONS["without-miniupnpc"] then table.insert(used_extern_libs, "miniupnpc") end -- Bundles static libs together with main.cpp and builds game executable. function setup_main_exe () local target_type = get_main_project_target_type() project_create("pyrogenesis", target_type) filter "system:not macosx" linkgroups 'On' filter {} links { "mocks_real" } local extra_params = { extra_files = { "main.cpp" }, no_pch = 1 } project_add_contents(source_root, {}, {}, extra_params) project_add_extern_libs(used_extern_libs, target_type) dependson { "Collada" } rtti "off" -- Platform Specifics if os.istarget("windows") then files { source_root.."lib/sysdep/os/win/icon.rc" } -- from "lowlevel" static lib; must be added here to be linked in files { source_root.."lib/sysdep/os/win/error_dialog.rc" } linkoptions { -- wraps main thread in a __try block(see wseh.cpp). replace with mainCRTStartup if that's undesired. "/ENTRY:wseh_EntryPoint", -- see wstartup.h "/INCLUDE:_wstartup_InitAndRegisterShutdown", -- allow manual unload of delay-loaded DLLs "/DELAY:UNLOAD", } -- allow the executable to use more than 2GB of RAM. -- this should not be enabled during development, so that memory issues are easily spotted. if _OPTIONS["large-address-aware"] then linkoptions { "/LARGEADDRESSAWARE" } end -- see manifest.cpp project_add_manifest() elseif os.istarget("linux") or os.istarget("bsd") then if not _OPTIONS["android"] and not (os.getversion().description == "OpenBSD") then links { "rt" } end if _OPTIONS["android"] then -- NDK's STANDALONE-TOOLCHAIN.html says this is required linkoptions { "-Wl,--fix-cortex-a8" } links { "log" } end if link_execinfo then links { "execinfo" } end if os.istarget("linux") or os.getversion().description == "GNU/kFreeBSD" then links { -- Dynamic libraries (needed for linking for gold) "dl", } end -- Threading support buildoptions { "-pthread" } if not _OPTIONS["android"] then linkoptions { "-pthread" } end -- For debug_resolve_symbol filter "Debug" linkoptions { "-rdynamic" } filter { } elseif os.istarget("macosx") then links { "pthread" } links { "ApplicationServices.framework", "Cocoa.framework", "CoreFoundation.framework" } if _OPTIONS["macosx-version-min"] then xcodebuildsettings { MACOSX_DEPLOYMENT_TARGET = _OPTIONS["macosx-version-min"] } end end end -------------------------------------------------------------------------------- -- atlas -------------------------------------------------------------------------------- -- setup a typical Atlas component project -- extra_params, rel_source_dirs and rel_include_dirs: as in project_add_contents; function setup_atlas_project(project_name, target_type, rel_source_dirs, rel_include_dirs, extern_libs, extra_params) local source_root = rootdir.."/source/tools/atlas/" .. project_name .. "/" project_create(project_name, target_type) -- if not specified, the default for atlas pch files is in the project root. if not extra_params["pch_dir"] then extra_params["pch_dir"] = source_root end project_add_contents(source_root, rel_source_dirs, rel_include_dirs, extra_params) project_add_extern_libs(extern_libs, target_type) -- Platform Specifics if os.istarget("windows") then -- Link to required libraries links { "winmm", "delayimp" } elseif os.istarget("linux") or os.istarget("bsd") then buildoptions { "-rdynamic", "-fPIC" } linkoptions { "-fPIC", "-rdynamic" } -- warnings triggered by wxWidgets buildoptions { "-Wno-unused-local-typedefs" } end end -- build all Atlas component projects function setup_atlas_projects() setup_atlas_project("AtlasObject", "StaticLib", { -- src ".", "../../../third_party/jsonspirit" },{ -- include "../../../third_party/jsonspirit" },{ -- extern_libs "boost", "iconv", "libxml2" },{ -- extra_params no_pch = 1 }) atlas_src = { "ActorEditor", "CustomControls/Buttons", "CustomControls/Canvas", "CustomControls/ColorDialog", "CustomControls/DraggableListCtrl", "CustomControls/EditableListCtrl", "CustomControls/FileHistory", "CustomControls/HighResTimer", "CustomControls/MapDialog", "CustomControls/MapResizeDialog", "CustomControls/SnapSplitterWindow", "CustomControls/VirtualDirTreeCtrl", "CustomControls/Windows", "General", "General/VideoRecorder", "Misc", "ScenarioEditor", "ScenarioEditor/Sections/Common", "ScenarioEditor/Sections/Cinema", "ScenarioEditor/Sections/Environment", "ScenarioEditor/Sections/Map", "ScenarioEditor/Sections/Object", "ScenarioEditor/Sections/Player", "ScenarioEditor/Sections/Terrain", "ScenarioEditor/Tools", "ScenarioEditor/Tools/Common", } atlas_extra_links = { "AtlasObject" } atlas_extern_libs = { "boost", "comsuppw", "iconv", "libxml2", "sdl", -- key definitions "wxwidgets", "zlib", } if not os.istarget("windows") and not os.istarget("macosx") then -- X11 should only be linked on *nix table.insert(atlas_extern_libs, "x11") end setup_atlas_project("AtlasUI", "SharedLib", atlas_src, { -- include "..", "CustomControls", "Misc", "../../../third_party/jsonspirit" }, atlas_extern_libs, { -- extra_params pch_dir = rootdir.."/source/tools/atlas/AtlasUI/Misc/", no_pch = false, extra_links = atlas_extra_links, extra_files = { "Misc/atlas.rc" } }) end -- Atlas 'frontend' tool-launching projects function setup_atlas_frontend_project (project_name) local target_type = get_main_project_target_type() project_create(project_name, target_type) local source_root = rootdir.."/source/tools/atlas/AtlasFrontends/" files { source_root..project_name..".cpp" } if os.istarget("windows") then files { source_root..project_name..".rc" } end includedirs { source_root .. ".." } -- Platform Specifics if os.istarget("windows") then -- see manifest.cpp project_add_manifest() else -- Non-Windows, = Unix links { "AtlasObject" } end links { "AtlasUI" } end function setup_atlas_frontends() setup_atlas_frontend_project("ActorEditor") end -------------------------------------------------------------------------------- -- collada -------------------------------------------------------------------------------- function setup_collada_project(project_name, target_type, rel_source_dirs, rel_include_dirs, extern_libs, extra_params) project_create(project_name, target_type) local source_root = source_root.."collada/" extra_params["pch_dir"] = source_root project_add_contents(source_root, rel_source_dirs, rel_include_dirs, extra_params) project_add_extern_libs(extern_libs, target_type) -- Platform Specifics if os.istarget("windows") then characterset "MBCS" elseif os.istarget("linux") then defines { "LINUX" } links { "dl", } -- FCollada is not aliasing-safe, so disallow dangerous optimisations -- (TODO: It'd be nice to fix FCollada, but that looks hard) buildoptions { "-fno-strict-aliasing" } buildoptions { "-rdynamic" } linkoptions { "-rdynamic" } elseif os.istarget("bsd") then if os.getversion().description == "OpenBSD" then links { "c", } end if os.getversion().description == "GNU/kFreeBSD" then links { "dl", } end buildoptions { "-fno-strict-aliasing" } buildoptions { "-rdynamic" } linkoptions { "-rdynamic" } elseif os.istarget("macosx") then -- define MACOS-something? buildoptions { "-fno-strict-aliasing" } -- On OSX, fcollada uses a few utility functions from coreservices links { "CoreServices.framework" } end end -- build all Collada component projects function setup_collada_projects() setup_collada_project("Collada", "SharedLib", { -- src "." },{ -- include },{ -- extern_libs "fcollada", "iconv", "libxml2" },{ -- extra_params }) end -------------------------------------------------------------------------------- -- tests -------------------------------------------------------------------------------- function setup_tests() local cxxtest = require "cxxtest" if os.istarget("windows") then cxxtest.setpath(rootdir.."/build/bin/cxxtestgen.exe") else cxxtest.setpath(rootdir.."/libraries/source/cxxtest-4.4/bin/cxxtestgen") end local runner = "ErrorPrinter" if _OPTIONS["jenkins-tests"] then runner = "XmlPrinter" end local includefiles = { -- Precompiled headers - the header is added to all generated .cpp files -- note that the header isn't actually precompiled here, only #included -- so that the build stage can use it as a precompiled header. "precompiled.h", -- This is required to build against SDL 2.0.4 on Windows. "lib/external_libraries/libsdl.h", } cxxtest.init(source_root, true, runner, includefiles) local target_type = get_main_project_target_type() project_create("test", target_type) -- Find header files in 'test' subdirectories local all_files = os.matchfiles(source_root .. "**/tests/*.h") local test_files = {} for i,v in pairs(all_files) do -- Don't include sysdep tests on the wrong sys -- Don't include Atlas tests unless Atlas is being built if not (string.find(v, "/sysdep/os/win/") and not os.istarget("windows")) and not (string.find(v, "/tools/atlas/") and not _OPTIONS["atlas"]) and not (string.find(v, "/sysdep/arch/x86_x64/") and ((arch ~= "amd64") or (arch ~= "x86"))) then table.insert(test_files, v) end end cxxtest.configure_project(test_files) filter "system:not macosx" linkgroups 'On' filter {} links { static_lib_names } filter "Debug" links { static_lib_names_debug } filter "Release" links { static_lib_names_release } filter { } links { "mocks_test" } if _OPTIONS["atlas"] then links { "AtlasObject" } end extra_params = { extra_files = { "test_setup.cpp" }, } project_add_contents(source_root, {}, {}, extra_params) project_add_extern_libs(used_extern_libs, target_type) dependson { "Collada" } rtti "off" -- TODO: should fix the duplication between this OS-specific linking -- code, and the similar version in setup_main_exe if os.istarget("windows") then -- from "lowlevel" static lib; must be added here to be linked in files { source_root.."lib/sysdep/os/win/error_dialog.rc" } -- see wstartup.h linkoptions { "/INCLUDE:_wstartup_InitAndRegisterShutdown" } -- Enables console for the TEST project on Windows linkoptions { "/SUBSYSTEM:CONSOLE" } project_add_manifest() elseif os.istarget("linux") or os.istarget("bsd") then if link_execinfo then links { "execinfo" } end if not _OPTIONS["android"] and not (os.getversion().description == "OpenBSD") then links { "rt" } end if _OPTIONS["android"] then -- NDK's STANDALONE-TOOLCHAIN.html says this is required linkoptions { "-Wl,--fix-cortex-a8" } end if os.istarget("linux") or os.getversion().description == "GNU/kFreeBSD" then links { -- Dynamic libraries (needed for linking for gold) "dl", } end -- Threading support buildoptions { "-pthread" } if not _OPTIONS["android"] then linkoptions { "-pthread" } end -- For debug_resolve_symbol filter "Debug" linkoptions { "-rdynamic" } filter { } includedirs { source_root .. "pch/test/" } elseif os.istarget("macosx") and _OPTIONS["macosx-version-min"] then xcodebuildsettings { MACOSX_DEPLOYMENT_TARGET = _OPTIONS["macosx-version-min"] } end end -- must come first, so that VC sets it as the default project and therefore -- allows running via F5 without the "where is the EXE" dialog. setup_main_exe() setup_all_libs() -- add the static libs to the main EXE project. only now (after -- setup_all_libs has run) are the lib names known. cannot move -- setup_main_exe to run after setup_all_libs (see comment above). -- we also don't want to hardcode the names - that would require more -- work when changing the static lib breakdown. project("pyrogenesis") -- Set the main project active links { static_lib_names } filter "Debug" links { static_lib_names_debug } filter "Release" links { static_lib_names_release } filter { } if _OPTIONS["atlas"] then setup_atlas_projects() setup_atlas_frontends() end setup_collada_projects() if not _OPTIONS["without-tests"] then setup_tests() end Index: ps/trunk/source/main.cpp =================================================================== --- ps/trunk/source/main.cpp (revision 24517) +++ ps/trunk/source/main.cpp (revision 24518) @@ -1,788 +1,784 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* This module drives the game when running without Atlas (our integrated map editor). It receives input and OS messages via SDL and feeds them into the input dispatcher, where they are passed on to the game GUI and simulation. It also contains main(), which either runs the above controller or that of Atlas depending on commandline parameters. */ // not for any PCH effort, but instead for the (common) definitions // included there. #define MINIMAL_PCH 2 #include "lib/precompiled.h" #include "lib/debug.h" #include "lib/status.h" #include "lib/secure_crt.h" #include "lib/frequency_filter.h" #include "lib/input.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/external_libraries/libsdl.h" #include "ps/ArchiveBuilder.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Loader.h" #include "ps/ModInstaller.h" #include "ps/Profile.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" #include "ps/Replay.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/World.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/Paths.h" #include "ps/XML/Xeromyces.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "network/NetSession.h" #include "lobby/IXmppClient.h" #include "graphics/Camera.h" #include "graphics/GameView.h" #include "graphics/TextureManager.h" #include "gui/GUIManager.h" #include "renderer/Renderer.h" #include "rlinterface/RLInterface.cpp" #include "scriptinterface/ScriptEngine.h" #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" #include "soundmanager/ISoundManager.h" #if OS_UNIX #include // geteuid #endif // OS_UNIX #if OS_MACOSX #include "lib/sysdep/os/osx/osx_atlas.h" #endif #if MSC_VERSION #include #define getpid _getpid // Use the non-deprecated function name #endif #include #include extern CmdLineArgs g_args; extern CStrW g_UniqueLogPostfix; // Marks terrain as modified so the minimap can repaint (is there a cleaner way of handling this?) bool g_GameRestarted = false; // Determines the lifetime of the mainloop enum ShutdownType { // The application shall continue the main loop. None, // The process shall terminate as soon as possible. Quit, // The engine should be restarted in the same process, for instance to activate different mods. Restart, // Atlas should be started in the same process. RestartAsAtlas }; static ShutdownType g_Shutdown = ShutdownType::None; // to avoid redundant and/or recursive resizing, we save the new // size after VIDEORESIZE messages and only update the video mode // once per frame. // these values are the latest resize message, and reset to 0 once we've // updated the video mode static int g_ResizedW; static int g_ResizedH; static std::chrono::high_resolution_clock::time_point lastFrameTime; bool IsQuitRequested() { return g_Shutdown == ShutdownType::Quit; } void QuitEngine() { g_Shutdown = ShutdownType::Quit; } void RestartEngine() { g_Shutdown = ShutdownType::Restart; } void StartAtlas() { g_Shutdown = ShutdownType::RestartAsAtlas; } // main app message handler static InReaction MainInputHandler(const SDL_Event_* ev) { switch(ev->ev.type) { case SDL_WINDOWEVENT: switch(ev->ev.window.event) { case SDL_WINDOWEVENT_ENTER: RenderCursor(true); break; case SDL_WINDOWEVENT_LEAVE: RenderCursor(false); break; case SDL_WINDOWEVENT_RESIZED: g_ResizedW = ev->ev.window.data1; g_ResizedH = ev->ev.window.data2; break; case SDL_WINDOWEVENT_MOVED: g_VideoMode.UpdatePosition(ev->ev.window.data1, ev->ev.window.data2); } break; case SDL_QUIT: QuitEngine(); break; case SDL_DROPFILE: { char* dropped_filedir = ev->ev.drop.file; const Paths paths(g_args); CModInstaller installer(paths.UserData() / "mods", paths.Cache()); installer.Install(std::string(dropped_filedir), g_ScriptContext, true); SDL_free(dropped_filedir); if (installer.GetInstalledMods().empty()) LOGERROR("Failed to install mod %s", dropped_filedir); else { LOGMESSAGE("Installed mod %s", installer.GetInstalledMods().front()); RestartEngine(); } break; } case SDL_HOTKEYPRESS: std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "exit") { QuitEngine(); return IN_HANDLED; } else if (hotkey == "screenshot") { WriteScreenshot(L".png"); return IN_HANDLED; } else if (hotkey == "bigscreenshot") { WriteBigScreenshot(L".bmp", 10); return IN_HANDLED; } else if (hotkey == "togglefullscreen") { g_VideoMode.ToggleFullscreen(); return IN_HANDLED; } else if (hotkey == "profile2.toggle") { g_Profiler2.Toggle(); return IN_HANDLED; } break; } return IN_PASS; } // dispatch all pending events to the various receivers. static void PumpEvents() { ScriptRequest rq(g_GUI->GetScriptInterface()); PROFILE3("dispatch events"); SDL_Event_ ev; while (in_poll_event(&ev)) { PROFILE2("event"); if (g_GUI) { JS::RootedValue tmpVal(rq.cx); ScriptInterface::ToJSVal(rq, &tmpVal, ev); std::string data = g_GUI->GetScriptInterface()->StringifyJSON(&tmpVal); PROFILE2_ATTR("%s", data.c_str()); } in_dispatch_event(&ev); } g_TouchInput.Frame(); } /** * Optionally throttle the render frequency in order to * prevent 100% workload of the currently used CPU core. */ inline static void LimitFPS() { if (g_VSync) return; double fpsLimit = 0.0; CFG_GET_VAL(g_Game && g_Game->IsGameStarted() ? "adaptivefps.session" : "adaptivefps.menu", fpsLimit); // Keep in sync with options.json if (fpsLimit < 20.0 || fpsLimit >= 100.0) return; double wait = 1000.0 / fpsLimit - std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - lastFrameTime).count() / 1000.0; if (wait > 0.0) SDL_Delay(wait); lastFrameTime = std::chrono::high_resolution_clock::now(); } static int ProgressiveLoad() { PROFILE3("progressive load"); wchar_t description[100]; int progress_percent; try { Status ret = LDR_ProgressiveLoad(10e-3, description, ARRAY_SIZE(description), &progress_percent); switch(ret) { // no load active => no-op (skip code below) case INFO::OK: return 0; // current task didn't complete. we only care about this insofar as the // load process is therefore not yet finished. case ERR::TIMED_OUT: break; // just finished loading case INFO::ALL_COMPLETE: g_Game->ReallyStartGame(); wcscpy_s(description, ARRAY_SIZE(description), L"Game is starting.."); // LDR_ProgressiveLoad returns L""; set to valid text to // avoid problems in converting to JSString break; // error! default: WARN_RETURN_STATUS_IF_ERR(ret); // can't do this above due to legit ERR::TIMED_OUT break; } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map loading failed // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } GUI_DisplayLoadProgress(progress_percent, description); return 0; } static void RendererIncrementalLoad() { PROFILE3("renderer incremental load"); const double maxTime = 0.1f; double startTime = timer_Time(); bool more; do { more = g_Renderer.GetTextureManager().MakeProgress(); } while (more && timer_Time() - startTime < maxTime); } static void Frame() { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); ogl_WarnIfError(); // get elapsed time const double time = timer_Time(); g_frequencyFilter->Update(time); // .. old method - "exact" but contains jumps #if 0 static double last_time; const double time = timer_Time(); const float TimeSinceLastFrame = (float)(time-last_time); last_time = time; ONCE(return); // first call: set last_time and return // .. new method - filtered and more smooth, but errors may accumulate #else const float realTimeSinceLastFrame = 1.0 / g_frequencyFilter->SmoothedFrequency(); #endif ENSURE(realTimeSinceLastFrame > 0.0f); // Decide if update is necessary bool need_update = true; // If we are not running a multiplayer game, disable updates when the game is // minimized or out of focus and relinquish the CPU a bit, in order to make // debugging easier. if (g_PauseOnFocusLoss && !g_NetClient && !g_app_has_focus) { PROFILE3("non-focus delay"); need_update = false; // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored SDL_Delay(10); } // this scans for changed files/directories and reloads them, thus // allowing hotloading (changes are immediately assimilated in-game). ReloadChangedFiles(); ProgressiveLoad(); RendererIncrementalLoad(); PumpEvents(); // if the user quit by closing the window, the GL context will be broken and // may crash when we call Render() on some drivers, so leave this loop // before rendering if (g_Shutdown != ShutdownType::None) return; // respond to pumped resize events if (g_ResizedW || g_ResizedH) { g_VideoMode.ResizeWindow(g_ResizedW, g_ResizedH); g_ResizedW = g_ResizedH = 0; } if (g_NetClient) g_NetClient->Poll(); ogl_WarnIfError(); g_GUI->TickObjects(); ogl_WarnIfError(); if (g_RLInterface) g_RLInterface->TryApplyMessage(); if (g_Game && g_Game->IsGameStarted() && need_update) { if (!g_RLInterface) g_Game->Update(realTimeSinceLastFrame); g_Game->GetView()->Update(float(realTimeSinceLastFrame)); } - // Immediately flush any messages produced by simulation code - if (g_NetClient) - g_NetClient->Flush(); - // Keep us connected to any XMPP servers if (g_XmppClient) g_XmppClient->recv(); g_UserReporter.Update(); g_Console->Update(realTimeSinceLastFrame); ogl_WarnIfError(); if (g_SoundManager) g_SoundManager->IdleTask(); if (ShouldRender()) { Render(); { PROFILE3("swap buffers"); SDL_GL_SwapWindow(g_VideoMode.GetWindow()); ogl_WarnIfError(); } g_Renderer.OnSwapBuffers(); } g_Profiler.Frame(); g_GameRestarted = false; LimitFPS(); } static void NonVisualFrame() { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); static u32 turn = 0; debug_printf("Turn %u (%u)...\n", turn++, DEFAULT_TURN_LENGTH_SP); g_Game->GetSimulation2()->Update(DEFAULT_TURN_LENGTH_SP); g_Profiler.Frame(); if (g_Game->IsGameFinished()) QuitEngine(); } static void MainControllerInit() { // add additional input handlers only needed by this controller: // must be registered after gui_handler. Should mayhap even be last. in_add_handler(MainInputHandler); } static void MainControllerShutdown() { in_reset_handlers(); } static void StartRLInterface(CmdLineArgs args) { std::string server_address; CFG_GET_VAL("rlinterface.address", server_address); if (!args.Get("rl-interface").empty()) server_address = args.Get("rl-interface"); g_RLInterface = new RLInterface(); g_RLInterface->EnableHTTP(server_address.c_str()); debug_printf("RL interface listening on %s\n", server_address.c_str()); } static void RunRLServer(const bool isNonVisual, const std::vector modsToInstall, const CmdLineArgs args) { int flags = INIT_MODS; while (!Init(args, flags)) { flags &= ~INIT_MODS; Shutdown(SHUTDOWN_FROM_CONFIG); } g_Shutdown = ShutdownType::None; std::vector installedMods; if (!modsToInstall.empty()) { Paths paths(args); CModInstaller installer(paths.UserData() / "mods", paths.Cache()); // Install the mods without deleting the pyromod files for (const OsPath& modPath : modsToInstall) installer.Install(modPath, g_ScriptContext, true); installedMods = installer.GetInstalledMods(); } if (isNonVisual) { InitNonVisual(args); StartRLInterface(args); while (g_Shutdown == ShutdownType::None) g_RLInterface->TryApplyMessage(); QuitEngine(); } else { InitGraphics(args, 0, installedMods); MainControllerInit(); StartRLInterface(args); while (g_Shutdown == ShutdownType::None) Frame(); } Shutdown(0); MainControllerShutdown(); CXeromyces::Terminate(); delete g_RLInterface; } // moved into a helper function to ensure args is destroyed before // exit(), which may result in a memory leak. static void RunGameOrAtlas(int argc, const char* argv[]) { CmdLineArgs args(argc, argv); g_args = args; if (args.Has("version")) { debug_printf("Pyrogenesis %s\n", engine_version); return; } if (args.Has("autostart-nonvisual") && args.Get("autostart").empty() && !args.Has("rl-interface")) { LOGERROR("-autostart-nonvisual cant be used alone. A map with -autostart=\"TYPEDIR/MAPNAME\" is needed."); return; } if (args.Has("unique-logs")) g_UniqueLogPostfix = L"_" + std::to_wstring(std::time(nullptr)) + L"_" + std::to_wstring(getpid()); const bool isVisualReplay = args.Has("replay-visual"); const bool isNonVisualReplay = args.Has("replay"); const bool isNonVisual = args.Has("autostart-nonvisual"); const OsPath replayFile( isVisualReplay ? args.Get("replay-visual") : isNonVisualReplay ? args.Get("replay") : ""); if (isVisualReplay || isNonVisualReplay) { if (!FileExists(replayFile)) { debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile.string8().c_str()); return; } if (DirectoryExists(replayFile)) { debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.string8().c_str()); return; } } std::vector modsToInstall; for (const CStr& arg : args.GetArgsWithoutName()) { const OsPath modPath(arg); if (!CModInstaller::IsDefaultModExtension(modPath.Extension())) { debug_printf("Skipping file '%s' which does not have a mod file extension.\n", modPath.string8().c_str()); continue; } if (!FileExists(modPath)) { debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath.string8().c_str()); continue; } if (DirectoryExists(modPath)) { debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath.string8().c_str()); continue; } modsToInstall.emplace_back(std::move(modPath)); } // We need to initialize SpiderMonkey and libxml2 in the main thread before // any thread uses them. So initialize them here before we might run Atlas. ScriptEngine scriptEngine; CXeromyces::Startup(); if (ATLAS_RunIfOnCmdLine(args, false)) { CXeromyces::Terminate(); return; } if (isNonVisualReplay) { if (!args.Has("mod")) { LOGERROR("At least one mod should be specified! Did you mean to add the argument '-mod=public'?"); CXeromyces::Terminate(); return; } Paths paths(args); g_VFS = CreateVfs(); g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); MountMods(paths, GetMods(args, INIT_MODS)); { CReplayPlayer replay; replay.Load(replayFile); replay.Replay( args.Has("serializationtest"), args.Has("rejointest") ? args.Get("rejointest").ToInt() : -1, args.Has("ooslog"), !args.Has("hashtest-full") || args.Get("hashtest-full") == "true", args.Has("hashtest-quick") && args.Get("hashtest-quick") == "true"); } g_VFS.reset(); CXeromyces::Terminate(); return; } // run in archive-building mode if requested if (args.Has("archivebuild")) { Paths paths(args); OsPath mod(args.Get("archivebuild")); OsPath zip; if (args.Has("archivebuild-output")) zip = args.Get("archivebuild-output"); else zip = mod.Filename().ChangeExtension(L".zip"); CArchiveBuilder builder(mod, paths.Cache()); // Add mods provided on the command line // NOTE: We do not handle mods in the user mod path here std::vector mods = args.GetMultiple("mod"); for (size_t i = 0; i < mods.size(); ++i) builder.AddBaseMod(paths.RData()/"mods"/mods[i]); builder.Build(zip, args.Has("archivebuild-compress")); CXeromyces::Terminate(); return; } const double res = timer_Resolution(); g_frequencyFilter = CreateFrequencyFilter(res, 30.0); if (args.Has("rl-interface")) { RunRLServer(isNonVisual, modsToInstall, args); return; } // run the game int flags = INIT_MODS; do { g_Shutdown = ShutdownType::None; if (!Init(args, flags)) { flags &= ~INIT_MODS; Shutdown(SHUTDOWN_FROM_CONFIG); continue; } std::vector installedMods; if (!modsToInstall.empty()) { Paths paths(args); CModInstaller installer(paths.UserData() / "mods", paths.Cache()); // Install the mods without deleting the pyromod files for (const OsPath& modPath : modsToInstall) installer.Install(modPath, g_ScriptContext, true); installedMods = installer.GetInstalledMods(); } if (isNonVisual) { InitNonVisual(args); while (g_Shutdown == ShutdownType::None) NonVisualFrame(); } else { InitGraphics(args, 0, installedMods); MainControllerInit(); while (g_Shutdown == ShutdownType::None) Frame(); } // Do not install mods again in case of restart (typically from the mod selector) modsToInstall.clear(); Shutdown(0); MainControllerShutdown(); flags &= ~INIT_MODS; } while (g_Shutdown == ShutdownType::Restart); #if OS_MACOSX if (g_Shutdown == ShutdownType::RestartAsAtlas) startNewAtlasProcess(); #else if (g_Shutdown == ShutdownType::RestartAsAtlas) ATLAS_RunIfOnCmdLine(args, true); #endif CXeromyces::Terminate(); } #if OS_ANDROID // In Android we compile the engine as a shared library, not an executable, // so rename main() to a different symbol that the wrapper library can load #undef main #define main pyrogenesis_main extern "C" __attribute__((visibility ("default"))) int main(int argc, char* argv[]); #endif extern "C" int main(int argc, char* argv[]) { #if OS_UNIX // Don't allow people to run the game with root permissions, // because bad things can happen, check before we do anything if (geteuid() == 0) { std::cerr << "********************************************************\n" << "WARNING: Attempted to run the game with root permission!\n" << "This is not allowed because it can alter home directory \n" << "permissions and opens your system to vulnerabilities. \n" << "(You received this message because you were either \n" <<" logged in as root or used e.g. the 'sudo' command.) \n" << "********************************************************\n\n"; return EXIT_FAILURE; } #endif // OS_UNIX EarlyInit(); // must come at beginning of main RunGameOrAtlas(argc, const_cast(argv)); // Shut down profiler initialised by EarlyInit g_Profiler2.Shutdown(); return EXIT_SUCCESS; } Index: ps/trunk/source/network/NetClient.cpp =================================================================== --- ps/trunk/source/network/NetClient.cpp (revision 24517) +++ ps/trunk/source/network/NetClient.cpp (revision 24518) @@ -1,861 +1,851 @@ /* Copyright (C) 2020 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 "NetMessage.h" #include "NetSession.h" #include "lib/byte_order.h" #include "lib/external_libraries/enet.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/Loader.h" +#include "ps/Profile.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" CNetClient *g_NetClient = NULL; /** * Async task for receiving the initial game state when rejoining an * in-progress network game. */ class CNetFileReceiveTask_ClientRejoin : public CNetFileReceiveTask { NONCOPYABLE(CNetFileReceiveTask_ClientRejoin); public: CNetFileReceiveTask_ClientRejoin(CNetClient& client) : m_Client(client) { } virtual void OnComplete() { // We've received the game state from the server // Save it so we can use it after the map has finished loading m_Client.m_JoinSyncBuffer = m_Buffer; // Pretend the server told us to start the game CGameStartMessage start; m_Client.HandleMessage(&start); } private: CNetClient& m_Client; }; CNetClient::CNetClient(CGame* game, bool isLocalClient) : m_Session(NULL), m_UserName(L"anonymous"), m_HostID((u32)-1), m_ClientTurnManager(NULL), m_Game(game), m_GameAttributes(game->GetSimulation2()->GetScriptInterface().GetGeneralJSContext()), m_IsLocalClient(isLocalClient), m_LastConnectionCheck(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_INITIAL_GAMESETUP, (void*)&OnAuthenticate, context); AddTransition(NCS_INITIAL_GAMESETUP, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context); AddTransition(NCS_PREGAME, (uint)NMT_CHAT, NCS_PREGAME, (void*)&OnChat, context); AddTransition(NCS_PREGAME, (uint)NMT_READY, NCS_PREGAME, (void*)&OnReady, context); AddTransition(NCS_PREGAME, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context); AddTransition(NCS_PREGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_PREGAME, (void*)&OnPlayerAssignment, context); AddTransition(NCS_PREGAME, (uint)NMT_KICKED, NCS_PREGAME, (void*)&OnKicked, context); AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_PREGAME, (void*)&OnClientTimeout, context); AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_PREGAME, (void*)&OnClientPerformance, context); AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, (void*)&OnGameStart, context); AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, (void*)&OnJoinSyncStart, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, (void*)&OnChat, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_SETUP, NCS_JOIN_SYNCING, (void*)&OnGameSetup, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_JOIN_SYNCING, (void*)&OnPlayerAssignment, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_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::SetHostingPlayerName(const CStr& hostingPlayerName) { m_HostingPlayerName = hostingPlayerName; } bool CNetClient::SetupConnection(const CStr& server, const u16 port, ENetHost* enetClient) { CNetClientSession* session = new CNetClientSession(*this); bool ok = session->Connect(server, port, m_IsLocalClient, enetClient); SetAndOwnSession(session); + m_PollingThread = std::thread(CNetClientSession::RunNetLoop, m_Session); return ok; } void CNetClient::SetAndOwnSession(CNetClientSession* session) { delete m_Session; m_Session = session; } void CNetClient::DestroyConnection() { - // Attempt to send network messages from the current frame before connection is destroyed. - if (m_ClientTurnManager) - { - m_ClientTurnManager->OnDestroyConnection(); - Flush(); - } - SAFE_DELETE(m_Session); + 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->Poll(); + 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 > DEFAULT_TURN_LENGTH_MP) { PushGuiMessage( "type", "netwarn", "warntype", "server-latency", "meanRTT", meanRTT); } } -void CNetClient::Flush() -{ - if (m_Session) - m_Session->Flush(); -} - void CNetClient::GuiPoll(JS::MutableHandleValue ret) { if (m_GuiMessageQueue.empty()) { ret.setUndefined(); return; } ret.set(m_GuiMessageQueue.front()); m_GuiMessageQueue.pop_front(); } std::string CNetClient::TestReadGuiMessages() { ScriptRequest rq(GetScriptInterface()); std::string r; JS::RootedValue msg(rq.cx); while (true) { GuiPoll(&msg); if (msg.isUndefined()) break; r += GetScriptInterface().ToString(&msg) + "\n"; } return r; } const ScriptInterface& CNetClient::GetScriptInterface() { return m_Game->GetSimulation2()->GetScriptInterface(); } void CNetClient::PostPlayerAssignmentsToScript() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue newAssignments(rq.cx); ScriptInterface::CreateObject(rq, &newAssignments); for (const std::pair& p : m_PlayerAssignments) { JS::RootedValue assignment(rq.cx); ScriptInterface::CreateObject( rq, &assignment, "name", p.second.m_Name, "player", p.second.m_PlayerID, "status", p.second.m_Status); GetScriptInterface().SetProperty(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); - SAFE_DELETE(m_Session); + 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() { CGameStartMessage gameStart; 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 = L""; // TODO authenticate.m_IsLocalClient = m_IsLocalClient; 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_HostingPlayerName.empty()) g_XmppClient->SendIqLobbyAuth(client->m_HostingPlayerName, 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->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->m_GameAttributes = message->m_Data; 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); - client->m_Session->SetLongTimeout(true); - // Find the player assigned to our GUID int player = -1; if (client->m_PlayerAssignments.find(client->m_GUID) != client->m_PlayerAssignments.end()) player = client->m_PlayerAssignments[client->m_GUID].m_PlayerID; client->m_ClientTurnManager = new CNetClientTurnManager( *client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger()); client->m_Game->SetPlayerID(player); client->m_Game->StartGame(&client->m_GameAttributes, ""); client->PushGuiMessage("type", "start"); return true; } bool CNetClient::OnJoinSyncStart(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START); CNetClient* client = static_cast(context); // The server wants us to start downloading the game state from it, so do so client->m_Session->GetFileTransferer().StartTask( shared_ptr(new CNetFileReceiveTask_ClientRejoin(*client)) ); return true; } bool CNetClient::OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH); CNetClient* client = 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 < DEFAULT_TURN_LENGTH_MP || 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()); - bool finished = true; std::vector guids; guids.reserve(message->m_Clients.size()); for (const CClientsLoadingMessage::S_m_Clients& mClient : message->m_Clients) - { - if (client->m_GUID == mClient.m_GUID) - finished = false; - guids.push_back(mClient.m_GUID); - } - - // Disable the timeout here after processing the enet message, so as to ensure that the connection isn't currently - // timing out (as it is when just leaving the loading screen in LoadFinished). - if (finished) - client->m_Session->SetLongTimeout(false); 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(); - // The last client to leave the loading screen didn't receive the CClientsLoadingMessage, so disable here. - client->m_Session->SetLongTimeout(false); - 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/NetClient.h =================================================================== --- ps/trunk/source/network/NetClient.h (revision 24517) +++ ps/trunk/source/network/NetClient.h (revision 24518) @@ -1,312 +1,314 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef NETCLIENT_H #define NETCLIENT_H #include "network/fsm.h" #include "network/NetFileTransfer.h" #include "network/NetHost.h" #include "scriptinterface/ScriptInterface.h" #include "ps/CStr.h" +#include #include #include +#include class CGame; class CNetClientSession; class CNetClientTurnManager; class CNetServer; class ScriptInterface; typedef struct _ENetHost ENetHost; // NetClient session FSM states enum { NCS_UNCONNECTED, NCS_CONNECT, NCS_HANDSHAKE, NCS_AUTHENTICATE, NCS_INITIAL_GAMESETUP, NCS_PREGAME, NCS_LOADING, NCS_JOIN_SYNCING, NCS_INGAME }; /** * Network client. * This code is run by every player (including the host, if they are not * a dedicated server). * It provides an interface between the GUI, the network (via CNetClientSession), * and the game (via CGame and CNetClientTurnManager). */ class CNetClient : public CFsm { NONCOPYABLE(CNetClient); friend class CNetFileReceiveTask_ClientRejoin; public: /** * Construct a client associated with the given game object. * The game must exist for the lifetime of this object. */ CNetClient(CGame* game, bool isLocalClient); virtual ~CNetClient(); /** * We assume that adding a tracing function that's only called * during GC is better for performance than using a * PersistentRooted where each value needs to be added to * the root set. */ static void Trace(JSTracer *trc, void *data) { reinterpret_cast(data)->TraceMember(trc); } void TraceMember(JSTracer *trc); /** * Set the user's name that will be displayed to all players. * This must not be called after the connection setup. */ void SetUserName(const CStrW& username); /** * Set the name of the hosting player. * This is needed for the secure lobby authentication. */ void SetHostingPlayerName(const CStr& hostingPlayerName); /** * Returns the GUID of the local client. * Used for distinguishing observers. */ CStr GetGUID() const { return m_GUID; } /** * Set up a connection to the remote networked server. * @param server IP address or host name to connect to * @return true on success, false on connection failure */ bool SetupConnection(const CStr& server, const u16 port, ENetHost* enetClient); /** * Destroy the connection to the server. * This client probably cannot be used again. */ void DestroyConnection(); /** * Poll the connection for messages from the server and process them, and send * any queued messages. * This must be called frequently (i.e. once per frame). */ void Poll(); /** * Locally triggers a GUI message if the connection to the server is being lost or has bad latency. */ void CheckServerConnection(); /** - * Flush any queued outgoing network messages. - * This should be called soon after sending a group of messages that may be batched together. - */ - void Flush(); - - /** * Retrieves the next queued GUI message, and removes it from the queue. * The returned value is in the GetScriptInterface() JS context. * * This is the only mechanism for the networking code to send messages to * the GUI - it is pull-based (instead of push) so the engine code does not * need to know anything about the code structure of the GUI scripts. * * The structure of the messages is { "type": "...", ... }. * The exact types and associated data are not specified anywhere - the * implementation and GUI scripts must make the same assumptions. * * @return next message, or the value 'undefined' if the queue is empty */ void GuiPoll(JS::MutableHandleValue); /** * Add a message to the queue, to be read by GuiPoll. * The script value must be in the GetScriptInterface() JS context. */ template void PushGuiMessage(Args const&... args) { ScriptRequest rq(GetScriptInterface()); JS::RootedValue message(rq.cx); ScriptInterface::CreateObject(rq, &message, args...); m_GuiMessageQueue.push_back(JS::Heap(message)); } /** * Return a concatenation of all messages in the GUI queue, * for test cases to easily verify the queue contents. */ std::string TestReadGuiMessages(); /** * Get the script interface associated with this network client, * which is equivalent to the one used by the CGame in the constructor. */ const ScriptInterface& GetScriptInterface(); /** * Send a message to the server. * @param message message to send * @return true on success */ bool SendMessage(const CNetMessage* message); /** * Call when the network connection has been successfully initiated. */ void HandleConnect(); /** * Call when the network connection has been lost. */ void HandleDisconnect(u32 reason); /** * Call when a message has been received from the network. */ bool HandleMessage(CNetMessage* message); /** * Call when the game has started and all data files have been loaded, * to signal to the server that we are ready to begin the game. */ void LoadFinished(); void SendGameSetupMessage(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface); void SendAssignPlayerMessage(const int playerID, const CStr& guid); void SendChatMessage(const std::wstring& text); void SendReadyMessage(const int status); void SendClearAllReadyMessage(); void SendStartGameMessage(); /** * Call when the client has rejoined a running match and finished * the loading screen. */ void SendRejoinedMessage(); /** * Call to kick/ban a client */ void SendKickPlayerMessage(const CStrW& playerName, bool ban); /** * Call when the client has paused or unpaused the game. */ void SendPausedMessage(bool pause); + /** + * @return Whether the NetClient is shutting down. + */ + bool ShouldShutdown() const; private: void SendAuthenticateMessage(); // Net message / FSM transition handlers static bool OnConnect(void* context, CFsmEvent* event); static bool OnHandshake(void* context, CFsmEvent* event); static bool OnHandshakeResponse(void* context, CFsmEvent* event); static bool OnAuthenticateRequest(void* context, CFsmEvent* event); static bool OnAuthenticate(void* context, CFsmEvent* event); static bool OnChat(void* context, CFsmEvent* event); static bool OnReady(void* context, CFsmEvent* event); static bool OnGameSetup(void* context, CFsmEvent* event); static bool OnPlayerAssignment(void* context, CFsmEvent* event); static bool OnInGame(void* context, CFsmEvent* event); static bool OnGameStart(void* context, CFsmEvent* event); static bool OnJoinSyncStart(void* context, CFsmEvent* event); static bool OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event); static bool OnRejoined(void* context, CFsmEvent* event); static bool OnKicked(void* context, CFsmEvent* event); static bool OnClientTimeout(void* context, CFsmEvent* event); static bool OnClientPerformance(void* context, CFsmEvent* event); static bool OnClientsLoading(void* context, CFsmEvent* event); static bool OnClientPaused(void* context, CFsmEvent* event); static bool OnLoadedGame(void* context, CFsmEvent* event); /** * Take ownership of a session object, and use it for all network communication. */ void SetAndOwnSession(CNetClientSession* session); /** * Push a message onto the GUI queue listing the current player assignments. */ void PostPlayerAssignmentsToScript(); CGame *m_Game; CStrW m_UserName; CStr m_HostingPlayerName; /// Current network session (or NULL if not connected) CNetClientSession* m_Session; + std::thread m_PollingThread; + /// Turn manager associated with the current game (or NULL if we haven't started the game yet) CNetClientTurnManager* m_ClientTurnManager; /// Unique-per-game identifier of this client, used to identify the sender of simulation commands u32 m_HostID; /// True if the player is currently rejoining or has already rejoined the game. bool m_Rejoin; /// Whether to prevent the client of the host from timing out bool m_IsLocalClient; /// Latest copy of game setup attributes heard from the server JS::PersistentRootedValue m_GameAttributes; /// Latest copy of player assignments heard from the server PlayerAssignmentMap m_PlayerAssignments; /// Globally unique identifier to distinguish users beyond the lifetime of a single network session CStr m_GUID; /// Queue of messages for GuiPoll std::deque> m_GuiMessageQueue; /// Serialized game state received when joining an in-progress game std::string m_JoinSyncBuffer; /// Time when the server was last checked for timeouts and bad latency std::time_t m_LastConnectionCheck; }; /// Global network client for the standard game extern CNetClient *g_NetClient; #endif // NETCLIENT_H Index: ps/trunk/source/network/NetServer.cpp =================================================================== --- ps/trunk/source/network/NetServer.cpp (revision 24517) +++ ps/trunk/source/network/NetServer.cpp (revision 24518) @@ -1,1634 +1,1625 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "NetServer.h" #include "NetClient.h" #include "NetMessage.h" #include "NetSession.h" #include "NetServerTurnManager.h" #include "NetStats.h" #include "lib/external_libraries/enet.h" #include "lib/types.h" #include "network/StunClient.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/GUID.h" #include "ps/Profile.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" #if CONFIG2_MINIUPNPC #include #include #include #include #endif #include /** * Number of peers to allocate for the enet host. * Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096). * * At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason. */ #define MAX_CLIENTS 41 #define DEFAULT_SERVER_NAME L"Unnamed Server" -static const int CHANNEL_COUNT = 1; +constexpr int CHANNEL_COUNT = 1; /** * enet_host_service timeout (msecs). * Smaller numbers may hurt performance; larger numbers will * hurt latency responding to messages from game thread. */ static const int HOST_SERVICE_TIMEOUT = 50; CNetServer* g_NetServer = NULL; static CStr DebugName(CNetServerSession* session) { if (session == NULL) return "[unknown host]"; if (session->GetGUID().empty()) return "[unauthed host]"; return "[" + session->GetGUID().substr(0, 8) + "...]"; } /** * Async task for receiving the initial game state to be forwarded to another * client that is rejoining an in-progress network game. */ class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask { NONCOPYABLE(CNetFileReceiveTask_ServerRejoin); public: CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID) : m_Server(server), m_RejoinerHostID(hostID) { } virtual void OnComplete() { // We've received the game state from an existing player - now // we need to send it onwards to the newly rejoining player // Find the session corresponding to the rejoining host (if any) CNetServerSession* session = NULL; for (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; CJoinSyncStartMessage message; session->SendMessage(&message); } private: CNetServerWorker& m_Server; u32 m_RejoinerHostID; }; /* * XXX: We use some non-threadsafe functions from the worker thread. * See http://trac.wildfiregames.com/ticket/654 */ CNetServerWorker::CNetServerWorker(bool useLobbyAuth, int autostartPlayers) : m_AutostartPlayers(autostartPlayers), m_LobbyAuth(useLobbyAuth), m_Shutdown(false), m_ScriptInterface(NULL), m_NextHostID(1), m_Host(NULL), m_HostGUID(), m_Stats(NULL), m_LastConnectionCheck(0) { m_State = SERVER_STATE_UNCONNECTED; m_ServerTurnManager = NULL; m_ServerName = DEFAULT_SERVER_NAME; } 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; } bool CNetServerWorker::SetupConnection(const u16 port) { ENSURE(m_State == SERVER_STATE_UNCONNECTED); ENSURE(!m_Host); // Bind to default host ENetAddress addr; addr.host = ENET_HOST_ANY; addr.port = port; // Create ENet server m_Host = enet_host_create(&addr, MAX_CLIENTS, CHANNEL_COUNT, 0, 0); if (!m_Host) { LOGERROR("Net server: enet_host_create failed"); return false; } m_Stats = new CNetStatsTable(); if (CProfileViewer::IsInitialised()) g_ProfileViewer.AddRootTable(m_Stats); m_State = SERVER_STATE_PREGAME; // Launch the worker thread m_WorkerThread = std::thread(RunThread, this); #if CONFIG2_MINIUPNPC // Launch the UPnP thread m_UPnPThread = std::thread(SetupUPnP); #endif return true; } #if CONFIG2_MINIUPNPC void CNetServerWorker::SetupUPnP() { debug_SetThreadName("UPnP"); // Values we want to set. char psPort[6]; sprintf_s(psPort, ARRAY_SIZE(psPort), "%d", PS_DEFAULT_PORT); const char* leaseDuration = "0"; // Indefinite/permanent lease duration. const char* description = "0AD Multiplayer"; const char* protocall = "UDP"; char internalIPAddress[64]; char externalIPAddress[40]; // Variables to hold the values that actually get set. char intClient[40]; char intPort[6]; char duration[16]; // Intermediate variables. bool allocatedUrls = false; struct UPNPUrls urls; struct IGDdatas data; struct UPNPDev* devlist = NULL; // Make sure everything is properly freed. std::function freeUPnP = [&allocatedUrls, &urls, &devlist]() { if (allocatedUrls) FreeUPNPUrls(&urls); freeUPNPDevlist(devlist); // IGDdatas does not need to be freed according to UPNP_GetIGDFromUrl }; // Cached root descriptor URL. std::string rootDescURL; CFG_GET_VAL("network.upnprootdescurl", rootDescURL); if (!rootDescURL.empty()) LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL.c_str()); int ret = 0; // Try a cached URL first if (!rootDescURL.empty() && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress))) { LOGMESSAGE("Net server: using cached IGD = %s", urls.controlURL); ret = 1; } // No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds. #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14 else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL) #else else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL) #endif { ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress)); allocatedUrls = ret != 0; // urls is allocated on non-zero return values } else { LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL."); freeUPnP(); return; } switch (ret) { case 0: LOGMESSAGE("Net server: No IGD found"); break; case 1: LOGMESSAGE("Net server: found valid IGD = %s", urls.controlURL); break; case 2: LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls.controlURL); break; case 3: LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls.controlURL); break; default: debug_warn(L"Unrecognized return value from UPNP_GetValidIGD"); } // Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance. ret = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIPAddress); if (ret != UPNPCOMMAND_SUCCESS) { LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret, strupnperror(ret)); freeUPnP(); return; } LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress); // Try to setup port forwarding. ret = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, psPort, psPort, internalIPAddress, description, protocall, 0, leaseDuration); if (ret != UPNPCOMMAND_SUCCESS) { LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)", psPort, psPort, internalIPAddress, ret, strupnperror(ret)); freeUPnP(); return; } // Check that the port was actually forwarded. ret = UPNP_GetSpecificPortMappingEntry(urls.controlURL, data.first.servicetype, psPort, protocall, #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10 NULL/*remoteHost*/, #endif intClient, intPort, NULL/*desc*/, NULL/*enabled*/, duration); if (ret != UPNPCOMMAND_SUCCESS) { LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret, strupnperror(ret)); freeUPnP(); return; } LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)", externalIPAddress, psPort, protocall, intClient, intPort, duration); // Cache root descriptor URL to try to avoid discovery next time. g_ConfigDB.SetValueString(CFG_USER, "network.upnprootdescurl", urls.controlURL); g_ConfigDB.WriteValueToFile(CFG_USER, "network.upnprootdescurl", urls.controlURL); LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls.controlURL); freeUPnP(); } #endif // CONFIG2_MINIUPNPC bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message) { ENSURE(m_Host); CNetServerSession* session = static_cast(peer->data); return CNetHost::SendMessage(message, peer, DebugName(session).c_str()); } bool CNetServerWorker::Broadcast(const CNetMessage* message, const std::vector& targetStates) { ENSURE(m_Host); bool ok = true; // TODO: this does lots of repeated message serialisation if we have lots // of remote peers; could do it more efficiently if that's a real problem for (CNetServerSession* session : m_Sessions) if (std::find(targetStates.begin(), targetStates.end(), static_cast(session->GetCurrState())) != targetStates.end() && !session->SendMessage(message)) ok = false; return ok; } void CNetServerWorker::RunThread(CNetServerWorker* data) { debug_SetThreadName("NetServer"); data->Run(); } void CNetServerWorker::Run() { // The script context uses the profiler and therefore the thread must be registered before the context is created g_Profiler2.RegisterCurrentThread("Net server"); // We create a new ScriptContext for this network thread, with a single ScriptInterface. shared_ptr netServerContext = ScriptContext::CreateContext(); m_ScriptInterface = new ScriptInterface("Engine", "Net server", netServerContext); m_GameAttributes.init(m_ScriptInterface->GetGeneralJSContext(), JS::UndefinedValue()); while (true) { if (!RunStep()) break; // Implement autostart mode if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers) StartGame(); // 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_GameAttributesQueue); newLobbyAuths.swap(m_LobbyAuthQueue); newTurnLength.swap(m_TurnLengthQueue); } if (!newGameAttributes.empty()) { JS::RootedValue gameAttributesVal(rq.cx); GetScriptInterface().ParseJSON(newGameAttributes.back(), &gameAttributesVal); UpdateGameAttributes(&gameAttributesVal); } if (!newTurnLength.empty()) SetTurnLength(newTurnLength.back()); // Do StartGame last, so we have the most up-to-date game attributes when we start if (!newStartGame.empty()) StartGame(); 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 > DEFAULT_TURN_LENGTH_MP) { CClientPerformanceMessage* msg = new CClientPerformanceMessage(); CClientPerformanceMessage::S_m_Clients client; client.m_GUID = m_Sessions[i]->GetGUID(); client.m_MeanRTT = meanRTT; msg->m_Clients.push_back(client); message = msg; } // Send to all clients except the affected one // (since that will show the locally triggered warning instead). // 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*)&OnStartGame, 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()); if (m_HostGUID.empty() && session->IsLocalClient()) m_HostGUID = session->GetGUID(); CGameSetupMessage gameSetupMessage(GetScriptInterface()); gameSetupMessage.m_Data = m_GameAttributes; session->SendMessage(&gameSetupMessage); CPlayerAssignmentMessage assignMessage; ConstructPlayerAssignmentMessage(assignMessage); session->SendMessage(&assignMessage); } void CNetServerWorker::OnUserLeave(CNetServerSession* session) { std::vector::iterator pausing = std::find(m_PausingPlayers.begin(), m_PausingPlayers.end(), session->GetGUID()); if (pausing != m_PausingPlayers.end()) m_PausingPlayers.erase(pausing); RemovePlayer(session->GetGUID()); if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING) m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers // TODO: ought to switch the player controlled by that client // back to AI control, or something? } void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name) { // Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1) std::set usedIDs; for (const std::pair& p : m_PlayerAssignments) if (p.second.m_Enabled && p.second.m_PlayerID != -1) usedIDs.insert(p.second.m_PlayerID); // If the player is rejoining after disconnecting, try to give them // back their old player ID i32 playerID = -1; // Try to match GUID first for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it) { if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end()) { playerID = it->second.m_PlayerID; m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now goto found; } } // Try to match username next for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it) { if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end()) { playerID = it->second.m_PlayerID; m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now goto found; } } // Otherwise leave the player ID as -1 (observer) and let gamesetup change it as needed. found: PlayerAssignment assignment; assignment.m_Enabled = true; assignment.m_Name = name; assignment.m_PlayerID = playerID; assignment.m_Status = 0; m_PlayerAssignments[guid] = assignment; // Send the new assignments to all currently active players // (which does not include the one that's just joining) SendPlayerAssignments(); } void CNetServerWorker::RemovePlayer(const CStr& guid) { m_PlayerAssignments[guid].m_Enabled = false; SendPlayerAssignments(); } void CNetServerWorker::ClearAllPlayerReady() { for (std::pair& p : m_PlayerAssignments) if (p.second.m_Status != 2) p.second.m_Status = 0; SendPlayerAssignments(); } void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban) { // Find the user with that name std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(), [&](CNetServerSession* session) { return session->GetUserName() == playerName; }); // and return if no one or the host has that name if (it == m_Sessions.end() || (*it)->GetGUID() == m_HostGUID) 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; } // 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; } // TODO: check server password etc? u32 newHostID = server.m_NextHostID++; session->SetUserName(username); session->SetHostID(newHostID); session->SetLocalClient(message->m_IsLocalClient); CAuthenticateResultMessage authenticateResult; authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK; authenticateResult.m_HostID = newHostID; authenticateResult.m_Message = L"Logged in"; session->SendMessage(&authenticateResult); server.OnUserJoin(session); if (isRejoining) { // Request a copy of the current game state from an existing player, // so we can send it on to the new player // Assume session 0 is most likely the local player, so they're // the most efficient client to request a copy from CNetServerSession* sourceSession = server.m_Sessions.at(0); - session->SetLongTimeout(true); - sourceSession->GetFileTransferer().StartTask( shared_ptr(new CNetFileReceiveTask_ServerRejoin(server, newHostID)) ); session->SetNextState(NSS_JOIN_SYNCING); } return true; } bool CNetServerWorker::OnSimulationCommand(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SIMULATION_COMMAND); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CSimulationMessage* message = (CSimulationMessage*)event->GetParamRef(); // Ignore messages sent by one player on behalf of another player // unless cheating is enabled bool cheatsEnabled = false; const ScriptInterface& scriptInterface = server.GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue settings(rq.cx); scriptInterface.GetProperty(server.m_GameAttributes, "settings", &settings); if (scriptInterface.HasProperty(settings, "CheatsEnabled")) scriptInterface.GetProperty(settings, "CheatsEnabled", cheatsEnabled); PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID()); // When cheating is disabled, fail if the player the message claims to // represent does not exist or does not match the sender's player name if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != message->m_Player)) return true; // Send it back to all clients that have finished // the loading screen (and the synchronization when rejoining) server.Broadcast(message, { NSS_INGAME }); // Save all the received commands if (server.m_SavedCommands.size() < message->m_Turn + 1) server.m_SavedCommands.resize(message->m_Turn + 1); server.m_SavedCommands[message->m_Turn].push_back(*message); // TODO: we shouldn't send the message back to the client that first sent it return true; } bool CNetServerWorker::OnSyncCheck(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SYNC_CHECK); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CSyncCheckMessage* message = (CSyncCheckMessage*)event->GetParamRef(); server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, message->m_Turn, message->m_Hash); return true; } bool CNetServerWorker::OnEndCommandBatch(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CEndCommandBatchMessage* message = (CEndCommandBatchMessage*)event->GetParamRef(); // The turn-length field is ignored server.m_ServerTurnManager->NotifyFinishedClientCommands(*session, message->m_Turn); return true; } bool CNetServerWorker::OnChat(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CHAT); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CChatMessage* message = (CChatMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); server.Broadcast(message, { NSS_PREGAME, NSS_INGAME }); return true; } bool CNetServerWorker::OnReady(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_READY); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); // Occurs if a client presses not-ready // in the very last moment before the hosts starts the game if (server.m_State == SERVER_STATE_LOADING) return true; CReadyMessage* message = (CReadyMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); server.Broadcast(message, { NSS_PREGAME }); server.m_PlayerAssignments[message->m_GUID].m_Status = message->m_Status; return true; } bool CNetServerWorker::OnClearAllReady(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CLEAR_ALL_READY); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); if (session->GetGUID() == server.m_HostGUID) 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; if (session->GetGUID() == server.m_HostGUID) { CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef(); server.UpdateGameAttributes(&(message->m_Data)); } 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_HostGUID) { CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef(); server.AssignPlayer(message->m_PlayerID, message->m_GUID); } return true; } bool CNetServerWorker::OnStartGame(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_START); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); if (session->GetGUID() == server.m_HostGUID) server.StartGame(); return true; } bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetServerSession* loadedSession = (CNetServerSession*)context; CNetServerWorker& server = loadedSession->GetServer(); - loadedSession->SetLongTimeout(false); - // We're in the loading state, so wait until every client has loaded // before starting the game ENSURE(server.m_State == SERVER_STATE_LOADING); if (server.CheckGameLoadStatus(loadedSession)) return true; CClientsLoadingMessage message; // We always send all GUIDs of clients in the loading state // so that we don't have to bother about switching GUI pages for (CNetServerSession* session : server.m_Sessions) if (session->GetCurrState() != NSS_INGAME && loadedSession->GetGUID() != session->GetGUID()) { CClientsLoadingMessage::S_m_Clients client; client.m_GUID = session->GetGUID(); message.m_Clients.push_back(client); } // Send to the client who has loaded the game but did not reach the NSS_INGAME state yet loadedSession->SendMessage(&message); server.Broadcast(&message, { NSS_INGAME }); return true; } bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event) { // A client rejoining an in-progress game has now finished loading the // map and deserialized the initial state. // The simulation may have progressed since then, so send any subsequent // commands to them and set them as an active player so they can participate // in all future turns. // // (TODO: if it takes a long time for them to receive and execute all these // commands, the other players will get frozen for that time and may be unhappy; // we could try repeating this process a few times until the client converges // on the up-to-date state, before setting them as active.) ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef(); u32 turn = message->m_CurrentTurn; u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn(); // Send them all commands received since their saved state, // and turn-ended messages for any turns that have already been processed for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i) { if (i < server.m_SavedCommands.size()) for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j) session->SendMessage(&server.m_SavedCommands[i][j]); if (i <= readyTurn) { CEndCommandBatchMessage endMessage; endMessage.m_Turn = i; endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i); session->SendMessage(&endMessage); } } // Tell the turn manager to expect commands from this new client server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn); // 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); } - session->SetLongTimeout(false); - 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_HostGUID) { 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() { for (std::pair& player : m_PlayerAssignments) if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0) { LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player.second.m_Name).c_str()); return; } m_ServerTurnManager = new CNetServerTurnManager(*this); for (CNetServerSession* session : m_Sessions) - { m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0); // TODO: only for non-observers - session->SetLongTimeout(true); - } m_State = SERVER_STATE_LOADING; // Send the final setup state to all clients UpdateGameAttributes(&m_GameAttributes); // Remove players and observers that are not present when the game starts for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end();) if (it->second.m_Enabled) ++it; else it = m_PlayerAssignments.erase(it); SendPlayerAssignments(); CGameStartMessage gameStart; Broadcast(&gameStart, { NSS_PREGAME }); } void CNetServerWorker::UpdateGameAttributes(JS::MutableHandleValue attrs) { m_GameAttributes = attrs; if (!m_Host) return; CGameSetupMessage gameSetupMessage(GetScriptInterface()); gameSetupMessage.m_Data = m_GameAttributes; Broadcast(&gameSetupMessage, { NSS_PREGAME }); } CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original) { const size_t MAX_LENGTH = 32; CStrW name = original; name.Replace(L"[", L"{"); // remove GUI tags name.Replace(L"]", L"}"); // remove for symmetry // Restrict the length if (name.length() > MAX_LENGTH) name = name.Left(MAX_LENGTH); // Don't allow surrounding whitespace name.Trim(PS_TRIM_BOTH); // Don't allow empty name if (name.empty()) name = L"Anonymous"; return name; } CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original) { CStrW name = original; // Try names "Foo", "Foo (2)", "Foo (3)", etc size_t id = 2; while (true) { bool unique = true; for (const CNetServerSession* session : m_Sessions) { if (session->GetUserName() == name) { unique = false; break; } } if (unique) return name; name = original + L" (" + CStrW::FromUInt(id++) + L")"; } } void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port) { if (m_Host) StunClient::SendHolePunchingMessages(*m_Host, ipStr, port); } CNetServer::CNetServer(bool useLobbyAuth, int autostartPlayers) : m_Worker(new CNetServerWorker(useLobbyAuth, autostartPlayers)), m_LobbyAuth(useLobbyAuth) { } CNetServer::~CNetServer() { delete m_Worker; } bool CNetServer::UseLobbyAuth() const { return m_LobbyAuth; } bool CNetServer::SetupConnection(const u16 port) { return m_Worker->SetupConnection(port); } void CNetServer::StartGame() { std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_StartGameQueue.push_back(true); } void CNetServer::UpdateGameAttributes(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface) { // Pass the attributes as JSON, since that's the easiest safe // cross-thread way of passing script data std::string attrsJSON = scriptInterface.StringifyJSON(attrs, false); std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_GameAttributesQueue.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 24517) +++ ps/trunk/source/network/NetSession.cpp (revision 24518) @@ -1,302 +1,290 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" + #include "NetSession.h" + #include "NetClient.h" -#include "NetServer.h" #include "NetMessage.h" +#include "NetServer.h" #include "NetStats.h" -#include "lib/external_libraries/enet.h" #include "ps/CLogger.h" -#include "ps/ConfigDB.h" #include "ps/Profile.h" #include "scriptinterface/ScriptInterface.h" -const u32 NETWORK_WARNING_TIMEOUT = 2000; - -const u32 MAXIMUM_HOST_TIMEOUT = std::numeric_limits::max(); +constexpr int NETCLIENT_POLL_TIMEOUT = 50; -static const int CHANNEL_COUNT = 1; - -// Only disable long timeouts after a packet from the remote enet peer has been processed. -// Otherwise a long timeout can still be in progress when disabling it here. -void SetEnetLongTimeout(ENetPeer* peer, bool isLocalClient, bool enabled) -{ -#if (ENET_VERSION >= ENET_VERSION_CREATE(1, 3, 4)) - if (!peer || isLocalClient) - return; - - if (enabled) - { - u32 timeout; - CFG_GET_VAL("network.gamestarttimeout", timeout); - enet_peer_timeout(peer, 0, timeout, timeout); - } - else - enet_peer_timeout(peer, 0, 0, 0); -#endif -} +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_IsLocalClient(false) + m_Client(client), m_FileTransferer(this), m_Host(nullptr), m_Server(nullptr), + m_Stats(nullptr), m_IsLocalClient(false), 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, const bool isLocalClient, 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); 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_IsLocalClient = isLocalClient; - // Prevent the local client of the host from timing out too quickly. -#if (ENET_VERSION >= ENET_VERSION_CREATE(1, 3, 4)) - if (isLocalClient) - enet_peer_timeout(peer, 1, MAXIMUM_HOST_TIMEOUT, MAXIMUM_HOST_TIMEOUT); -#endif - m_Stats = new CNetStatsTable(m_Server); if (CProfileViewer::IsInitialised()) g_ProfileViewer.AddRootTable(m_Stats); return true; } -void CNetClientSession::Disconnect(NetDisconnectReason reason) +void CNetClientSession::RunNetLoop(CNetClientSession* session) { - if (reason == NDR_UNKNOWN) - LOGWARNING("Disconnecting from the server without communicating the disconnect reason!"); + ENSURE(!session->m_LoopRunning); + session->m_LoopRunning = true; - ENSURE(m_Host && m_Server); + debug_SetThreadName("NetClientSession loop"); - // TODO: ought to do reliable async disconnects, probably - enet_peer_disconnect_now(m_Server, static_cast(reason)); - enet_host_destroy(m_Host); + while (!session->m_ShouldShutdown) + { + ENSURE(session->m_Host && session->m_Server); - m_Host = NULL; - m_Server = NULL; + 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; + } - SAFE_DELETE(m_Stats); + 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() { - PROFILE3("net client poll"); + ENetEvent event; - ENSURE(m_Host && m_Server); + // Use the timeout to make the thread wait and save CPU time. + if (enet_host_service(m_Host, &event, NETCLIENT_POLL_TIMEOUT) <= 0) + return; - m_FileTransferer.Poll(); + if (event.type == ENET_EVENT_TYPE_CONNECT) + { + ENSURE(event.peer == m_Server); - ENetEvent event; - while (enet_host_service(m_Host, &event, 0) > 0) + // 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_IncomingMessages.push(event); + } + else if (event.type == ENET_EVENT_TYPE_DISCONNECT) { - switch (event.type) - { - case ENET_EVENT_TYPE_CONNECT: - { - ENSURE(event.peer == m_Server); + ENSURE(event.peer == m_Server); - // Report the server address - char hostname[256] = "(error)"; - enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname)); - LOGMESSAGE("Net client: Connected to %s:%u", hostname, (unsigned int)event.peer->address.port); + // Report immediately. + LOGMESSAGE("Net client: Disconnected"); - m_Client.HandleConnect(); + m_IncomingMessages.push(event); + } + else if (event.type == ENET_EVENT_TYPE_RECEIVE) + m_IncomingMessages.push(event); +} - break; - } +void CNetClientSession::Flush() +{ + ENetPacket* packet; + while (m_OutgoingMessages.pop(packet)) + if (enet_peer_send(m_Server, CNetHost::DEFAULT_CHANNEL, packet) < 0) + LOGERROR("NetClient: Failed to send packet to server"); - case ENET_EVENT_TYPE_DISCONNECT: - { - ENSURE(event.peer == m_Server); + enet_host_flush(m_Host); +} - LOGMESSAGE("Net client: Disconnected"); +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); - return; + break; } - - case ENET_EVENT_TYPE_RECEIVE: + 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); - - delete msg; } - + // Thread-safe enet_packet_destroy(event.packet); - - break; - } - - case ENET_EVENT_TYPE_NONE: - break; } } - } -void CNetClientSession::Flush() +bool CNetClientSession::SendMessage(const CNetMessage* message) { - PROFILE3("net client flush"); - ENSURE(m_Host && m_Server); - enet_host_flush(m_Host); -} + // Thread-safe. + ENetPacket* packet = CNetHost::CreatePacket(message); + if (!packet) + return false; -bool CNetClientSession::SendMessage(const CNetMessage* message) -{ - ENSURE(m_Host && m_Server); + if (!m_OutgoingMessages.push(packet)) + { + LOGERROR("NetClient: Failed to push message on the outgoing queue."); + return false; + } - return CNetHost::SendMessage(message, m_Server, "server"); + return true; } u32 CNetClientSession::GetLastReceivedTime() const { if (!m_Server) return 0; - return enet_time_get() - m_Server->lastReceiveTime; + return m_LastReceivedTime; } u32 CNetClientSession::GetMeanRTT() const { if (!m_Server) return 0; - return m_Server->roundTripTime; -} - -void CNetClientSession::SetLongTimeout(bool enabled) -{ - SetEnetLongTimeout(m_Server, m_IsLocalClient, enabled); + return m_MeanRTT; } CNetServerSession::CNetServerSession(CNetServerWorker& server, ENetPeer* peer) : m_Server(server), m_FileTransferer(this), m_Peer(peer), m_IsLocalClient(false), 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); } bool CNetServerSession::IsLocalClient() const { return m_IsLocalClient; } void CNetServerSession::SetLocalClient(bool isLocalClient) { m_IsLocalClient = isLocalClient; if (!isLocalClient) return; - - // Prevent the local client of the host from timing out too quickly -#if (ENET_VERSION >= ENET_VERSION_CREATE(1, 3, 4)) - enet_peer_timeout(m_Peer, 0, MAXIMUM_HOST_TIMEOUT, MAXIMUM_HOST_TIMEOUT); -#endif -} - -void CNetServerSession::SetLongTimeout(bool enabled) -{ - SetEnetLongTimeout(m_Peer, m_IsLocalClient, enabled); } Index: ps/trunk/source/network/NetSession.h =================================================================== --- ps/trunk/source/network/NetSession.h (revision 24517) +++ ps/trunk/source/network/NetSession.h (revision 24518) @@ -1,218 +1,229 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef NETSESSION_H #define NETSESSION_H +#include "lib/external_libraries/enet.h" #include "network/fsm.h" #include "network/NetFileTransfer.h" #include "network/NetHost.h" #include "ps/CStr.h" -/** - * Report the peer if we didn't receive a packet after this time (milliseconds). - */ -extern const u32 NETWORK_WARNING_TIMEOUT; +#include + +#include /** - * Maximum timeout of the local client of the host (milliseconds). + * Report the peer if we didn't receive a packet after this time (milliseconds). */ -extern const u32 MAXIMUM_HOST_TIMEOUT; +inline constexpr u32 NETWORK_WARNING_TIMEOUT = 2000; class CNetClient; class CNetServerWorker; class CNetStatsTable; typedef struct _ENetHost ENetHost; /** * @file * Network client/server sessions. * * Each session has two classes: CNetClientSession runs on the client, * and CNetServerSession runs on the server. * A client runs one session at once; a server typically runs many. */ /** * Interface for sessions to which messages can be sent. */ class INetSession { public: virtual ~INetSession() {} virtual bool SendMessage(const CNetMessage* message) = 0; }; /** * The client end of a network session. * Provides an abstraction of the network interface, allowing communication with the server. + * The NetClientSession is threaded, so all calls to the public interface must be thread-safe. */ class CNetClientSession : public INetSession { NONCOPYABLE(CNetClientSession); public: CNetClientSession(CNetClient& client); ~CNetClientSession(); bool Connect(const CStr& server, const u16 port, const bool isLocalClient, ENetHost* enetClient); /** - * Process queued incoming messages. + * The client NetSession is threaded to avoid getting timeouts if the main thread hangs. + * Call Connect() before starting this loop. */ - void Poll(); + static void RunNetLoop(CNetClientSession* session); /** - * Flush queued outgoing network messages. + * Shut down the net session. */ - void Flush(); + void Shutdown(); /** - * Disconnect from the server. - * Sends a disconnection notification to the server. + * Processes pending messages. */ - void Disconnect(NetDisconnectReason reason); + void ProcessPolledMessages(); /** - * Send a message to the server. + * Queue up a message to send to the server on the next Loop() call. */ - virtual bool SendMessage(const CNetMessage* message); + virtual bool SendMessage(const CNetMessage* message) override; /** * Number of milliseconds since the most recent packet of the server was received. */ u32 GetLastReceivedTime() const; /** * Average round trip time to the server. */ u32 GetMeanRTT() const; + CNetFileTransferer& GetFileTransferer() { return m_FileTransferer; } +private: /** - * Allows increasing the timeout to prevent drops during an expensive operation, - * and decreasing it back to normal afterwards. + * Process queued incoming messages. */ - void SetLongTimeout(bool longTimeout); + void Poll(); - CNetFileTransferer& GetFileTransferer() { return m_FileTransferer; } + /** + * Flush queued outgoing network messages. + */ + void Flush(); -private: CNetClient& m_Client; CNetFileTransferer m_FileTransferer; + // Net messages received and waiting for fetching. + boost::lockfree::queue m_IncomingMessages; + // Net messages to send on the next flush() call. + boost::lockfree::queue m_OutgoingMessages; + + // Wrapper around enet stats - those are atomic as the code is lock-free. + std::atomic m_LastReceivedTime; + std::atomic m_MeanRTT; + + // If this is true, calling Connect() or deleting the session is an error. + std::atomic m_LoopRunning; + std::atomic m_ShouldShutdown; + ENetHost* m_Host; ENetPeer* m_Server; CNetStatsTable* m_Stats; bool m_IsLocalClient; }; /** * The server's end of a network session. * Represents an abstraction of the state of the client, storing all the per-client data * needed by the server. * * Thread-safety: * - This is constructed and used by CNetServerWorker in the network server thread. */ class CNetServerSession : public CFsm, public INetSession { NONCOPYABLE(CNetServerSession); public: CNetServerSession(CNetServerWorker& server, ENetPeer* peer); CNetServerWorker& GetServer() { return m_Server; } const CStr& GetGUID() const { return m_GUID; } void SetGUID(const CStr& guid) { m_GUID = guid; } const CStrW& GetUserName() const { return m_UserName; } void SetUserName(const CStrW& name) { m_UserName = name; } u32 GetHostID() const { return m_HostID; } void SetHostID(u32 id) { m_HostID = id; } u32 GetIPAddress() const; /** * Whether this client is running in the same process as the server. */ bool IsLocalClient() const; /** * Number of milliseconds since the latest packet of that client was received. */ u32 GetLastReceivedTime() const; /** * Average round trip time to the client. */ u32 GetMeanRTT() const; /** * Sends a disconnection notification to the client, * and sends a NMT_CONNECTION_LOST message to the session FSM. * The server will receive a disconnection notification after a while. * The server will not receive any further messages sent via this session. */ void Disconnect(NetDisconnectReason reason); /** * Sends an unreliable disconnection notification to the client. * The server will not receive any disconnection notification. * The server will not receive any further messages sent via this session. */ void DisconnectNow(NetDisconnectReason reason); /** * Prevent timeouts for the client running in the same process as the server. */ void SetLocalClient(bool isLocalClient); /** - * Allows increasing the timeout to prevent drops during an expensive operation, - * and decreasing it back to normal afterwards. - */ - void SetLongTimeout(bool longTimeout); - - /** * Send a message to the client. */ virtual bool SendMessage(const CNetMessage* message); CNetFileTransferer& GetFileTransferer() { return m_FileTransferer; } private: CNetServerWorker& m_Server; CNetFileTransferer m_FileTransferer; ENetPeer* m_Peer; CStr m_GUID; CStrW m_UserName; u32 m_HostID; bool m_IsLocalClient; }; #endif // NETSESSION_H