Finished wiring up clean_exit_minutes, unclean_exit_minutes, and idle_exit_minutes in server configs

This commit is contained in:
Eric Froemling 2020-11-16 11:23:05 -08:00
parent 5125b005c7
commit 5f909d0b91
20 changed files with 313 additions and 160 deletions

View File

@ -3932,24 +3932,24 @@
"assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450", "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/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", "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/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/5c/83/e1f9e8db08f24a1d0b08958b9e09", "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/59/ae/bc2f695f28eb3405415ea11c8083", "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/cb/ab/076371cc80fb408dfe2cbd4da8b0", "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/f0/70/203ffd8485f6e3b5432c70b556b3", "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/f5/51/9b37b71adfaa2d5ca706bc168b2f", "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/5d/87/089a0508c2876e6090f337a6a1c6", "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/fe/49/c33812c8596e946c4b7ff4a70176", "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/65/c0/cede9d63e3c3fd3148b1d25ba62f", "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/4b/e0/26d2281f316b0f704a6a404b030b", "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/a7/b8/988ab1f0d337516c034301ce219a", "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/69/80/9068d8f99a060c625abee7b49184", "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/4f/4c/8590730e5d1cdae456c1b734a2a1", "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/44/e5/d1c3162e114e51a5b5b826c2ec7c", "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/ee/21/8fab3da6b974cf323024d076b609", "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/20/5d/881676243c5f44bdca677497b4d4", "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/90/a3/e265c556587921cdfd8c753b592f", "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/4c/5e/80ac1a7a4a75de8869755aa7cbc1", "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/b0/4a/2144949c2cb6c3ef7d99c4409bfb", "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/f8/ce/078603417240c7f8a7f067d55a6f" "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/22/9f/c6686359ba318f695a0a0900f2c2"
} }

View File

@ -1,6 +1,7 @@
### 1.5.29 (20246) ### 1.5.29 (20246)
- Exposed ba method/class initing in public C++ layer. - Exposed ba method/class initing in public C++ layer.
- The 'restart' and 'shutdown' commands in the server script now default to immediate=True - 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) ### 1.5.28 (20239)
- Simplified ba.enum_by_value() - Simplified ba.enum_by_value()

View File

