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/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"
}

View File

@ -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()

View File

@ -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()

View File

@ -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:

View File

@ -1,5 +1,5 @@
<!-- 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,
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>

View File

@ -16,6 +16,10 @@ auto AppConfig::Entry::FloatValue() const -> float {
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 {
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<float> {
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<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 {
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<float>());
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<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 i = string_entries_.find(id);
if (i == string_entries_.end()) {

View File

@ -5,6 +5,7 @@
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <vector>
@ -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<float>;
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<float>;
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<float>;
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 <typename T>
@ -120,6 +130,7 @@ class AppConfig {
void SetupEntries();
std::map<std::string, const Entry*> entries_by_name_;
std::map<FloatID, FloatEntry> float_entries_;
std::map<OptionalFloatID, OptionalFloatEntry> optional_float_entries_;
std::map<IntID, IntEntry> int_entries_;
std::map<StringID, StringEntry> string_entries_;
std::map<BoolID, BoolEntry> bool_entries_;

View File

@ -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.

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.
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();
}

View File

@ -6,6 +6,7 @@
#include <list>
#include <memory>
#include <mutex>
#include <optional>
#include <set>
#include <string>
#include <unordered_map>
@ -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<ScoreToBeat>& 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<uint8_t>;
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::list<std::pair<millisecs_t, PlayerSpec> > banned_players_;
std::list<std::string> chat_messages_;
@ -314,6 +316,8 @@ class Game : public Module {
std::unordered_map<SpecialChar, std::string> special_char_strings_;
bool ran_app_launch_commands_{};
bool kick_idle_players_{};
std::optional<float> idle_exit_minutes_{};
bool idle_exiting_{};
std::unique_ptr<TimerList> realtimers_;
Timer* process_timer_{};
Timer* headless_update_timer_{};

View File

@ -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) {

View File

@ -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<uint8_t> 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> player_;
Object::WeakRef<ConnectionToHost> 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);
};

View File

@ -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;

View File

@ -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<std::string, std::unordered_map<std::string, int> >
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_{};

View File

@ -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<long>( // 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<PyMethodDef> {
"\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"

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 {
assert(InGameThread());
assert(objexists(ObjID::kConfig));

View File

@ -5,6 +5,7 @@
#include <list>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <vector>
@ -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<float> default_value)
-> std::optional<float>;
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);

View File

@ -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);

View File

@ -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

View File

@ -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/'