diff --git a/.efrocachemap b/.efrocachemap index 398ea3ea..f813a0e9 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -3932,24 +3932,24 @@ "assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450", "assets/build/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/1c/e1/4a1a2eddda2f4aebd5f8b64ab08e", "assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f", - "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/62/e1/70ff36467d1875af3e0e38da754c", - "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/5c/83/e1f9e8db08f24a1d0b08958b9e09", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/59/ae/bc2f695f28eb3405415ea11c8083", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cb/ab/076371cc80fb408dfe2cbd4da8b0", - "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f0/70/203ffd8485f6e3b5432c70b556b3", - "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/f5/51/9b37b71adfaa2d5ca706bc168b2f", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/5d/87/089a0508c2876e6090f337a6a1c6", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/fe/49/c33812c8596e946c4b7ff4a70176", - "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/65/c0/cede9d63e3c3fd3148b1d25ba62f", - "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/4b/e0/26d2281f316b0f704a6a404b030b", - "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/a7/b8/988ab1f0d337516c034301ce219a", - "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/69/80/9068d8f99a060c625abee7b49184", - "build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4f/4c/8590730e5d1cdae456c1b734a2a1", - "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/44/e5/d1c3162e114e51a5b5b826c2ec7c", - "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ee/21/8fab3da6b974cf323024d076b609", - "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/20/5d/881676243c5f44bdca677497b4d4", - "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/90/a3/e265c556587921cdfd8c753b592f", - "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4c/5e/80ac1a7a4a75de8869755aa7cbc1", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b0/4a/2144949c2cb6c3ef7d99c4409bfb", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f8/ce/078603417240c7f8a7f067d55a6f" + "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/46/08/c5c18d49571ed38eebf9596704c5", + "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/1c/2d/9858a8c8735debb23fe24e2efe4b", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3f/e9/7600ca36570a7dcf5af9647a2e21", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b9/e4/ec93fdb61177cda5ff992506ac17", + "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/df/fb/05e0398c5bbe64fad410ea375356", + "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/25/eb/ea4a1b0694ad2cf60352defc69af", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/26/50/422233084047a81e036c132bde39", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3f/4d/3447d7b2f7a8bc64ca7bdd6536f1", + "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/38/35/fa7173464f9527e374a0ce94b3bd", + "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/cc/b1/a94d7f123b22ed23e00f40e579d1", + "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/53/4a/02860b49bda65c53123447c69b90", + "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/fe/e2/1c603aed8baf632d28226b2031e6", + "build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/54/a1/e5a2906b6bd18229f21f5a5d0876", + "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b9/e7/ac87044e0eac75551bcacdf2e41d", + "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/18/c2/3a457694a20c7dc22eb0d5cb67fc", + "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3a/0c/96264568710c8e6a84af77a4f0d9", + "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/51/96/56697bf4a444ebe66b28c616e279", + "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2c/5b/303bfdb73180faf55ba78fa2bcc8", + "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/09/83/ecaaf27743299eb80fd6ae704f23", + "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/22/9f/c6686359ba318f695a0a0900f2c2" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f253e9f..76d61fc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ### 1.5.29 (20246) - Exposed ba method/class initing in public C++ layer. - The 'restart' and 'shutdown' commands in the server script now default to immediate=True +- Wired up 'clean_exit_minutes', 'unclean_exit_minutes', and 'idle_exit_minutes' options in the server config ### 1.5.28 (20239) - Simplified ba.enum_by_value() diff --git a/assets/src/ba_data/python/_ba.py b/assets/src/ba_data/python/_ba.py index 0db90ad6..9d614a65 100644 --- a/assets/src/ba_data/python/_ba.py +++ b/assets/src/ba_data/python/_ba.py @@ -2079,7 +2079,7 @@ def get_idle_time() -> int: (internal) - Returns the amount of time since any game input has been processed + Returns the amount of time since any game input has been received. """ return int() diff --git a/assets/src/server/ballisticacore_server.py b/assets/src/server/ballisticacore_server.py index 4711204f..5be8849e 100755 --- a/assets/src/server/ballisticacore_server.py +++ b/assets/src/server/ballisticacore_server.py @@ -124,7 +124,7 @@ class ServerManagerApp: # Python will handle SIGINT for us (as KeyboardInterrupt) but we # need to register a SIGTERM handler so we have a chance to clean - # up our child-process when someone tells us to die. (and avoid + # up our subprocess when someone tells us to die. (and avoid # zombie processes) signal.signal(signal.SIGTERM, self._handle_term_signal) @@ -150,12 +150,17 @@ class ServerManagerApp: print(f'{Clr.SRED}Unexpected interpreter exception:' f' {exc} ({type(exc)}){Clr.RST}') + print(f'{Clr.CYN}Server manager shutting down...{Clr.RST}') + + if self._subprocess_thread.is_alive(): + print(f'{Clr.CYN}Waiting for subprocess exit...{Clr.RST}') + # Mark ourselves as shutting down and wait for the process to wrap up. self._done = True self._subprocess_thread.join() def cmd(self, statement: str) -> None: - """Exec a Python command on the current running server child-process. + """Exec a Python command on the current running server subprocess. Note that commands are executed asynchronously and no status or return value is accessible from this manager app. @@ -227,7 +232,7 @@ class ServerManagerApp: KickCommand(client_id=client_id, ban_time=ban_time)) def restart(self, immediate: bool = True) -> None: - """Restart the server child-process. + """Restart the server subprocess. This can be necessary for some config changes to take effect. By default, the server will exit immediately. If 'immediate' is passed @@ -246,7 +251,7 @@ class ServerManagerApp: IMMEDIATE_SHUTDOWN_TIME_LIMIT) def shutdown(self, immediate: bool = True) -> None: - """Shut down the server child-process and exit the wrapper + """Shut down the server subprocess and exit the wrapper By default, the server will exit immediately. If 'immediate' is passed as False, however, the server will instead exit at the next clean @@ -256,7 +261,8 @@ class ServerManagerApp: self._enqueue_server_command( ShutdownCommand(reason=ShutdownReason.NONE, immediate=immediate)) - # So we know to bail completely once this subprocess completes. + # An explicit shutdown means we know to bail completely once this + # subprocess completes. self._wrapper_shutdown_desired = True # If we're asking for an immediate shutdown but don't get one within @@ -304,7 +310,7 @@ class ServerManagerApp: raise SystemExit() def _run_server_cycle(self) -> None: - """Spin up the server child-process and run it until exit.""" + """Spin up the server subprocess and run it until exit.""" self._prep_subprocess_environment() @@ -317,7 +323,7 @@ class ServerManagerApp: # slight behavior tweaks. Hmm; should this be an argument instead? os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1' - print(f'{Clr.CYN}Launching server child-process...{Clr.RST}') + print(f'{Clr.CYN}Launching server subprocess...{Clr.RST}') binary_name = ('ballisticacore_headless.exe' if os.name == 'nt' else './ballisticacore_headless') self._subprocess = subprocess.Popen( @@ -332,14 +338,17 @@ class ServerManagerApp: finally: self._kill_subprocess() + # If we want to die completely after this subprocess has ended, + # tell the main thread to die. if self._wrapper_shutdown_desired: - # Note: need to only do this if main thread is still in the - # interpreter; otherwise it seems this can lead to deadlock. + + # Only do this if the main thread is not already waiting for + # us to die; otherwise it can lead to deadlock. if not self._done: self._done = True - # Our main thread is still be blocked in its prompt or - # whatnot; let it know it should die. + # This should break the main thread out of its blocking + # interpreter call. os.kill(os.getpid(), signal.SIGTERM) def _prep_subprocess_environment(self) -> None: @@ -356,6 +365,7 @@ class ServerManagerApp: bincfg['Port'] = self._config.port bincfg['Auto Balance Teams'] = self._config.auto_balance_teams bincfg['Show Tutorial'] = False + bincfg['Idle Exit Minutes'] = self._config.idle_exit_minutes with open('dist/ba_root/config.json', 'w') as outfile: outfile.write(json.dumps(bincfg)) @@ -384,11 +394,9 @@ class ServerManagerApp: self._subprocess.stdin.flush() def _run_subprocess_until_exit(self) -> None: - # pylint: disable=too-many-branches assert current_thread() is self._subprocess_thread assert self._subprocess is not None assert self._subprocess.stdin is not None - assert self._subprocess_launch_time is not None # Send the initial server config which should kick things off. # (but make sure its values are still valid first) @@ -414,35 +422,7 @@ class ServerManagerApp: self._subprocess_commands = [] # Request restarts/shut-downs for various reasons. - sincelaunch = time.time() - self._subprocess_launch_time - if (self._restart_minutes is not None and sincelaunch > - (self._restart_minutes * 60.0) - and not self._subprocess_sent_auto_restart): - print(f'{Clr.CYN}restart_minutes ({self._restart_minutes})' - f' elapsed; requesting child-process' - f' soft restart...{Clr.RST}') - self.restart() - self._subprocess_sent_auto_restart = True - if self._config.clean_exit_minutes is not None: - elapsed = (time.time() - self._launch_time) / 60.0 - if (elapsed > self._config.clean_exit_minutes - and not self._subprocess_sent_clean_exit): - print(f'{Clr.CYN}clean_exit_minutes' - f' ({self._config.clean_exit_minutes})' - f' elapsed; requesting child-process' - f' shutdown...{Clr.RST}') - self.shutdown(immediate=False) - self._subprocess_sent_clean_exit = True - if self._config.unclean_exit_minutes is not None: - elapsed = (time.time() - self._launch_time) / 60.0 - if (elapsed > self._config.unclean_exit_minutes - and not self._subprocess_sent_unclean_exit): - print(f'{Clr.CYN}unclean_exit_minutes' - f' ({self._config.unclean_exit_minutes})' - f' elapsed; requesting child-process' - f' shutdown...{Clr.RST}') - self.shutdown(immediate=True) - self._subprocess_sent_unclean_exit = True + self._request_shutdowns_or_restarts() # If they want to force-kill our subprocess, simply exit this # loop; the cleanup code will kill the process. @@ -457,17 +437,60 @@ class ServerManagerApp: if code == 0: clr = Clr.CYN slp = 0.0 + desc = '' + elif code == 154: + clr = Clr.CYN + slp = 0.0 + desc = ' (idle_exit_minutes reached)' + self._wrapper_shutdown_desired = True else: clr = Clr.SRED slp = 5.0 # Avoid super fast death loops. - print(f'{clr}Server child-process exited' - f' with code {code}.{Clr.RST}') + desc = '' + print(f'{clr}Server subprocess exited' + f' with code {code}{desc}.{Clr.RST}') self._reset_subprocess_vars() time.sleep(slp) break time.sleep(0.25) + def _request_shutdowns_or_restarts(self) -> None: + assert current_thread() is self._subprocess_thread + assert self._subprocess_launch_time is not None + sincelaunch = time.time() - self._subprocess_launch_time + + if (self._restart_minutes is not None and sincelaunch > + (self._restart_minutes * 60.0) + and not self._subprocess_sent_auto_restart): + print(f'{Clr.CYN}restart_minutes ({self._restart_minutes})' + f' elapsed; requesting subprocess' + f' soft restart...{Clr.RST}') + self.restart() + self._subprocess_sent_auto_restart = True + + if self._config.clean_exit_minutes is not None: + elapsed = (time.time() - self._launch_time) / 60.0 + if (elapsed > self._config.clean_exit_minutes + and not self._subprocess_sent_clean_exit): + print(f'{Clr.CYN}clean_exit_minutes' + f' ({self._config.clean_exit_minutes})' + f' elapsed; requesting subprocess' + f' shutdown...{Clr.RST}') + self.shutdown(immediate=False) + self._subprocess_sent_clean_exit = True + + if self._config.unclean_exit_minutes is not None: + elapsed = (time.time() - self._launch_time) / 60.0 + if (elapsed > self._config.unclean_exit_minutes + and not self._subprocess_sent_unclean_exit): + print(f'{Clr.CYN}unclean_exit_minutes' + f' ({self._config.unclean_exit_minutes})' + f' elapsed; requesting subprocess' + f' shutdown...{Clr.RST}') + self.shutdown(immediate=True) + self._subprocess_sent_unclean_exit = True + def _reset_subprocess_vars(self) -> None: self._subprocess = None self._subprocess_launch_time = None @@ -477,12 +500,12 @@ class ServerManagerApp: self._subprocess_force_kill_time = None def _kill_subprocess(self) -> None: - """End the server process if it still exists.""" + """End the server subprocess if it still exists.""" assert current_thread() is self._subprocess_thread if self._subprocess is None: return - print(f'{Clr.CYN}Stopping server process...{Clr.RST}') + print(f'{Clr.CYN}Stopping subprocess...{Clr.RST}') # First, ask it nicely to die and give it a moment. # If that doesn't work, bring down the hammer. @@ -492,7 +515,7 @@ class ServerManagerApp: except subprocess.TimeoutExpired: self._subprocess.kill() self._reset_subprocess_vars() - print(f'{Clr.CYN}Server process stopped.{Clr.RST}') + print(f'{Clr.CYN}Subprocess stopped.{Clr.RST}') def main() -> None: diff --git a/docs/ba_module.md b/docs/ba_module.md index 1674f850..3e06d91c 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2020-11-12 for Ballistica version 1.5.29 build 20248