@ -2079,7 +2079,7 @@ def get_idle_time() -> int:
(internal) (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() return int()

View File

@ -124,7 +124,7 @@ class ServerManagerApp:
# Python will handle SIGINT for us (as KeyboardInterrupt) but we # Python will handle SIGINT for us (as KeyboardInterrupt) but we
# need to register a SIGTERM handler so we have a chance to clean # 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) # zombie processes)
signal.signal(signal.SIGTERM, self._handle_term_signal) signal.signal(signal.SIGTERM, self._handle_term_signal)
@ -150,12 +150,17 @@ class ServerManagerApp:
print(f'{Clr.SRED}Unexpected interpreter exception:' print(f'{Clr.SRED}Unexpected interpreter exception:'
f' {exc} ({type(exc)}){Clr.RST}') 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. # Mark ourselves as shutting down and wait for the process to wrap up.
self._done = True self._done = True
self._subprocess_thread.join() self._subprocess_thread.join()
def cmd(self, statement: str) -> None: 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 Note that commands are executed asynchronously and no status or
return value is accessible from this manager app. return value is accessible from this manager app.
@ -227,7 +232,7 @@ class ServerManagerApp:
KickCommand(client_id=client_id, ban_time=ban_time)) KickCommand(client_id=client_id, ban_time=ban_time))
def restart(self, immediate: bool = True) -> None: 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. This can be necessary for some config changes to take effect.
By default, the server will exit immediately. If 'immediate' is passed By default, the server will exit immediately. If 'immediate' is passed
@ -246,7 +251,7 @@ class ServerManagerApp:
IMMEDIATE_SHUTDOWN_TIME_LIMIT) IMMEDIATE_SHUTDOWN_TIME_LIMIT)
def shutdown(self, immediate: bool = True) -> None: 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 By default, the server will exit immediately. If 'immediate' is passed
as False, however, the server will instead exit at the next clean as False, however, the server will instead exit at the next clean
@ -256,7 +261,8 @@ class ServerManagerApp:
self._enqueue_server_command( self._enqueue_server_command(
ShutdownCommand(reason=ShutdownReason.NONE, immediate=immediate)) 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 self._wrapper_shutdown_desired = True
# If we're asking for an immediate shutdown but don't get one within # If we're asking for an immediate shutdown but don't get one within
@ -304,7 +310,7 @@ class ServerManagerApp:
raise SystemExit() raise SystemExit()
def _run_server_cycle(self) -> None: 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() self._prep_subprocess_environment()
@ -317,7 +323,7 @@ class ServerManagerApp:
# slight behavior tweaks. Hmm; should this be an argument instead? # slight behavior tweaks. Hmm; should this be an argument instead?
os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1' 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' binary_name = ('ballisticacore_headless.exe'
if os.name == 'nt' else './ballisticacore_headless') if os.name == 'nt' else './ballisticacore_headless')
self._subprocess = subprocess.Popen( self._subprocess = subprocess.Popen(
@ -332,14 +338,17 @@ class ServerManagerApp:
finally: finally:
self._kill_subprocess() 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: 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: if not self._done:
self._done = True self._done = True
# Our main thread is still be blocked in its prompt or # This should break the main thread out of its blocking
# whatnot; let it know it should die. # interpreter call.
os.kill(os.getpid(), signal.SIGTERM) os.kill(os.getpid(), signal.SIGTERM)
def _prep_subprocess_environment(self) -> None: def _prep_subprocess_environment(self) -> None:
@ -356,6 +365,7 @@ class ServerManagerApp:
bincfg['Port'] = self._config.port bincfg['Port'] = self._config.port
bincfg['Auto Balance Teams'] = self._config.auto_balance_teams bincfg['Auto Balance Teams'] = self._config.auto_balance_teams
bincfg['Show Tutorial'] = False bincfg['Show Tutorial'] = False
bincfg['Idle Exit Minutes'] = self._config.idle_exit_minutes
with open('dist/ba_root/config.json', 'w') as outfile: with open('dist/ba_root/config.json', 'w') as outfile:
outfile.write(json.dumps(bincfg)) outfile.write(json.dumps(bincfg))
@ -384,11 +394,9 @@ class ServerManagerApp:
self._subprocess.stdin.flush() self._subprocess.stdin.flush()
def _run_subprocess_until_exit(self) -> None: def _run_subprocess_until_exit(self) -> None:
# pylint: disable=too-many-branches
assert current_thread() is self._subprocess_thread assert current_thread() is self._subprocess_thread
assert self._subprocess is not None assert self._subprocess is not None
assert self._subprocess.stdin 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. # Send the initial server config which should kick things off.
# (but make sure its values are still valid first) # (but make sure its values are still valid first)
@ -414,35 +422,7 @@ class ServerManagerApp:
self._subprocess_commands = [] self._subprocess_commands = []
# Request restarts/shut-downs for various reasons. # Request restarts/shut-downs for various reasons.
sincelaunch = time.time() - self._subprocess_launch_time self._request_shutdowns_or_restarts()
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
# If they want to force-kill our subprocess, simply exit this # If they want to force-kill our subprocess, simply exit this
# loop; the cleanup code will kill the process. # loop; the cleanup code will kill the process.
@ -457,17 +437,60 @@ class ServerManagerApp:
if code == 0: if code == 0:
clr = Clr.CYN clr = Clr.CYN
slp = 0.0 slp = 0.0
desc = ''
elif code == 154:
clr = Clr.CYN
slp = 0.0
desc = ' (idle_exit_minutes reached)'
self._wrapper_shutdown_desired = True
else: else:
clr = Clr.SRED clr = Clr.SRED
slp = 5.0 # Avoid super fast death loops. slp = 5.0 # Avoid super fast death loops.
print(f'{clr}Server child-process exited' desc = ''
f' with code {code}.{Clr.RST}') print(f'{clr}Server subprocess exited'
f' with code {code}{desc}.{Clr.RST}')
self._reset_subprocess_vars() self._reset_subprocess_vars()
time.sleep(slp) time.sleep(slp)
break break
time.sleep(0.25) 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: def _reset_subprocess_vars(self) -> None:
self._subprocess = None self._subprocess = None
self._subprocess_launch_time = None self._subprocess_launch_time = None
@ -477,12 +500,12 @@ class ServerManagerApp:
self._subprocess_force_kill_time = None self._subprocess_force_kill_time = None
def _kill_subprocess(self) -> 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 assert current_thread() is self._subprocess_thread
if self._subprocess is None: if self._subprocess is None:
return 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. # First, ask it nicely to die and give it a moment.
# If that doesn't work, bring down the hammer. # If that doesn't work, bring down the hammer.
@ -492,7 +515,7 @@ class ServerManagerApp:
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
self._subprocess.kill() self._subprocess.kill()
self._reset_subprocess_vars() self._reset_subprocess_vars()
print(f'{Clr.CYN}Server process stopped.{Clr.RST}') print(f'{Clr.CYN}Subprocess stopped.{Clr.RST}')
def main() -> None: def main() -> None:

View File

@ -1,5 +1,5 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND --> <!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<h4><em>last updated on 2020-11-12 for Ballistica version 1.5.29 build 20248</em></h4> <h4><em>last updated on 2020-11-15 for Ballistica version 1.5.29 build 20254</em></h4>
<p>This page documents the Python classes and functions in the 'ba' module, <p>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 <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p> 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 <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
<hr> <hr>

View File

@ -16,6 +16,10 @@ auto AppConfig::Entry::FloatValue() const -> float {
throw Exception("not a float entry"); throw Exception("not a float entry");
} }
auto AppConfig::Entry::OptionalFloatValue() const -> std::optional<float> {
throw Exception("not an optional float entry");
}
auto AppConfig::Entry::StringValue() const -> std::string { auto AppConfig::Entry::StringValue() const -> std::string {
throw Exception("not a string entry"); throw Exception("not a string entry");
} }
@ -32,6 +36,11 @@ auto AppConfig::Entry::DefaultFloatValue() const -> float {
throw Exception("not a float entry"); throw Exception("not a float entry");
} }
auto AppConfig::Entry::DefaultOptionalFloatValue() const
-> std::optional<float> {
throw Exception("not an optional float entry");
}
auto AppConfig::Entry::DefaultStringValue() const -> std::string { auto AppConfig::Entry::DefaultStringValue() const -> std::string {
throw Exception("not a string entry"); throw Exception("not a string entry");
} }
@ -78,6 +87,26 @@ class AppConfig::FloatEntry : public AppConfig::Entry {
float default_value_{}; float default_value_{};
}; };
class AppConfig::OptionalFloatEntry : public AppConfig::Entry {
public:
OptionalFloatEntry() = default;
OptionalFloatEntry(const char* name, std::optional<float> default_value)
: Entry(name), default_value_(default_value) {}
auto GetType() const -> Type override { return Type::kOptionalFloat; }
auto Resolve() const -> std::optional<float> {
return g_python->GetRawConfigValue(name().c_str(), default_value_);
}
auto OptionalFloatValue() const -> std::optional<float> override {
return Resolve();
}
auto DefaultOptionalFloatValue() const -> std::optional<float> override {
return default_value_;
}
private:
std::optional<float> default_value_{};
};
class AppConfig::IntEntry : public AppConfig::Entry { class AppConfig::IntEntry : public AppConfig::Entry {
public: public:
IntEntry() = default; IntEntry() = default;
@ -157,6 +186,9 @@ void AppConfig::SetupEntries() {
float_entries_[FloatID::kGoogleVRRenderTargetScale] = float_entries_[FloatID::kGoogleVRRenderTargetScale] =
FloatEntry("GVR Render Target Scale", gvrrts_default); FloatEntry("GVR Render Target Scale", gvrrts_default);
optional_float_entries_[OptionalFloatID::kIdleExitMinutes] =
OptionalFloatEntry("Idle Exit Minutes", std::optional<float>());
string_entries_[StringID::kResolutionAndroid] = string_entries_[StringID::kResolutionAndroid] =
StringEntry("Resolution (Android)", "Auto"); StringEntry("Resolution (Android)", "Auto");
string_entries_[StringID::kTouchActionControlType] = string_entries_[StringID::kTouchActionControlType] =
@ -220,6 +252,14 @@ auto AppConfig::Resolve(FloatID id) -> float {
return i->second.Resolve(); return i->second.Resolve();
} }
auto AppConfig::Resolve(OptionalFloatID id) -> std::optional<float> {
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 AppConfig::Resolve(StringID id) -> std::string {
auto i = string_entries_.find(id); auto i = string_entries_.find(id);
if (i == string_entries_.end()) { if (i == string_entries_.end()) {

View File

@ -5,6 +5,7 @@
#include <map> #include <map>
#include <memory> #include <memory>
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
@ -30,6 +31,11 @@ class AppConfig {
kLast // Sentinel. kLast // Sentinel.
}; };
enum class OptionalFloatID {
kIdleExitMinutes,
kLast // Sentinel.
};
enum class StringID { enum class StringID {
kResolutionAndroid, kResolutionAndroid,
kTouchActionControlType, kTouchActionControlType,
@ -68,16 +74,18 @@ class AppConfig {
class Entry { class Entry {
public: public:
enum class Type { kString, kInt, kFloat, kBool }; enum class Type { kString, kInt, kFloat, kOptionalFloat, kBool };
Entry() = default; Entry() = default;
explicit Entry(const char* name) : name_(name) {} explicit Entry(const char* name) : name_(name) {}
virtual auto GetType() const -> Type = 0; virtual auto GetType() const -> Type = 0;
auto name() const -> const std::string& { return name_; } auto name() const -> const std::string& { return name_; }
virtual auto FloatValue() const -> float; virtual auto FloatValue() const -> float;
virtual auto OptionalFloatValue() const -> std::optional<float>;
virtual auto StringValue() const -> std::string; virtual auto StringValue() const -> std::string;
virtual auto IntValue() const -> int; virtual auto IntValue() const -> int;
virtual auto BoolValue() const -> bool; virtual auto BoolValue() const -> bool;
virtual auto DefaultFloatValue() const -> float; virtual auto DefaultFloatValue() const -> float;
virtual auto DefaultOptionalFloatValue() const -> std::optional<float>;
virtual auto DefaultStringValue() const -> std::string; virtual auto DefaultStringValue() const -> std::string;
virtual auto DefaultIntValue() const -> int; virtual auto DefaultIntValue() const -> int;
virtual auto DefaultBoolValue() const -> bool; virtual auto DefaultBoolValue() const -> bool;
@ -91,6 +99,7 @@ class AppConfig {
// Given specific ids, returns resolved values (fastest access). // Given specific ids, returns resolved values (fastest access).
auto Resolve(FloatID id) -> float; auto Resolve(FloatID id) -> float;
auto Resolve(OptionalFloatID id) -> std::optional<float>;
auto Resolve(StringID id) -> std::string; auto Resolve(StringID id) -> std::string;
auto Resolve(IntID id) -> int; auto Resolve(IntID id) -> int;
auto Resolve(BoolID id) -> bool; auto Resolve(BoolID id) -> bool;
@ -113,6 +122,7 @@ class AppConfig {
private: private:
class StringEntry; class StringEntry;
class FloatEntry; class FloatEntry;
class OptionalFloatEntry;
class IntEntry; class IntEntry;
class BoolEntry; class BoolEntry;
template <typename T> template <typename T>
@ -120,6 +130,7 @@ class AppConfig {
void SetupEntries(); void SetupEntries();
std::map<std::string, const Entry*> entries_by_name_; std::map<std::string, const Entry*> entries_by_name_;
std::map<FloatID, FloatEntry> float_entries_; std::map<FloatID, FloatEntry> float_entries_;
std::map<OptionalFloatID, OptionalFloatEntry> optional_float_entries_;
std::map<IntID, IntEntry> int_entries_; std::map<IntID, IntEntry> int_entries_;
std::map<StringID, StringEntry> string_entries_; std::map<StringID, StringEntry> string_entries_;
std::map<BoolID, BoolEntry> bool_entries_; std::map<BoolID, BoolEntry> bool_entries_;

View File

@ -21,7 +21,7 @@
namespace ballistica { namespace ballistica {
// These are set automatically via script; don't change here. // These are set automatically via script; don't change here.
const int kAppBuildNumber = 20249; const int kAppBuildNumber = 20256;
const char* kAppVersion = "1.5.29"; const char* kAppVersion = "1.5.29";
// Our standalone globals. // Our standalone globals.

View File

@ -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. // Bring our scenes, real-time timers, etc up to date.
void Game::Update() { void Game::Update() {
assert(InGameThread()); assert(InGameThread());
@ -521,6 +542,8 @@ void Game::Update() {
g_input->Update(); g_input->Update();
UpdateKickVote(); UpdateKickVote();
HandleQuitOnIdle();
// Send the game roster to our clients if it's changed recently. // Send the game roster to our clients if it's changed recently.
if (game_roster_dirty_) { if (game_roster_dirty_) {
if (real_time > last_game_roster_send_time_ + 2500) { 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, // 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 // 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 // towards clean step patterns in all cases, not just the 60hz and 90hz
// we handle now. In general we want stuff like 1,1,2,1,1,2,1,1,2, not // cases we handle now. In general we want stuff like 1,1,2,1,1,2,1,1,2,
// 1,1,1,2,1,2,2,1,1. // not 1,1,1,2,1,2,2,1,1.
// Figure out where our net-time *should* be getting to to match real-time. // Figure out where our net-time *should* be getting to to match real-time.
millisecs_t target_master_time = real_time + master_time_offset_; millisecs_t target_master_time = real_time + master_time_offset_;
@ -1212,7 +1235,7 @@ void Game::Draw() {
g_graphics->BuildAndPushFrameDef(); g_graphics->BuildAndPushFrameDef();
// Now bring the game up to date. // 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. // 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 // Could maybe try to be smart about which to do first, but not sure
// if its worth it. // if its worth it.
@ -1403,6 +1426,9 @@ void Game::ApplyConfig() {
g_app_config->Resolve(AppConfig::BoolID::kDisableCameraGyro); g_app_config->Resolve(AppConfig::BoolID::kDisableCameraGyro);
g_graphics->set_camera_gyro_explicitly_disabled(disable_camera_gyro); g_graphics->set_camera_gyro_explicitly_disabled(disable_camera_gyro);
idle_exit_minutes_ =
g_app_config->Resolve(AppConfig::OptionalFloatID::kIdleExitMinutes);
// Any platform-specific settings. // Any platform-specific settings.
g_platform->ApplyConfig(); g_platform->ApplyConfig();
} }

View File

@ -6,6 +6,7 @@
#include <list> #include <list>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional>
#include <set> #include <set>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
@ -251,6 +252,7 @@ class Game : public Module {
auto mark_game_roster_dirty() -> void { game_roster_dirty_ = true; } auto mark_game_roster_dirty() -> void { game_roster_dirty_ = true; }
private: private:
auto HandleQuitOnIdle() -> void;
auto InitSpecialChars() -> void; auto InitSpecialChars() -> void;
auto Draw() -> void; auto Draw() -> void;
auto PartyInvite(const std::string& name, const std::string& invite_id) 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<ScoreToBeat>& scores, auto ScoresToBeatResponse(bool success, const std::list<ScoreToBeat>& scores,
void* py_callback) -> void; 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 Prune() -> void; // Periodic pruning of dead stuff.
auto Update() -> void; auto Update() -> void;
auto Process() -> void; auto Process() -> void;
@ -284,6 +279,13 @@ class Game : public Module {
auto GetGameRosterMessage() -> std::vector<uint8_t>; auto GetGameRosterMessage() -> std::vector<uint8_t>;
auto Shutdown(bool soft) -> void; 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<ConnectionSet> connections_; std::unique_ptr<ConnectionSet> connections_;
std::list<std::pair<millisecs_t, PlayerSpec> > banned_players_; std::list<std::pair<millisecs_t, PlayerSpec> > banned_players_;
std::list<std::string> chat_messages_; std::list<std::string> chat_messages_;
@ -314,6 +316,8 @@ class Game : public Module {
std::unordered_map<SpecialChar, std::string> special_char_strings_; std::unordered_map<SpecialChar, std::string> special_char_strings_;
bool ran_app_launch_commands_{}; bool ran_app_launch_commands_{};
bool kick_idle_players_{}; bool kick_idle_players_{};
std::optional<float> idle_exit_minutes_{};
bool idle_exiting_{};
std::unique_ptr<TimerList> realtimers_; std::unique_ptr<TimerList> realtimers_;
Timer* process_timer_{}; Timer* process_timer_{};
Timer* headless_update_timer_{}; Timer* headless_update_timer_{};

View File

@ -268,7 +268,10 @@ void InputDevice::Update() {
} }
void InputDevice::UpdateLastInputTime() { 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(); last_input_time_ = g_game->master_time();
g_input->mark_input_active();
} }
void InputDevice::InputCommand(InputType type, float value) { void InputDevice::InputCommand(InputType type, float value) {

View File

@ -22,18 +22,18 @@ class InputDevice : public Object {
~InputDevice() override; ~InputDevice() override;
/// Called when the device is attached/detached to a local player. /// Called when the device is attached/detached to a local player.
virtual void AttachToLocalPlayer(Player* player); virtual auto AttachToLocalPlayer(Player* player) -> void;
virtual void AttachToRemotePlayer(ConnectionToHost* connection_to_host, virtual auto AttachToRemotePlayer(ConnectionToHost* connection_to_host,
int remote_player_id); int remote_player_id) -> void;
virtual void DetachFromPlayer(); virtual auto DetachFromPlayer() -> void;
/// Issues a command to the remote game to remove the player we're attached /// Issues a command to the remote game to remove the player we're attached
/// to. /// to.
void RemoveRemotePlayerFromGame(); auto RemoveRemotePlayerFromGame() -> void;
/// Return the (not necessarily unique) name of the input device. /// Return the (not necessarily unique) name of the input device.
auto GetDeviceName() -> std::string; auto GetDeviceName() -> std::string;
virtual void ResetHeldStates(); virtual auto ResetHeldStates() -> void;
/// Return the default base player name for players using this input device. /// Return the default base player name for players using this input device.
virtual auto GetDefaultPlayerName() -> std::string; virtual auto GetDefaultPlayerName() -> std::string;
@ -69,10 +69,10 @@ class InputDevice : public Object {
auto index() const -> int { return index_; } auto index() const -> int { return index_; }
/// Read new control values from config. /// Read new control values from config.
virtual void UpdateMapping() {} virtual auto UpdateMapping() -> void {}
/// Called during the game loop - for manual button repeats, etc. /// Called during the game loop - for manual button repeats, etc.
virtual void Update(); virtual auto Update() -> void;
/// Return client id or -1 if local. /// Return client id or -1 if local.
virtual auto GetClientID() const -> int; virtual auto GetClientID() const -> int;
@ -81,7 +81,7 @@ class InputDevice : public Object {
virtual auto IsRemoteClient() const -> bool; virtual auto IsRemoteClient() const -> bool;
#if BA_SDL_BUILD || BA_MINSDL_BUILD #if BA_SDL_BUILD || BA_MINSDL_BUILD
virtual void HandleSDLEvent(const SDL_Event* e) {} virtual auto HandleSDLEvent(const SDL_Event* e) -> void {}
#endif #endif
virtual auto GetAllowsConfiguring() -> bool { return true; } virtual auto GetAllowsConfiguring() -> bool { return true; }
@ -121,26 +121,14 @@ class InputDevice : public Object {
auto has_py_ref() -> bool { return (py_ref_ != nullptr); } auto has_py_ref() -> bool { return (py_ref_ != nullptr); }
auto last_input_time() const -> millisecs_t { return last_input_time_; } auto last_input_time() const -> millisecs_t { return last_input_time_; }
virtual auto ShouldBeHiddenFromUser() -> bool; virtual auto ShouldBeHiddenFromUser() -> bool;
static void ResetRandomNames(); static auto ResetRandomNames() -> void;
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();
/// Return a human-readable name for the device's type. /// Return a human-readable name for the device's type.
/// This is used for display and also for storing configs/etc. /// This is used for display and also for storing configs/etc.
virtual auto GetRawDeviceName() -> std::string { return "Input Device"; } virtual auto GetRawDeviceName() -> std::string { return "Input Device"; }
auto number() const { return number_; }
/// Return any extra description for the device. /// Return any extra description for the device.
/// This portion is only used for display and not for storing configs. /// This portion is only used for display and not for storing configs.
/// An example is Mac PS3 controllers; they return "(bluetooth)" or "(usb)" /// An example is Mac PS3 controllers; they return "(bluetooth)" or "(usb)"
@ -152,28 +140,45 @@ class InputDevice : public Object {
/// a string. /// a string.
virtual auto GetDeviceIdentifier() -> std::string { return ""; } 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_; } auto remote_player_id() const -> int { return remote_player_id_; }
void UpdateLastInputTime();
private: 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<uint8_t> remote_input_commands_buffer_; std::vector<uint8_t> remote_input_commands_buffer_;
// note: this is in base-net-time // 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. // We're attached to *one* of these two.
Object::WeakRef<Player> player_; Object::WeakRef<Player> player_;
Object::WeakRef<ConnectionToHost> remote_player_; Object::WeakRef<ConnectionToHost> remote_player_;
int remote_player_id_ = -1; int remote_player_id_{-1};
PyObject* py_ref_ = nullptr; PyObject* py_ref_{};
auto GetPyInputDevice(bool new_ref) -> PyObject*; int index_{-1}; // Our overall device index.
void set_index(int index_in) { index_ = index_in; } int number_{-1}; // Our type-specific number.
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;
BA_DISALLOW_CLASS_COPIES(InputDevice); BA_DISALLOW_CLASS_COPIES(InputDevice);
}; };

View File

@ -411,7 +411,7 @@ auto Input::GetNewNumberedIdentifier(const std::string& name,
// suffix that's not taken. // suffix that's not taken.
for (auto&& i : input_devices_) { for (auto&& i : input_devices_) {
if (i.exists()) { if (i.exists()) {
if ((i->GetRawDeviceName() == name) && i->number_ == num) { if ((i->GetRawDeviceName() == name) && i->number() == num) {
in_use = true; in_use = true;
break; break;
} }
@ -717,18 +717,18 @@ auto Input::GetLocalActiveInputDeviceCount() -> int {
// This can get called alot so lets cache the value. // This can get called alot so lets cache the value.
millisecs_t current_time = g_game->master_time(); millisecs_t current_time = g_game->master_time();
if (current_time != last_have_many_local_active_input_devices_check_time_) { if (current_time != last_get_local_active_input_device_count_check_time_) {
last_have_many_local_active_input_devices_check_time_ = current_time; last_get_local_active_input_device_count_check_time_ = current_time;
int count = 0; int count = 0;
for (auto& input_device : input_devices_) { 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. // used in the last minute.
if (input_device.exists() && !(*input_device).IsKeyboard() if (input_device.exists() && !input_device->IsKeyboard()
&& !(*input_device).IsTouchScreen() && !(*input_device).IsUIOnly() && !input_device->IsTouchScreen() && !input_device->IsUIOnly()
&& (*input_device).IsLocal() && input_device->IsLocal()
&& ((*input_device).last_input_time() != 0 && (input_device->last_input_time() != 0
&& g_game->master_time() - (*input_device).last_input_time() && g_game->master_time() - input_device->last_input_time()
< 60000)) { < 60000)) {
count++; count++;
} }
@ -801,9 +801,9 @@ auto Input::ShouldCompletelyIgnoreInputDevice(InputDevice* input_device)
return ignore_sdl_controllers_ && input_device->IsSDLController(); return ignore_sdl_controllers_ && input_device->IsSDLController();
} }
auto Input::GetIdleTime() const -> millisecs_t { // auto Input::GetIdleTime() const -> millisecs_t {
return GetRealTime() - last_input_time_; // return GetRealTime() - last_input_time_;
} // }
void Input::UpdateEnabledControllerSubsystems() { void Input::UpdateEnabledControllerSubsystems() {
assert(IsBootstrapped()); assert(IsBootstrapped());
@ -872,6 +872,14 @@ void Input::Update() {
if (real_time - last_input_device_count_update_time_ > incr) { if (real_time - last_input_device_count_update_time_ > incr) {
UpdateInputDeviceCounts(); UpdateInputDeviceCounts();
last_input_device_count_update_time_ = real_time; 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_) { for (auto& input_device : input_devices_) {
@ -1080,7 +1088,7 @@ void Input::HandleBackPress(bool from_toolbar) {
void Input::PushTextInputEvent(const std::string& text) { void Input::PushTextInputEvent(const std::string& text) {
g_game->PushCall([this, text] { g_game->PushCall([this, text] {
ResetIdleTime(); mark_input_active();
// Ignore if input is locked. // Ignore if input is locked.
if (IsInputLocked()) { if (IsInputLocked()) {
@ -1115,7 +1123,7 @@ void Input::HandleJoystickEvent(const SDL_Event& event,
} }
// Make note that we're not idle. // Make note that we're not idle.
ResetIdleTime(); mark_input_active();
// And that this particular device isn't idle either. // And that this particular device isn't idle either.
input_device->UpdateLastInputTime(); input_device->UpdateLastInputTime();
@ -1139,7 +1147,7 @@ void Input::PushKeyReleaseEvent(const SDL_Keysym& keysym) {
void Input::HandleKeyPress(const SDL_Keysym* keysym) { void Input::HandleKeyPress(const SDL_Keysym* keysym) {
assert(InGameThread()); assert(InGameThread());
ResetIdleTime(); mark_input_active();
// Ignore all key presses if input is locked. // Ignore all key presses if input is locked.
if (IsInputLocked()) { 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. // 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. // Give Python a crack at it for captures, etc.
if (g_python->HandleKeyReleaseEvent(*keysym)) { if (g_python->HandleKeyReleaseEvent(*keysym)) {
@ -1381,7 +1389,7 @@ auto Input::HandleMouseScroll(const Vector2f& amount) -> void {
if (IsInputLocked()) { if (IsInputLocked()) {
return; return;
} }
ResetIdleTime(); mark_input_active();
Widget* root_widget = g_ui->root_widget(); Widget* root_widget = g_ui->root_widget();
if (std::abs(amount.y) > 0.0001f && 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()) { if (IsInputLocked()) {
return; return;
} }
ResetIdleTime(); mark_input_active();
bool handled = false; bool handled = false;
Widget* root_widget = g_ui->root_widget(); 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 { auto Input::HandleMouseMotion(const Vector2f& position) -> void {
assert(g_graphics); assert(g_graphics);
assert(InGameThread()); assert(InGameThread());
ResetIdleTime(); mark_input_active();
float old_cursor_pos_x = cursor_pos_x_; float old_cursor_pos_x = cursor_pos_x_;
float old_cursor_pos_y = cursor_pos_y_; float old_cursor_pos_y = cursor_pos_y_;
@ -1508,7 +1516,7 @@ auto Input::HandleMouseDown(int button, const Vector2f& position) -> void {
return; return;
} }
ResetIdleTime(); mark_input_active();
last_mouse_move_time_ = GetRealTime(); last_mouse_move_time_ = GetRealTime();
mouse_move_count_++; 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 { auto Input::HandleMouseUp(int button, const Vector2f& position) -> void {
assert(InGameThread()); assert(InGameThread());
ResetIdleTime(); mark_input_active();
// Convert normalized view coords to our virtual ones. // Convert normalized view coords to our virtual ones.
cursor_pos_x_ = g_graphics->PixelToVirtualX( cursor_pos_x_ = g_graphics->PixelToVirtualX(
@ -1629,7 +1637,7 @@ void Input::HandleTouchEvent(const TouchEvent& e) {
return; return;
} }
ResetIdleTime(); mark_input_active();
// float x = e.x; // float x = e.x;
// float y = e.y; // float y = e.y;

View File

@ -81,10 +81,11 @@ class Input {
// Get the total idle time for the system. // Get the total idle time for the system.
// FIXME - should better coordinate this with InputDevice::getLastUsedTime(). // 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. // 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. // Should be called regularly to update button repeats, etc.
auto Update() -> void; auto Update() -> void;
@ -128,6 +129,9 @@ class Input {
auto PushDestroyKeyboardInputDevices() -> void; auto PushDestroyKeyboardInputDevices() -> void;
auto PushCreateKeyboardInputDevices() -> 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: private:
auto UpdateInputDeviceCounts() -> void; auto UpdateInputDeviceCounts() -> void;
auto GetNewNumberedIdentifier(const std::string& name, auto GetNewNumberedIdentifier(const std::string& name,
@ -151,8 +155,11 @@ class Input {
auto UpdateModKeyStates(const SDL_Keysym* keysym, bool press) -> void; auto UpdateModKeyStates(const SDL_Keysym* keysym, bool press) -> void;
auto CreateKeyboardInputDevices() -> void; auto CreateKeyboardInputDevices() -> void;
auto DestroyKeyboardInputDevices() -> void; auto DestroyKeyboardInputDevices() -> void;
bool input_active_{};
millisecs_t input_idle_time_{};
int local_active_input_device_count_{}; 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<std::string, std::unordered_map<std::string, int> > std::unordered_map<std::string, std::unordered_map<std::string, int> >
reserved_identifiers_; reserved_identifiers_;
int max_controller_count_so_far_{}; int max_controller_count_so_far_{};
@ -165,7 +172,7 @@ class Input {
bool have_non_touch_inputs_{}; bool have_non_touch_inputs_{};
float cursor_pos_x_{}; float cursor_pos_x_{};
float cursor_pos_y_{}; float cursor_pos_y_{};
millisecs_t last_input_time_{}; // millisecs_t last_input_time_{};
millisecs_t last_click_time_{}; millisecs_t last_click_time_{};
millisecs_t double_click_time_{200}; millisecs_t double_click_time_{200};
millisecs_t last_mouse_move_time_{}; millisecs_t last_mouse_move_time_{};

View File

@ -177,7 +177,7 @@ auto PyGetIdleTime(PyObject* self, PyObject* args) -> PyObject* {
BA_PYTHON_TRY; BA_PYTHON_TRY;
Platform::SetLastPyCall("get_idle_time"); Platform::SetLastPyCall("get_idle_time");
return PyLong_FromLong(static_cast_check_fit<long>( // NOLINT return PyLong_FromLong(static_cast_check_fit<long>( // NOLINT
g_input ? g_input->GetIdleTime() : 0)); g_input ? g_input->input_idle_time() : 0));
BA_PYTHON_CATCH; BA_PYTHON_CATCH;
} }
@ -964,7 +964,7 @@ auto PythonMethodsSystem::GetMethods() -> std::vector<PyMethodDef> {
"\n" "\n"
"(internal)\n" "(internal)\n"
"\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", PySetHaveMods, METH_VARARGS,
"set_have_mods(have_mods: bool) -> None\n" "set_have_mods(have_mods: bool) -> None\n"

View File

@ -2442,6 +2442,26 @@ auto Python::GetRawConfigValue(const char* name, float default_value) -> float {
} }
} }
auto Python::GetRawConfigValue(const char* name,
std::optional<float> default_value)
-> std::optional<float> {
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<float>();
}
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 { auto Python::GetRawConfigValue(const char* name, int default_value) -> int {
assert(InGameThread()); assert(InGameThread());
assert(objexists(ObjID::kConfig)); assert(objexists(ObjID::kConfig));

View File

@ -5,6 +5,7 @@
#include <list> #include <list>
#include <map> #include <map>
#include <optional>
#include <set> #include <set>
#include <string> #include <string>
#include <vector> #include <vector>
@ -160,6 +161,8 @@ class Python {
auto GetRawConfigValue(const char* name, const char* default_value) auto GetRawConfigValue(const char* name, const char* default_value)
-> std::string; -> std::string;
auto GetRawConfigValue(const char* name, float default_value) -> float; auto GetRawConfigValue(const char* name, float default_value) -> float;
auto GetRawConfigValue(const char* name, std::optional<float> default_value)
-> std::optional<float>;
auto GetRawConfigValue(const char* name, int default_value) -> int; auto GetRawConfigValue(const char* name, int default_value) -> int;
auto GetRawConfigValue(const char* name, bool default_value) -> bool; auto GetRawConfigValue(const char* name, bool default_value) -> bool;
void SetRawConfigValue(const char* name, float value); void SetRawConfigValue(const char* name, float value);

View File

@ -230,7 +230,6 @@ void UI::AddWidget(Widget* w, ContainerWidget* parent) {
auto UI::SendWidgetMessage(const WidgetMessage& m) -> int { auto UI::SendWidgetMessage(const WidgetMessage& m) -> int {
if (!root_widget_.exists()) { if (!root_widget_.exists()) {
// Log("SendWidgetMessage() called before root widget created");
return false; return false;
} }
return root_widget_->HandleMessage(m); return root_widget_->HandleMessage(m);

View File

@ -91,23 +91,26 @@ class ServerConfig:
# http://bombsquadgame.com/accountquery?id=ACCOUNT_ID_HERE # http://bombsquadgame.com/accountquery?id=ACCOUNT_ID_HERE
stats_url: Optional[str] = None stats_url: Optional[str] = None
# If present, the server will attempt to gracefully exit after this # If present, the server manager will attempt to gracefully exit after
# amount of time. A graceful exit can occur at the end of a series # this amount of time. A graceful exit can occur at the end of a series
# or other opportune time. # or other opportune time.
# Servers with no exit times set will run indefinitely (though the server # Servers with no exit times set will run indefinitely, though the
# binary will be restarted periodically to clear any leaked memory). # server binary will be restarted periodically to clear any memory
# leaks or other bad state.
clean_exit_minutes: Optional[float] = None clean_exit_minutes: Optional[float] = None
# If present, the server will shut down immediately after the given # 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. # 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 # Servers with no exit times set will run indefinitely, though the
# binary will be restarted periodically to clear any leaked memory). # server binary will be restarted periodically to clear any memory
# leaks or other bad state.
unclean_exit_minutes: Optional[float] = None unclean_exit_minutes: Optional[float] = None
# If present, the server will shut down immediately if this amount of # If present, the server will shut down immediately if this amount of
# time passes with no connected clients. # time passes with no activity from any players.
# Servers with no exit times set will run indefinitely (though the server # Servers with no exit times set will run indefinitely, though the
# binary will be restarted periodically to clear any leaked memory). # server binary will be restarted periodically to clear any memory
# leaks or other bad state.
idle_exit_minutes: Optional[float] = None idle_exit_minutes: Optional[float] = None

View File

@ -642,11 +642,11 @@ def _get_server_config_template_yaml(projroot: str) -> str:
if vname == 'playlist_code': if vname == 'playlist_code':
# User wouldn't want to pass the default of None here. # User wouldn't want to pass the default of None here.
vval = 12345 vval = 12345
elif vname == 'clean_exit_time': elif vname == 'clean_exit_minutes':
vval = 60 vval = 60
elif vname == 'unclean_exit_time': elif vname == 'unclean_exit_minutes':
vval = 90 vval = 90
elif vname == 'idle_exit_time': elif vname == 'idle_exit_minutes':
vval = 20 vval = 20
elif vname == 'stats_url': elif vname == 'stats_url':
vval = ('https://mystatssite.com/' vval = ('https://mystatssite.com/'