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