+

last updated on 2020-11-15 for Ballistica version 1.5.29 build 20254

This page documents the Python classes and functions in the 'ba' module, which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!


diff --git a/src/ballistica/app/app_config.cc b/src/ballistica/app/app_config.cc index f5b60752..0868f75e 100644 --- a/src/ballistica/app/app_config.cc +++ b/src/ballistica/app/app_config.cc @@ -16,6 +16,10 @@ auto AppConfig::Entry::FloatValue() const -> float { throw Exception("not a float entry"); } +auto AppConfig::Entry::OptionalFloatValue() const -> std::optional { + throw Exception("not an optional float entry"); +} + auto AppConfig::Entry::StringValue() const -> std::string { throw Exception("not a string entry"); } @@ -32,6 +36,11 @@ auto AppConfig::Entry::DefaultFloatValue() const -> float { throw Exception("not a float entry"); } +auto AppConfig::Entry::DefaultOptionalFloatValue() const + -> std::optional { + throw Exception("not an optional float entry"); +} + auto AppConfig::Entry::DefaultStringValue() const -> std::string { throw Exception("not a string entry"); } @@ -78,6 +87,26 @@ class AppConfig::FloatEntry : public AppConfig::Entry { float default_value_{}; }; +class AppConfig::OptionalFloatEntry : public AppConfig::Entry { + public: + OptionalFloatEntry() = default; + OptionalFloatEntry(const char* name, std::optional default_value) + : Entry(name), default_value_(default_value) {} + auto GetType() const -> Type override { return Type::kOptionalFloat; } + auto Resolve() const -> std::optional { + return g_python->GetRawConfigValue(name().c_str(), default_value_); + } + auto OptionalFloatValue() const -> std::optional override { + return Resolve(); + } + auto DefaultOptionalFloatValue() const -> std::optional override { + return default_value_; + } + + private: + std::optional default_value_{}; +}; + class AppConfig::IntEntry : public AppConfig::Entry { public: IntEntry() = default; @@ -157,6 +186,9 @@ void AppConfig::SetupEntries() { float_entries_[FloatID::kGoogleVRRenderTargetScale] = FloatEntry("GVR Render Target Scale", gvrrts_default); + optional_float_entries_[OptionalFloatID::kIdleExitMinutes] = + OptionalFloatEntry("Idle Exit Minutes", std::optional()); + string_entries_[StringID::kResolutionAndroid] = StringEntry("Resolution (Android)", "Auto"); string_entries_[StringID::kTouchActionControlType] = @@ -220,6 +252,14 @@ auto AppConfig::Resolve(FloatID id) -> float { return i->second.Resolve(); } +auto AppConfig::Resolve(OptionalFloatID id) -> std::optional { + auto i = optional_float_entries_.find(id); + if (i == optional_float_entries_.end()) { + throw Exception("Invalid config entry"); + } + return i->second.Resolve(); +} + auto AppConfig::Resolve(StringID id) -> std::string { auto i = string_entries_.find(id); if (i == string_entries_.end()) { diff --git a/src/ballistica/app/app_config.h b/src/ballistica/app/app_config.h index 80d4d3f9..6e3303e7 100644 --- a/src/ballistica/app/app_config.h +++ b/src/ballistica/app/app_config.h @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -30,6 +31,11 @@ class AppConfig { kLast // Sentinel. }; + enum class OptionalFloatID { + kIdleExitMinutes, + kLast // Sentinel. + }; + enum class StringID { kResolutionAndroid, kTouchActionControlType, @@ -68,16 +74,18 @@ class AppConfig { class Entry { public: - enum class Type { kString, kInt, kFloat, kBool }; + enum class Type { kString, kInt, kFloat, kOptionalFloat, kBool }; Entry() = default; explicit Entry(const char* name) : name_(name) {} virtual auto GetType() const -> Type = 0; auto name() const -> const std::string& { return name_; } virtual auto FloatValue() const -> float; + virtual auto OptionalFloatValue() const -> std::optional; virtual auto StringValue() const -> std::string; virtual auto IntValue() const -> int; virtual auto BoolValue() const -> bool; virtual auto DefaultFloatValue() const -> float; + virtual auto DefaultOptionalFloatValue() const -> std::optional; virtual auto DefaultStringValue() const -> std::string; virtual auto DefaultIntValue() const -> int; virtual auto DefaultBoolValue() const -> bool; @@ -91,6 +99,7 @@ class AppConfig { // Given specific ids, returns resolved values (fastest access). auto Resolve(FloatID id) -> float; + auto Resolve(OptionalFloatID id) -> std::optional; auto Resolve(StringID id) -> std::string; auto Resolve(IntID id) -> int; auto Resolve(BoolID id) -> bool; @@ -113,6 +122,7 @@ class AppConfig { private: class StringEntry; class FloatEntry; + class OptionalFloatEntry; class IntEntry; class BoolEntry; template @@ -120,6 +130,7 @@ class AppConfig { void SetupEntries(); std::map entries_by_name_; std::map float_entries_; + std::map optional_float_entries_; std::map int_entries_; std::map string_entries_; std::map bool_entries_; diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc index ea12d21e..28f55776 100644 --- a/src/ballistica/ballistica.cc +++ b/src/ballistica/ballistica.cc @@ -21,7 +21,7 @@ namespace ballistica { // These are set automatically via script; don't change here. -const int kAppBuildNumber = 20249; +const int kAppBuildNumber = 20256; const char* kAppVersion = "1.5.29"; // Our standalone globals. diff --git a/src/ballistica/game/game.cc b/src/ballistica/game/game.cc index ce414e86..705c2a83 100644 --- a/src/ballistica/game/game.cc +++ b/src/ballistica/game/game.cc @@ -507,6 +507,27 @@ void Game::UpdateKickVote() { } } +void Game::HandleQuitOnIdle() { + if (idle_exit_minutes_) { + float idle_seconds{g_input->input_idle_time() * 0.001f}; + if (!idle_exiting_ && idle_seconds > (idle_exit_minutes_.value() * 60.0f)) { + idle_exiting_ = true; + + PushCall([this, idle_seconds] { + assert(InGameThread()); + + // Special exit value the wrapper script looks for to know we + // idled out. + g_app_globals->return_value = 154; + + // Just go through _ba.quit() + // FIXME: Shouldn't need to go out to the python layer here... + g_python->obj(Python::ObjID::kQuitCall).Call(); + }); + } + } +} + // Bring our scenes, real-time timers, etc up to date. void Game::Update() { assert(InGameThread()); @@ -521,6 +542,8 @@ void Game::Update() { g_input->Update(); UpdateKickVote(); + HandleQuitOnIdle(); + // Send the game roster to our clients if it's changed recently. if (game_roster_dirty_) { if (real_time > last_game_roster_send_time_ + 2500) { @@ -548,9 +571,9 @@ void Game::Update() { // TODO(ericf): On modern systems (VR and otherwise) we'll see 80hz, 90hz, // 120hz, 240hz, etc. It would be great to generalize this to gravitate - // towards clean step patterns in all cases, not just the 60hz and 90hz cases - // we handle now. In general we want stuff like 1,1,2,1,1,2,1,1,2, not - // 1,1,1,2,1,2,2,1,1. + // towards clean step patterns in all cases, not just the 60hz and 90hz + // cases we handle now. In general we want stuff like 1,1,2,1,1,2,1,1,2, + // not 1,1,1,2,1,2,2,1,1. // Figure out where our net-time *should* be getting to to match real-time. millisecs_t target_master_time = real_time + master_time_offset_; @@ -1212,7 +1235,7 @@ void Game::Draw() { g_graphics->BuildAndPushFrameDef(); // Now bring the game up to date. - // By doing this *after* shipping a new frame_def we're reducing the + // By doing this *after* shipping a new frame-def we're reducing the // chance of frame drops at the expense of adding a bit of visual latency. // Could maybe try to be smart about which to do first, but not sure // if its worth it. @@ -1403,6 +1426,9 @@ void Game::ApplyConfig() { g_app_config->Resolve(AppConfig::BoolID::kDisableCameraGyro); g_graphics->set_camera_gyro_explicitly_disabled(disable_camera_gyro); + idle_exit_minutes_ = + g_app_config->Resolve(AppConfig::OptionalFloatID::kIdleExitMinutes); + // Any platform-specific settings. g_platform->ApplyConfig(); } diff --git a/src/ballistica/game/game.h b/src/ballistica/game/game.h index 5f9e0735..4acd8ba5 100644 --- a/src/ballistica/game/game.h +++ b/src/ballistica/game/game.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -251,6 +252,7 @@ class Game : public Module { auto mark_game_roster_dirty() -> void { game_roster_dirty_ = true; } private: + auto HandleQuitOnIdle() -> void; auto InitSpecialChars() -> void; auto Draw() -> void; auto PartyInvite(const std::string& name, const std::string& invite_id) @@ -265,13 +267,6 @@ class Game : public Module { auto ScoresToBeatResponse(bool success, const std::list& scores, void* py_callback) -> void; -#if BA_VR_BUILD - VRHandsState vr_hands_state_; -#endif -#if BA_RIFT_BUILD - int rift_step_index_{}; -#endif - auto Prune() -> void; // Periodic pruning of dead stuff. auto Update() -> void; auto Process() -> void; @@ -284,6 +279,13 @@ class Game : public Module { auto GetGameRosterMessage() -> std::vector; auto Shutdown(bool soft) -> void; +#if BA_VR_BUILD + VRHandsState vr_hands_state_; +#endif +#if BA_RIFT_BUILD + int rift_step_index_{}; +#endif + std::unique_ptr connections_; std::list > banned_players_; std::list chat_messages_; @@ -314,6 +316,8 @@ class Game : public Module { std::unordered_map special_char_strings_; bool ran_app_launch_commands_{}; bool kick_idle_players_{}; + std::optional idle_exit_minutes_{}; + bool idle_exiting_{}; std::unique_ptr realtimers_; Timer* process_timer_{}; Timer* headless_update_timer_{}; diff --git a/src/ballistica/input/device/input_device.cc b/src/ballistica/input/device/input_device.cc index 68f18114..b36ebf76 100644 --- a/src/ballistica/input/device/input_device.cc +++ b/src/ballistica/input/device/input_device.cc @@ -268,7 +268,10 @@ void InputDevice::Update() { } void InputDevice::UpdateLastInputTime() { + // Keep our own individual time, and also let + // the overall input system know something happened. last_input_time_ = g_game->master_time(); + g_input->mark_input_active(); } void InputDevice::InputCommand(InputType type, float value) { diff --git a/src/ballistica/input/device/input_device.h b/src/ballistica/input/device/input_device.h index 6c852a51..9b97d18e 100644 --- a/src/ballistica/input/device/input_device.h +++ b/src/ballistica/input/device/input_device.h @@ -22,18 +22,18 @@ class InputDevice : public Object { ~InputDevice() override; /// Called when the device is attached/detached to a local player. - virtual void AttachToLocalPlayer(Player* player); - virtual void AttachToRemotePlayer(ConnectionToHost* connection_to_host, - int remote_player_id); - virtual void DetachFromPlayer(); + virtual auto AttachToLocalPlayer(Player* player) -> void; + virtual auto AttachToRemotePlayer(ConnectionToHost* connection_to_host, + int remote_player_id) -> void; + virtual auto DetachFromPlayer() -> void; /// Issues a command to the remote game to remove the player we're attached /// to. - void RemoveRemotePlayerFromGame(); + auto RemoveRemotePlayerFromGame() -> void; /// Return the (not necessarily unique) name of the input device. auto GetDeviceName() -> std::string; - virtual void ResetHeldStates(); + virtual auto ResetHeldStates() -> void; /// Return the default base player name for players using this input device. virtual auto GetDefaultPlayerName() -> std::string; @@ -69,10 +69,10 @@ class InputDevice : public Object { auto index() const -> int { return index_; } /// Read new control values from config. - virtual void UpdateMapping() {} + virtual auto UpdateMapping() -> void {} /// Called during the game loop - for manual button repeats, etc. - virtual void Update(); + virtual auto Update() -> void; /// Return client id or -1 if local. virtual auto GetClientID() const -> int; @@ -81,7 +81,7 @@ class InputDevice : public Object { virtual auto IsRemoteClient() const -> bool; #if BA_SDL_BUILD || BA_MINSDL_BUILD - virtual void HandleSDLEvent(const SDL_Event* e) {} + virtual auto HandleSDLEvent(const SDL_Event* e) -> void {} #endif virtual auto GetAllowsConfiguring() -> bool { return true; } @@ -121,26 +121,14 @@ class InputDevice : public Object { auto has_py_ref() -> bool { return (py_ref_ != nullptr); } auto last_input_time() const -> millisecs_t { return last_input_time_; } virtual auto ShouldBeHiddenFromUser() -> bool; - static void ResetRandomNames(); - - protected: - void ShipBufferIfFull(); - - /// Pass some input command on to whatever we're connected to - /// (player or remote-player). - void InputCommand(InputType type, float value = 0.0f); - - /// Called for all devices when they've successfully been added - /// to the input-device list, have a valid ID, name, etc. - virtual void ConnectionComplete() {} - - /// Subclasses should call this to request a player in the local game. - void RequestPlayer(); + static auto ResetRandomNames() -> void; /// Return a human-readable name for the device's type. /// This is used for display and also for storing configs/etc. virtual auto GetRawDeviceName() -> std::string { return "Input Device"; } + auto number() const { return number_; } + /// Return any extra description for the device. /// This portion is only used for display and not for storing configs. /// An example is Mac PS3 controllers; they return "(bluetooth)" or "(usb)" @@ -152,28 +140,45 @@ class InputDevice : public Object { /// a string. virtual auto GetDeviceIdentifier() -> std::string { return ""; } + /// Called for all devices when they've successfully been added + /// to the input-device list, have a valid ID, name, etc. + virtual auto ConnectionComplete() -> void {} + + auto UpdateLastInputTime() -> void; + + auto set_index(int index_in) -> void { index_ = index_in; } + auto set_numbered_identifier(int n) -> void { number_ = n; } + + protected: + auto ShipBufferIfFull() -> void; + + /// Pass some input command on to whatever we're connected to + /// (player or remote-player). + auto InputCommand(InputType type, float value = 0.0f) -> void; + + /// Subclasses should call this to request a player in the local game. + auto RequestPlayer() -> void; + auto remote_player_id() const -> int { return remote_player_id_; } - void UpdateLastInputTime(); private: - millisecs_t last_remote_input_commands_send_time_ = 0; + auto GetPyInputDevice(bool new_ref) -> PyObject*; + + millisecs_t last_remote_input_commands_send_time_{}; std::vector remote_input_commands_buffer_; // note: this is in base-net-time - millisecs_t last_input_time_ = 0; + millisecs_t last_input_time_{}; // We're attached to *one* of these two. Object::WeakRef player_; Object::WeakRef remote_player_; - int remote_player_id_ = -1; - PyObject* py_ref_ = nullptr; - auto GetPyInputDevice(bool new_ref) -> PyObject*; - void set_index(int index_in) { index_ = index_in; } - void set_numbered_identifier(int n) { number_ = n; } - int index_ = -1; // Our overall device index. - int number_ = -1; // Our type-specific number. - friend class Input; + int remote_player_id_{-1}; + PyObject* py_ref_{}; + int index_{-1}; // Our overall device index. + int number_{-1}; // Our type-specific number. + BA_DISALLOW_CLASS_COPIES(InputDevice); }; diff --git a/src/ballistica/input/input.cc b/src/ballistica/input/input.cc index 2ea36124..10b33bfa 100644 --- a/src/ballistica/input/input.cc +++ b/src/ballistica/input/input.cc @@ -411,7 +411,7 @@ auto Input::GetNewNumberedIdentifier(const std::string& name, // suffix that's not taken. for (auto&& i : input_devices_) { if (i.exists()) { - if ((i->GetRawDeviceName() == name) && i->number_ == num) { + if ((i->GetRawDeviceName() == name) && i->number() == num) { in_use = true; break; } @@ -717,18 +717,18 @@ auto Input::GetLocalActiveInputDeviceCount() -> int { // This can get called alot so lets cache the value. millisecs_t current_time = g_game->master_time(); - if (current_time != last_have_many_local_active_input_devices_check_time_) { - last_have_many_local_active_input_devices_check_time_ = current_time; + if (current_time != last_get_local_active_input_device_count_check_time_) { + last_get_local_active_input_device_count_check_time_ = current_time; int count = 0; for (auto& input_device : input_devices_) { - // Only count non-keyboard, non-touchscreen, local devices that have been + // Tally up local non-keyboard, non-touchscreen devices that have been // used in the last minute. - if (input_device.exists() && !(*input_device).IsKeyboard() - && !(*input_device).IsTouchScreen() && !(*input_device).IsUIOnly() - && (*input_device).IsLocal() - && ((*input_device).last_input_time() != 0 - && g_game->master_time() - (*input_device).last_input_time() + if (input_device.exists() && !input_device->IsKeyboard() + && !input_device->IsTouchScreen() && !input_device->IsUIOnly() + && input_device->IsLocal() + && (input_device->last_input_time() != 0 + && g_game->master_time() - input_device->last_input_time() < 60000)) { count++; } @@ -801,9 +801,9 @@ auto Input::ShouldCompletelyIgnoreInputDevice(InputDevice* input_device) return ignore_sdl_controllers_ && input_device->IsSDLController(); } -auto Input::GetIdleTime() const -> millisecs_t { - return GetRealTime() - last_input_time_; -} +// auto Input::GetIdleTime() const -> millisecs_t { +// return GetRealTime() - last_input_time_; +// } void Input::UpdateEnabledControllerSubsystems() { assert(IsBootstrapped()); @@ -872,6 +872,14 @@ void Input::Update() { if (real_time - last_input_device_count_update_time_ > incr) { UpdateInputDeviceCounts(); last_input_device_count_update_time_ = real_time; + + // Keep our idle-time up to date. + if (input_active_) { + input_idle_time_ = 0; + } else { + input_idle_time_ += incr; + } + input_active_ = false; } for (auto& input_device : input_devices_) { @@ -1080,7 +1088,7 @@ void Input::HandleBackPress(bool from_toolbar) { void Input::PushTextInputEvent(const std::string& text) { g_game->PushCall([this, text] { - ResetIdleTime(); + mark_input_active(); // Ignore if input is locked. if (IsInputLocked()) { @@ -1115,7 +1123,7 @@ void Input::HandleJoystickEvent(const SDL_Event& event, } // Make note that we're not idle. - ResetIdleTime(); + mark_input_active(); // And that this particular device isn't idle either. input_device->UpdateLastInputTime(); @@ -1139,7 +1147,7 @@ void Input::PushKeyReleaseEvent(const SDL_Keysym& keysym) { void Input::HandleKeyPress(const SDL_Keysym* keysym) { assert(InGameThread()); - ResetIdleTime(); + mark_input_active(); // Ignore all key presses if input is locked. if (IsInputLocked()) { @@ -1304,7 +1312,7 @@ void Input::HandleKeyRelease(const SDL_Keysym* keysym) { // Note: we want to let these through even if input is locked. - ResetIdleTime(); + mark_input_active(); // Give Python a crack at it for captures, etc. if (g_python->HandleKeyReleaseEvent(*keysym)) { @@ -1381,7 +1389,7 @@ auto Input::HandleMouseScroll(const Vector2f& amount) -> void { if (IsInputLocked()) { return; } - ResetIdleTime(); + mark_input_active(); Widget* root_widget = g_ui->root_widget(); if (std::abs(amount.y) > 0.0001f && root_widget) { @@ -1417,7 +1425,7 @@ auto Input::HandleSmoothMouseScroll(const Vector2f& velocity, bool momentum) if (IsInputLocked()) { return; } - ResetIdleTime(); + mark_input_active(); bool handled = false; Widget* root_widget = g_ui->root_widget(); @@ -1447,7 +1455,7 @@ auto Input::PushMouseMotionEvent(const Vector2f& position) -> void { auto Input::HandleMouseMotion(const Vector2f& position) -> void { assert(g_graphics); assert(InGameThread()); - ResetIdleTime(); + mark_input_active(); float old_cursor_pos_x = cursor_pos_x_; float old_cursor_pos_y = cursor_pos_y_; @@ -1508,7 +1516,7 @@ auto Input::HandleMouseDown(int button, const Vector2f& position) -> void { return; } - ResetIdleTime(); + mark_input_active(); last_mouse_move_time_ = GetRealTime(); mouse_move_count_++; @@ -1575,7 +1583,7 @@ auto Input::PushMouseUpEvent(int button, const Vector2f& position) -> void { auto Input::HandleMouseUp(int button, const Vector2f& position) -> void { assert(InGameThread()); - ResetIdleTime(); + mark_input_active(); // Convert normalized view coords to our virtual ones. cursor_pos_x_ = g_graphics->PixelToVirtualX( @@ -1629,7 +1637,7 @@ void Input::HandleTouchEvent(const TouchEvent& e) { return; } - ResetIdleTime(); + mark_input_active(); // float x = e.x; // float y = e.y; diff --git a/src/ballistica/input/input.h b/src/ballistica/input/input.h index b1724af1..b2750784 100644 --- a/src/ballistica/input/input.h +++ b/src/ballistica/input/input.h @@ -81,10 +81,11 @@ class Input { // Get the total idle time for the system. // FIXME - should better coordinate this with InputDevice::getLastUsedTime(). - auto GetIdleTime() const -> millisecs_t; + // auto GetIdleTime() const -> millisecs_t; // Should be called whenever user-input of some form comes through. - auto ResetIdleTime() -> void { last_input_time_ = GetRealTime(); } + // auto ResetIdleTime() -> void { last_input_time_ = GetRealTime(); } + auto mark_input_active() { input_active_ = true; } // Should be called regularly to update button repeats, etc. auto Update() -> void; @@ -128,6 +129,9 @@ class Input { auto PushDestroyKeyboardInputDevices() -> void; auto PushCreateKeyboardInputDevices() -> void; + /// Roughly how long in milliseconds have all input devices been idle. + auto input_idle_time() const { return input_idle_time_; } + private: auto UpdateInputDeviceCounts() -> void; auto GetNewNumberedIdentifier(const std::string& name, @@ -151,8 +155,11 @@ class Input { auto UpdateModKeyStates(const SDL_Keysym* keysym, bool press) -> void; auto CreateKeyboardInputDevices() -> void; auto DestroyKeyboardInputDevices() -> void; + + bool input_active_{}; + millisecs_t input_idle_time_{}; int local_active_input_device_count_{}; - millisecs_t last_have_many_local_active_input_devices_check_time_{}; + millisecs_t last_get_local_active_input_device_count_check_time_{}; std::unordered_map > reserved_identifiers_; int max_controller_count_so_far_{}; @@ -165,7 +172,7 @@ class Input { bool have_non_touch_inputs_{}; float cursor_pos_x_{}; float cursor_pos_y_{}; - millisecs_t last_input_time_{}; + // millisecs_t last_input_time_{}; millisecs_t last_click_time_{}; millisecs_t double_click_time_{200}; millisecs_t last_mouse_move_time_{}; diff --git a/src/ballistica/python/methods/python_methods_system.cc b/src/ballistica/python/methods/python_methods_system.cc index bf42a45c..9c0d0be4 100644 --- a/src/ballistica/python/methods/python_methods_system.cc +++ b/src/ballistica/python/methods/python_methods_system.cc @@ -177,7 +177,7 @@ auto PyGetIdleTime(PyObject* self, PyObject* args) -> PyObject* { BA_PYTHON_TRY; Platform::SetLastPyCall("get_idle_time"); return PyLong_FromLong(static_cast_check_fit( // NOLINT - g_input ? g_input->GetIdleTime() : 0)); + g_input ? g_input->input_idle_time() : 0)); BA_PYTHON_CATCH; } @@ -964,7 +964,7 @@ auto PythonMethodsSystem::GetMethods() -> std::vector { "\n" "(internal)\n" "\n" - "Returns the amount of time since any game input has been processed"}, + "Returns the amount of time since any game input has been received."}, {"set_have_mods", PySetHaveMods, METH_VARARGS, "set_have_mods(have_mods: bool) -> None\n" diff --git a/src/ballistica/python/python.cc b/src/ballistica/python/python.cc index 237b11fd..771be7bb 100644 --- a/src/ballistica/python/python.cc +++ b/src/ballistica/python/python.cc @@ -2442,6 +2442,26 @@ auto Python::GetRawConfigValue(const char* name, float default_value) -> float { } } +auto Python::GetRawConfigValue(const char* name, + std::optional default_value) + -> std::optional { + assert(InGameThread()); + assert(objexists(ObjID::kConfig)); + PyObject* value = PyDict_GetItemString(obj(ObjID::kConfig).get(), name); + if (value == nullptr) { + return default_value; + } + try { + if (value == Py_None) { + return std::optional(); + } + return GetPyFloat(value); + } catch (const std::exception&) { + Log("expected a float for config value '" + std::string(name) + "'"); + return default_value; + } +} + auto Python::GetRawConfigValue(const char* name, int default_value) -> int { assert(InGameThread()); assert(objexists(ObjID::kConfig)); diff --git a/src/ballistica/python/python.h b/src/ballistica/python/python.h index 0c2ae8bd..899939fc 100644 --- a/src/ballistica/python/python.h +++ b/src/ballistica/python/python.h @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -160,6 +161,8 @@ class Python { auto GetRawConfigValue(const char* name, const char* default_value) -> std::string; auto GetRawConfigValue(const char* name, float default_value) -> float; + auto GetRawConfigValue(const char* name, std::optional default_value) + -> std::optional; auto GetRawConfigValue(const char* name, int default_value) -> int; auto GetRawConfigValue(const char* name, bool default_value) -> bool; void SetRawConfigValue(const char* name, float value); diff --git a/src/ballistica/ui/ui.cc b/src/ballistica/ui/ui.cc index 3a9b0727..e2d32c04 100644 --- a/src/ballistica/ui/ui.cc +++ b/src/ballistica/ui/ui.cc @@ -230,7 +230,6 @@ void UI::AddWidget(Widget* w, ContainerWidget* parent) { auto UI::SendWidgetMessage(const WidgetMessage& m) -> int { if (!root_widget_.exists()) { - // Log("SendWidgetMessage() called before root widget created"); return false; } return root_widget_->HandleMessage(m); diff --git a/tools/bacommon/servermanager.py b/tools/bacommon/servermanager.py index c351ee91..57cbd7e9 100644 --- a/tools/bacommon/servermanager.py +++ b/tools/bacommon/servermanager.py @@ -91,23 +91,26 @@ class ServerConfig: # http://bombsquadgame.com/accountquery?id=ACCOUNT_ID_HERE stats_url: Optional[str] = None - # If present, the server will attempt to gracefully exit after this - # amount of time. A graceful exit can occur at the end of a series + # If present, the server manager will attempt to gracefully exit after + # this amount of time. A graceful exit can occur at the end of a series # or other opportune time. - # Servers with no exit times set will run indefinitely (though the server - # binary will be restarted periodically to clear any leaked memory). + # Servers with no exit times set will run indefinitely, though the + # server binary will be restarted periodically to clear any memory + # leaks or other bad state. clean_exit_minutes: Optional[float] = None - # If present, the server will shut down immediately after the given - # amount of time). This can be useful as a fallback for clean_exit_time. - # Servers with no exit times set will run indefinitely (though the server - # binary will be restarted periodically to clear any leaked memory). + # If present, the server manager will shut down immediately after this + # amount of time. This can be useful as a fallback for clean_exit_time. + # Servers with no exit times set will run indefinitely, though the + # server binary will be restarted periodically to clear any memory + # leaks or other bad state. unclean_exit_minutes: Optional[float] = None # If present, the server will shut down immediately if this amount of - # time passes with no connected clients. - # Servers with no exit times set will run indefinitely (though the server - # binary will be restarted periodically to clear any leaked memory). + # time passes with no activity from any players. + # Servers with no exit times set will run indefinitely, though the + # server binary will be restarted periodically to clear any memory + # leaks or other bad state. idle_exit_minutes: Optional[float] = None diff --git a/tools/batools/build.py b/tools/batools/build.py index 96867836..2a6c2694 100644 --- a/tools/batools/build.py +++ b/tools/batools/build.py @@ -642,11 +642,11 @@ def _get_server_config_template_yaml(projroot: str) -> str: if vname == 'playlist_code': # User wouldn't want to pass the default of None here. vval = 12345 - elif vname == 'clean_exit_time': + elif vname == 'clean_exit_minutes': vval = 60 - elif vname == 'unclean_exit_time': + elif vname == 'unclean_exit_minutes': vval = 90 - elif vname == 'idle_exit_time': + elif vname == 'idle_exit_minutes': vval = 20 elif vname == 'stats_url': vval = ('https://mystatssite.com/'