diff --git a/.efrocachemap b/.efrocachemap
index 78afb51e..7a2269bf 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -4064,50 +4064,50 @@
"build/assets/windows/Win32/ucrtbased.dll": "2def5335207d41b21b9823f6805997f1",
"build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae",
"build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599",
- "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "4542f7820b33b8f1f6719b17efa26453",
- "build/prefab/full/linux_arm64_gui/release/ballisticakit": "c94898207acb63e3e09cb08b50ebd287",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "ad606ee2ee1367b906c9cbb18c53baf6",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "776e2461a852753010278d5b90ff32df",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "aefcfd8774114e7cebc418d0adad56f2",
- "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "d21df3fc06db87786d68ccc1417d043e",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "7b218f79079e8e0d92b5f0a4afb0599d",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "94738f2f6fcbea9dad60af4bd6a7cd5d",
- "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "033b39b34a10d5bce6e53572a816d0ef",
- "build/prefab/full/mac_arm64_gui/release/ballisticakit": "f4eb1cc72a16eafcaba2d55c65f19a7b",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "fec5b1818aa97bc0dff1ddf127742574",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "76930acbbd2e8dd35f35639d85ce21ac",
- "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "d5ab5ad85dd8d113125d15f03c221db3",
- "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "29fbcf76221d71ea3ecb73d8ab353f7b",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "3d8b4b5d6ba376f2d280209e0aa3bcf2",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "fb9da4455aa7c0d8e5120a75ccaeabcb",
- "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "056f8137ba8b1179b66eea59944aefb2",
- "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "78f53224dd6052c93da31bda7ee1c84c",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "82128374a7eabc651061d778e099b923",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "2cd60cbebfbe25447791284b42c3caf9",
- "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "a589af5b31246539eac3264c829c41a0",
- "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "3dfaf945474294cb9f3808a835fb667c",
- "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "a589af5b31246539eac3264c829c41a0",
- "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "3dfaf945474294cb9f3808a835fb667c",
- "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "47e08d2f265f4dda15b309fa67ba163b",
- "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "c9a5225be07b4456e073014e1db2cafe",
- "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "47e08d2f265f4dda15b309fa67ba163b",
- "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "c9a5225be07b4456e073014e1db2cafe",
- "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "f79382e5342db6f38f4c07170589d62d",
- "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "b2d5386301891813a790f1a19d442022",
- "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "f79382e5342db6f38f4c07170589d62d",
- "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "b2d5386301891813a790f1a19d442022",
- "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "899eda04958efce6903b7dd2abe6c76f",
- "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "e98d76fe2d0af7e775801998d8591340",
- "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "e9d551c0bfbb330470b1e0784028e4d3",
- "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "e98d76fe2d0af7e775801998d8591340",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "77f25ad3348eb212969725c0ec7ebec8",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "df8038790ce73124950dc443c1e972ad",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "f184798b7973343cdc42d16e05aa335f",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "2e3ed4ea52261efd17d0927b21cf4075",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "1a5c374cfdd07ccbb2f1e9a44b886131",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "66da72816faa1d87d0b3fe677c6eb9c8",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "fbef69c5fba9b7c94d37e233eb6c9642",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "c37e1dc4f95a8db0b5059dae0b5f5241",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "3403caf882f5efc1c5a62bf452994bd1",
+ "build/prefab/full/linux_arm64_gui/release/ballisticakit": "c046dcf69b1d3cb122869ee1f8df06d6",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "10f90e814342ba8553c193d8ce3221d1",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "2827cb64562372b70fe78c30cc4dbf1e",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "37e25714c6b19f5edc1135820edb0ef4",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "bacec90936f644c739eb9230b61c1528",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "51e044f01c1f783f2c88d8ba490370a6",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "01aff77ee4c84b06b54664e26aaed0cf",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "7430ba5eadfdd15310d0b140d5ef3f2b",
+ "build/prefab/full/mac_arm64_gui/release/ballisticakit": "de88562b3c51a56a5a89bc35a7888e58",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "74b6f29023a4a3e90b2233d77c0c38bb",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "78150d99f0e8dfd2f6785af4335b3f49",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "ccc2f72a1a12a3314f6ffea3ea20875c",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "bab389beb2e52641e5a7e5cf9a62bf69",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "a3de45a3355c610719a477077babf451",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "cf35da279c2ff3d4a8241e51199fea5d",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "8fcb6c2f3be27b2aadee60a436ee5c82",
+ "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "695c693d94935c99e3990c885b882091",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "38a69802cca5b4bc3a344777f12679bd",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "4e24b9c6b980844de8c482dd1034fb97",
+ "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "a3607fd941915ab11503f82acfc392b5",
+ "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "b5a129d83796c9e7015ab5e319d2c22f",
+ "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "a3607fd941915ab11503f82acfc392b5",
+ "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "b5a129d83796c9e7015ab5e319d2c22f",
+ "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "13405a4a16a71d073b6b3cabbbcd9666",
+ "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "86b26dc84cc7fa7095e51cfcae759c0b",
+ "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "13405a4a16a71d073b6b3cabbbcd9666",
+ "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "86b26dc84cc7fa7095e51cfcae759c0b",
+ "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "678e09ecd5da367ce290ca7318617b61",
+ "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "a9cdc9dd029dabc6dfa5b61d33de7927",
+ "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "678e09ecd5da367ce290ca7318617b61",
+ "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "a9cdc9dd029dabc6dfa5b61d33de7927",
+ "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "4811585805942428ddb217917e4ad843",
+ "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "c5c40967e63471c9c4abd6dfbef892df",
+ "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "d34c0a142e7d391a109a33ea3cc77c08",
+ "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "c5c40967e63471c9c4abd6dfbef892df",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "4eb48f28a678a7f2eae8c10d4d86b879",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "675e12e534ebec6cb9760b066c158747",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "5d9bb2f2835069e186418b1b02598168",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "7fa0439993cdd80f098a2cf37d4e6b31",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "1bac9e9d670f8dfac75398085820d039",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "9ecb0b936f758250417f7c014f89d683",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "912935f68da6dfca6312b3859c14ee27",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "aa9ca81cff1cd3135af6fa81cb16851a",
"src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
"src/assets/ba_data/python/babase/_mgen/enums.py": "f8cd3af311ac63147882590123b78318",
"src/ballistica/base/mgen/pyembed/binding_base.inc": "ad347097a38e0d7ede9eb6dec6a80ee9",
diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index 993d45c3..77c090b9 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -823,6 +823,7 @@
dstent
dstfile
dstfin
+ dstfs
dstjson
dstlines
dstname
@@ -1085,6 +1086,7 @@
flycheck
fmod
fname
+ fnamefilt
fnamefull
fnames
fnmatch
@@ -2669,6 +2671,7 @@
shobs
shortname
shouldn
+ shouldnt
showbuffer
showpoints
showstats
@@ -2753,11 +2756,13 @@
splitnumstr
spwd
squadcore
+ src's
srcabs
srcattr
srcdata
srcdir
srcfolder
+ srcfs
srcgrp
srcid
srcjson
@@ -2840,6 +2845,7 @@
subdep
subdeps
subdirs
+ subdst
subfieldpath
subfolders
submpath
@@ -2852,6 +2858,7 @@
subprocesses
subrepos
subsel
+ subsrc
subsys
subtypestr
subval
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 101b2aa0..bbb43be2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
-### 1.7.28 (build 21289, api 8, 2023-08-31)
+### 1.7.28 (build 21293, api 8, 2023-08-31)
+- Added some high level functionality for copying and deleting feature-sets to
+ the `tools/spinoff` tool. For example, to create your own `poo` feature-set,
+ do `tools/spinoff fset-copy template_fs poo`. Then do `make update` and `make
+ cmake` to build and run the app, and from within it you should be able to do
+ `import bapoo` to get at your nice shiny poo feature-set. When you are done
+ playing, you can do `tools/spinoff fset-delete poo` to blow away any traces of
+ it.
+
### 1.7.27 (build 21282, api 8, 2023-08-30)
- Fixed a rare crash that could occur if the app shuts down while a background
diff --git a/ballisticakit-cmake/.idea/dictionaries/ericf.xml b/ballisticakit-cmake/.idea/dictionaries/ericf.xml
index 88a7f7dc..f1cbefcf 100644
--- a/ballisticakit-cmake/.idea/dictionaries/ericf.xml
+++ b/ballisticakit-cmake/.idea/dictionaries/ericf.xml
@@ -512,6 +512,7 @@
dstdir
dstdirfull
dstent
+ dstfs
dstnode
dstpath
dstr
@@ -662,6 +663,7 @@
flopsy
flushhhhh
fname
+ fnamefilt
fnode
fnsu
fnumc
@@ -1619,9 +1621,11 @@
spinups
spivak
spwd
+ src's
srcabs
srcattr
srcfolder
+ srcfs
srcgrp
srcid
srcname
@@ -1683,6 +1687,7 @@
subargs
subc
subclsssing
+ subdst
subentities
subfieldpath
subitems
@@ -1691,6 +1696,7 @@
subplatform
subscale
subscr
+ subsrc
subsys
subtypestr
successmsg
diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py
index 0dccbe5f..cdf8c69d 100644
--- a/src/assets/ba_data/python/babase/__init__.py
+++ b/src/assets/ba_data/python/babase/__init__.py
@@ -109,6 +109,7 @@ from babase._apputils import (
is_browser_likely_available,
garbage_collect,
get_remote_app_name,
+ AppHealthMonitor,
)
from babase._cloud import CloudSubsystem
from babase._emptyappmode import EmptyAppMode
@@ -176,6 +177,7 @@ __all__ = [
'app',
'App',
'AppConfig',
+ 'AppHealthMonitor',
'AppIntent',
'AppIntentDefault',
'AppIntentExec',
diff --git a/src/assets/ba_data/python/babase/_app.py b/src/assets/ba_data/python/babase/_app.py
index 9c1a2c6e..ab519610 100644
--- a/src/assets/ba_data/python/babase/_app.py
+++ b/src/assets/ba_data/python/babase/_app.py
@@ -13,6 +13,7 @@ from concurrent.futures import ThreadPoolExecutor
from functools import cached_property
from efro.call import tpartial
+
import _babase
from babase._language import LanguageSubsystem
from babase._plugin import PluginSubsystem
diff --git a/src/assets/ba_data/python/baclassic/_net.py b/src/assets/ba_data/python/baclassic/_net.py
index 7fd41d8f..a76258df 100644
--- a/src/assets/ba_data/python/baclassic/_net.py
+++ b/src/assets/ba_data/python/baclassic/_net.py
@@ -85,7 +85,9 @@ class MasterServerV1CallThread(threading.Thread):
# Tearing the app down while this is running can lead to
# rare crashes in LibSSL, so avoid that if at all possible.
- babase.shutdown_suppress_begin()
+ if not babase.shutdown_suppress_begin():
+ # App is already shutting down, so we're a no-op.
+ return
try:
classic = babase.app.classic
diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py
index b16b7924..c13351be 100644
--- a/src/assets/ba_data/python/baenv.py
+++ b/src/assets/ba_data/python/baenv.py
@@ -52,7 +52,7 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be
# using.
-TARGET_BALLISTICA_BUILD = 21289
+TARGET_BALLISTICA_BUILD = 21293
TARGET_BALLISTICA_VERSION = '1.7.28'
@@ -461,11 +461,12 @@ def _modular_main() -> None:
# First baenv sets up things like Python paths the way the engine
# needs them, and then we import and run the engine.
#
- # Below we're doing a slightly fancier version of that. Namely we do
- # some processing of command line args to allow overriding of paths
- # or running explicit commands or whatever else. Our goal is that
- # this modular form of the app should be basically indistinguishable
- # from the monolithic form when used from the command line.
+ # Below we're doing a slightly fancier version of that. Namely, we
+ # do some processing of command line args to allow overriding of
+ # paths or running explicit commands or whatever else. Our goal is
+ # that this modular form of the app should be basically
+ # indistinguishable from the monolithic form when used from the
+ # command line.
try:
# Take note that we're running via modular-main. The native
diff --git a/src/assets/ba_data/python/batemplatefs/__init__.py b/src/assets/ba_data/python/batemplatefs/__init__.py
index 387a84de..58ad2cd3 100644
--- a/src/assets/ba_data/python/batemplatefs/__init__.py
+++ b/src/assets/ba_data/python/batemplatefs/__init__.py
@@ -4,8 +4,12 @@
# ba_meta require api 8
+# Package up various private bits (including stuff from our native
+# module) into a nice clean public API.
+from _batemplatefs import hello_again_world
from batemplatefs._subsystem import TemplateFsSubsystem
__all__ = [
'TemplateFsSubsystem',
+ 'hello_again_world',
]
diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc
index e66d83e9..6a3b61e4 100644
--- a/src/ballistica/base/base.cc
+++ b/src/ballistica/base/base.cc
@@ -715,11 +715,33 @@ void BaseFeatureSet::DoPushObjCall(const PythonObjectSetBase* objset, int id,
}
auto BaseFeatureSet::IsAppStarted() const -> bool { return app_started_; }
-void BaseFeatureSet::ShutdownSuppressBegin() { shutdown_suppress_count_++; }
+
+auto BaseFeatureSet::ShutdownSuppressBegin() -> bool {
+ std::scoped_lock lock(shutdown_suppress_lock_);
+ if (!shutdown_suppress_disallowed_) {
+ return false;
+ }
+ shutdown_suppress_count_++;
+ return true;
+}
+
void BaseFeatureSet::ShutdownSuppressEnd() {
+ std::scoped_lock lock(shutdown_suppress_lock_);
shutdown_suppress_count_--;
assert(shutdown_suppress_count_ >= 0);
}
+
+auto BaseFeatureSet::ShutdownSuppressGetCount() -> int {
+ std::scoped_lock lock(shutdown_suppress_lock_);
+ return shutdown_suppress_count_;
+}
+
+void BaseFeatureSet::ShutdownSuppressDisallow() {
+ std::scoped_lock lock(shutdown_suppress_lock_);
+ assert(!shutdown_suppress_disallowed_);
+ shutdown_suppress_disallowed_ = true;
+}
+
auto BaseFeatureSet::GetReturnValue() const -> int { return return_value(); }
} // namespace ballistica::base
diff --git a/src/ballistica/base/base.h b/src/ballistica/base/base.h
index 34aab1d1..c828b325 100644
--- a/src/ballistica/base/base.h
+++ b/src/ballistica/base/base.h
@@ -3,6 +3,7 @@
#ifndef BALLISTICA_BASE_BASE_H_
#define BALLISTICA_BASE_BASE_H_
+#include
#include
#include
@@ -690,9 +691,18 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
void DoPushObjCall(const PythonObjectSetBase* objset, int id,
const std::string& arg) override;
void OnReachedEndOfBaBaseImport();
- void ShutdownSuppressBegin();
+
+ /// Begin a shutdown-suppressing operation. Returns true if the operation
+ /// can proceed; otherwise shutdown has already begun and the operation
+ /// should be aborted.
+ auto ShutdownSuppressBegin() -> bool;
+
+ /// End a shutddown-suppressing operation. Should only be called after a
+ /// successful begin.
void ShutdownSuppressEnd();
- auto shutdown_suppress_count() const { return shutdown_suppress_count_; }
+
+ auto ShutdownSuppressGetCount() -> int;
+ void ShutdownSuppressDisallow();
/// Called in the logic thread once our screen is up and assets are
/// loading.
@@ -756,6 +766,8 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
StressTest* stress_test_;
std::string console_startup_messages_;
+ std::mutex shutdown_suppress_lock_;
+ bool shutdown_suppress_disallowed_;
int shutdown_suppress_count_{};
bool tried_importing_plus_{};
bool tried_importing_classic_{};
diff --git a/src/ballistica/base/logic/logic.cc b/src/ballistica/base/logic/logic.cc
index 75b62fa1..884c8190 100644
--- a/src/ballistica/base/logic/logic.cc
+++ b/src/ballistica/base/logic/logic.cc
@@ -226,7 +226,10 @@ void Logic::OnAppShutdown() {
// Nuke the app from orbit if we get stuck while shutting down.
g_core->StartSuicideTimer("shutdown", 10000);
- // Let our subsystems know we're shutting down.
+ // Tell base to disallow shutdown-suppressors from here on out.
+ g_base->ShutdownSuppressDisallow();
+
+ // Let our logic thread subsystems know we're shutting down.
// Note: Keep these in opposite order of OnAppStart.
// Note2: Any shutdown processes that take a non-zero amount of time
// should be registered as shutdown-tasks
diff --git a/src/ballistica/base/python/methods/python_methods_app.cc b/src/ballistica/base/python/methods/python_methods_app.cc
index e6f40764..94a959df 100644
--- a/src/ballistica/base/python/methods/python_methods_app.cc
+++ b/src/ballistica/base/python/methods/python_methods_app.cc
@@ -1495,8 +1495,12 @@ static PyMethodDef PyGetImmediateReturnCodeDef = {
static auto PyShutdownSuppressBegin(PyObject* self) -> PyObject* {
BA_PYTHON_TRY;
assert(g_base);
- g_base->ShutdownSuppressBegin();
- Py_RETURN_NONE;
+ auto val = g_base->ShutdownSuppressBegin();
+ if (val) {
+ Py_RETURN_TRUE;
+ } else {
+ Py_RETURN_FALSE;
+ }
BA_PYTHON_CATCH;
}
@@ -1505,7 +1509,7 @@ static PyMethodDef PyShutdownSuppressBeginDef = {
(PyCFunction)PyShutdownSuppressBegin, // method
METH_NOARGS, // flags
- "shutdown_suppress_begin() -> None\n"
+ "shutdown_suppress_begin() -> bool\n"
"\n"
"(internal)\n",
};
@@ -1536,7 +1540,7 @@ static PyMethodDef PyShutdownSuppressEndDef = {
static auto PyShutdownSuppressCount(PyObject* self) -> PyObject* {
BA_PYTHON_TRY;
assert(g_base);
- return PyLong_FromLong(g_base->shutdown_suppress_count());
+ return PyLong_FromLong(g_base->ShutdownSuppressGetCount());
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
diff --git a/src/ballistica/core/core.cc b/src/ballistica/core/core.cc
index 1374988e..07a3b442 100644
--- a/src/ballistica/core/core.cc
+++ b/src/ballistica/core/core.cc
@@ -15,9 +15,8 @@ CoreFeatureSet* g_core{};
BaseSoftInterface* g_base_soft{};
auto CoreFeatureSet::Import(const CoreConfig* config) -> CoreFeatureSet* {
- // In monolithic builds we can accept an explicit core-config the first
- // time we're imported. In this case, Python is not even spun up yet so
- // it can influence even that.
+ // In monolithic builds, we accept an explicit core-config the first time
+ // we're imported. It is fully up to the caller to build the config.
if (g_buildconfig.monolithic_build()) {
if (config != nullptr) {
if (g_core != nullptr) {
@@ -29,17 +28,17 @@ auto CoreFeatureSet::Import(const CoreConfig* config) -> CoreFeatureSet* {
DoImport(*config);
}
} else {
- // No config passed; use a default.
+ // If no config is passed, use a default. If the user wants env vars
+ // or anything else factored in, they should do so themselves in the
+ // config they pass (CoreConfig::ForEnvVars(), etc.).
if (g_core == nullptr) {
DoImport({});
}
}
} else {
- // In modular builds we autogenerate a CoreConfig that takes into
- // account only env-vars (or env-vars plus Python args if we're being
- // run via the baenv script). In this case, Python is already spun up
- // and baenv already handled any Python environment stuff so we have
- // less to do.
+ // In modular builds, we generate a CoreConfig *after* Python is spun
+ // up, implicitly using Python's sys args and/or env vars when
+ // applicable.
if (config != nullptr) {
FatalError("CoreConfig can't be explicitly passed in modular builds.");
}
@@ -57,6 +56,7 @@ auto CoreFeatureSet::Import(const CoreConfig* config) -> CoreFeatureSet* {
DoImport(CoreConfig::ForArgsAndEnvVars(static_cast(argv.size()),
argv.data()));
} else {
+ // Not using Python sys args but we still want to process env vars.
DoImport(CoreConfig::ForEnvVars());
}
}
diff --git a/src/ballistica/core/core.h b/src/ballistica/core/core.h
index 49f0c342..08ad6331 100644
--- a/src/ballistica/core/core.h
+++ b/src/ballistica/core/core.h
@@ -24,27 +24,26 @@ class CoreFeatureSet;
class BaseSoftInterface;
// Our feature-set's globals.
-// Feature-sets should NEVER directly access globals in another feature-set's
-// namespace. All functionality we need from other feature-sets should be
-// imported into globals in our own namespace. Generally we do this when we
-// are initially imported (just as regular Python modules do).
+//
+// Feature-sets should NEVER directly access globals in another
+// feature-set's namespace. All functionality we need from other
+// feature-sets should be imported into globals in our own namespace.
+// Generally we do this when we are initially imported (just as regular
+// Python modules do).
+
+// Our pointer to our own feature-set.
extern CoreFeatureSet* g_core;
-// We don't require the base feature-set but can use it if present.
-// Base will supply us with this pointer if/when it spins up.
-// So we must never assume this pointer is valid and must check for it
-// with each use.
+// We don't require the base feature-set but can use it if present. Base
+// will supply us with this pointer if/when it spins up. So we must never
+// assume this pointer is valid and must check for it with each use.
extern BaseSoftInterface* g_base_soft;
-/// Platform-agnostic global state for our overall system.
-/// This gets created whenever we are used in any capacity, even if
-/// we don't create/run an app.
-/// Ideally most things here should be migrated to more specific
-/// subsystems.
+/// Core engine functionality.
class CoreFeatureSet {
public:
- /// Import the core feature set. A core-config can be passed ONLY
- /// in monolithic builds when it is guaranteed that the Import will be
+ /// Import the core feature set. A core-config can be passed ONLY in
+ /// monolithic builds when it is guaranteed that the Import will be
/// allocating the CoreFeatureSet singleton.
static auto Import(const CoreConfig* config = nullptr) -> CoreFeatureSet*;
@@ -76,32 +75,35 @@ class CoreFeatureSet {
auto HeadlessMode() -> bool;
/// Return current app-time in milliseconds.
+ ///
/// App-time is basically the total time that the engine has been actively
- /// running. (The 'App' here is a slight misnomer). It will stop progressing
- /// while the app is suspended and will never go backwards.
+ /// running. (The 'App' here is a slight misnomer). It will stop
+ /// progressing while the app is suspended and will never go backwards.
auto GetAppTimeMillisecs() -> millisecs_t;
/// Return current app-time in microseconds.
+ ///
/// App-time is basically the total time that the engine has been actively
- /// running. (The 'App' here is a slight misnomer). It will stop progressing
- /// while the app is suspended and will never go backwards.
+ /// running. (The 'App' here is a slight misnomer). It will stop
+ /// progressing while the app is suspended and will never go backwards.
auto GetAppTimeMicrosecs() -> microsecs_t;
/// Return current app-time in seconds.
+ ///
/// App-time is basically the total time that the engine has been actively
- /// running. (The 'App' here is a slight misnomer). It will stop progressing
- /// while the app is suspended and will never go backwards.
+ /// running. (The 'App' here is a slight misnomer). It will stop
+ /// progressing while the app is suspended and will never go backwards.
auto GetAppTimeSeconds() -> double;
- /// Are we in the thread the main event loop is running on?
- /// Generally this is the thread that runs graphics and os event processing.
+ /// Are we in the thread the main event loop is running on? Generally this
+ /// is the thread that runs graphics and os event processing.
auto InMainThread() -> bool;
/// Log a boot-related message (only if core_config.lifecycle_log is true).
void LifecycleLog(const char* msg, double offset_seconds = 0.0);
- /// Base path of build src dir so we can attempt to remove it from
- /// any source file paths we print.
+ /// Base path of build src dir so we can attempt to remove it from any
+ /// source file paths we print.
auto build_src_dir() const { return build_src_dir_; }
const auto& legacy_user_agent_string() const {
@@ -113,8 +115,8 @@ class CoreFeatureSet {
}
/// Return true if baenv values have been locked in: python paths, log
- /// handling, etc. Early-running code may wish to explicitly avoid making log
- /// calls until this condition is met to ensure predictable behavior.
+ /// handling, etc. Early-running code may wish to explicitly avoid making
+ /// log calls until this condition is met to ensure predictable behavior.
auto have_ba_env_vals() const { return have_ba_env_vals_; }
/// Return the directory where the app expects to find its bundled Python
@@ -138,8 +140,8 @@ class CoreFeatureSet {
/// Return the directory where bundled 3rd party Python files live.
auto GetSitePythonDirectory() -> std::optional;
- // Are we using a non-standard app python dir (such as a 'sys' dir within a
- // user-python-dir).
+ // Are we using a non-standard app python dir (such as a 'sys' dir within
+ // a user-python-dir).
auto using_custom_app_python_dir() const {
return using_custom_app_python_dir_;
}
@@ -165,7 +167,6 @@ class CoreFeatureSet {
bool reset_vr_orientation{};
bool user_ran_commands{};
std::thread::id main_thread_id{};
-
bool vr_mode;
std::mutex thread_name_map_mutex;
std::unordered_map thread_name_map;
@@ -177,11 +178,11 @@ class CoreFeatureSet {
#endif
private:
+ explicit CoreFeatureSet(CoreConfig config);
+ static void DoImport(const CoreConfig& config);
static auto CalcBuildSrcDir() -> std::string;
void RunSanityChecks();
- static void DoImport(const CoreConfig& config);
void UpdateAppTime();
- explicit CoreFeatureSet(CoreConfig config);
void PostInit();
bool tried_importing_base_{};
EventLoop* main_event_loop_{};
diff --git a/src/ballistica/core/platform/core_platform.cc b/src/ballistica/core/platform/core_platform.cc
index 9f201691..899aaff5 100644
--- a/src/ballistica/core/platform/core_platform.cc
+++ b/src/ballistica/core/platform/core_platform.cc
@@ -27,8 +27,9 @@
// ------------------------- PLATFORM SELECTION --------------------------------
-// This ugly chunk of macros simply pulls in the correct platform class header
-// for each platform and defines the actual class g_core->platform will be.
+// This ugly chunk of macros simply pulls in the correct platform class
+// header for each platform and defines the actual class g_core->platform
+// will be.
// Android ---------------------------------------------------------------------
@@ -85,6 +86,7 @@
// A call that can be used by custom built native libraries (Python, etc.)
// to forward along debug messages to us.
+//
// FIXME: Reconcile this with our existing C++ version. This one does not
// require the engine to be spun up so it better suited for things like
// debugging native libs.
diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc
index d8ce5baf..133ae48c 100644
--- a/src/ballistica/shared/ballistica.cc
+++ b/src/ballistica/shared/ballistica.cc
@@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int {
namespace ballistica {
// These are set automatically via script; don't modify them here.
-const int kEngineBuildNumber = 21289;
+const int kEngineBuildNumber = 21293;
const char* kEngineVersion = "1.7.28";
const int kEngineApiVersion = 8;
diff --git a/src/ballistica/template_fs/README.md b/src/ballistica/template_fs/README.md
index 161fff86..138d5097 100644
--- a/src/ballistica/template_fs/README.md
+++ b/src/ballistica/template_fs/README.md
@@ -1,4 +1,4 @@
-# Template Feature Set
+# Template Fs Feature Set
This is an empty feature-set for use as reference or as a starting point when
implementing new ones.
diff --git a/src/ballistica/template_fs/python/template_fs_python.cc b/src/ballistica/template_fs/python/template_fs_python.cc
index bd5b2b13..d179bab7 100644
--- a/src/ballistica/template_fs/python/template_fs_python.cc
+++ b/src/ballistica/template_fs/python/template_fs_python.cc
@@ -9,16 +9,18 @@
namespace ballistica::template_fs {
-// Declare a plain c PyInit_XXX function for our Python module;
-// this is how Python inits our binary module (and by extension, our
-// entire feature-set).
+// Declare a plain C PyInit_XXX function for our Python module. This is how
+// Python inits our binary module (and by extension, our entire
+// feature-set).
extern "C" auto PyInit__batemplatefs() -> PyObject* {
auto* builder = new PythonModuleBuilder(
"_batemplatefs",
- // Native methods to add.
+
+ // Our native methods.
{PythonMethodsTemplateFs::GetMethods()},
- // Our module exec. Here we can add classes, import other modules,
- // or whatever else (same as a regular Python script module).
+
+ // Our module exec. Here we can add classes, import other modules, or
+ // whatever else (same as a regular Python script module).
[](PyObject* module) -> int {
BA_PYTHON_TRY;
TemplateFsFeatureSet::OnModuleExec(module);
@@ -38,14 +40,16 @@ void TemplateFsPython::ImportPythonObjs() {
void TemplateFsPython::HelloWorld() {
// Hold the GIL throughout this call so we can run in any thread.
- // Alternately we could limit this function to the logic thread
- // which always holds the GIL. In that case we'd want to
- // stick a BA_PRECONDITION(InLogicThread()) here to be sure.
+ // Alternately, we could limit this function to the logic thread which
+ // always holds the GIL. In that case we'd want to stick a
+ // BA_PRECONDITION(InLogicThread()) here to so we'd raise an Exception if
+ // someone called us from another thread.
auto gil{Python::ScopedInterpreterLock()};
- // Run the Python callable we grabbed. This will simply print any
- // errors, but we could disable that print and look at the call
- // results if any logic depended on this code running successfully.
+ // Run the Python callable we grabbed in our binding code. By default,
+ // this Call() will simply print any errors, but we could disable that
+ // print and look at the call results if any logic depended on this code
+ // running successfully.
objs_.Get(ObjID::kHelloWorldCall).Call();
}
diff --git a/tools/batools/spinoff/_main.py b/tools/batools/spinoff/_main.py
index 5eb5aac4..ec394a4b 100644
--- a/tools/batools/spinoff/_main.py
+++ b/tools/batools/spinoff/_main.py
@@ -9,13 +9,17 @@ import sys
import subprocess
from enum import Enum
from pathlib import Path
-from typing import assert_never
+from typing import assert_never, TYPE_CHECKING
from efro.error import CleanError
from efro.terminal import Clr
from efrotools import replace_exact
+
from batools.spinoff._context import SpinoffContext
+if TYPE_CHECKING:
+ from batools.featureset import FeatureSet
+
class Command(Enum):
"""Our top level commands."""
@@ -32,6 +36,8 @@ class Command(Enum):
FEATURESETS = 'featuresets'
CREATE = 'create'
ADD_SUBMODULE_PARENT = 'add-submodule-parent'
+ FEATURE_SET_COPY = 'fset-copy'
+ FEATURE_SET_DELETE = 'fset-delete'
def spinoff_main() -> None:
@@ -101,6 +107,10 @@ def _main() -> None:
public = getprojectconfig(Path(dst_root))['public']
_do_add_submodule_parent(dst_root, is_new=False, public=public)
+ elif cmd is Command.FEATURE_SET_COPY:
+ _do_featureset_copy()
+ elif cmd is Command.FEATURE_SET_DELETE:
+ _do_featureset_delete()
else:
assert_never(cmd)
@@ -250,6 +260,251 @@ def _do_featuresets(dst_root: str) -> None:
print(f' {Clr.BLU}{fset.name}{Clr.RST}')
+def _fset_paths() -> list[str]:
+ """Given a feature-set, return all paths associated with it."""
+ return [
+ 'config/featuresets/featureset_$(NAME).py',
+ 'src/assets/ba_data/python/$(NAME_PY_PKG)',
+ 'src/ballistica/$(NAME)',
+ 'src/meta/$(NAME_PY_PKG_META)',
+ ]
+
+
+def _filter_fset_path(path: str, fset: FeatureSet) -> str:
+ return (
+ path.replace('$(NAME)', fset.name)
+ .replace('$(NAME_PY_PKG)', fset.name_python_package)
+ .replace('$(NAME_PY_PKG_META)', fset.name_python_package_meta)
+ )
+
+
+def _do_featureset_delete() -> None:
+ from batools.featureset import FeatureSet
+
+ args = sys.argv[2:]
+ if len(args) != 1:
+ raise CleanError('Expected a featureset name.')
+
+ name = args[0]
+
+ # Just make a theoretical new featureset in case only parts of it
+ # exist. (custom name formatting shouldnt matter here anyway)
+ fset = FeatureSet(name)
+
+ if not os.path.exists('config/featuresets'):
+ raise CleanError('Cannot run from this directory.')
+
+ paths_to_delete: list[str] = []
+ for path in _fset_paths():
+ paths_to_delete.append(_filter_fset_path(path, fset))
+
+ print(
+ '\n' + '⎯' * 80 + f'\n{Clr.BLD}Deleting feature-set{Clr.RST}'
+ f' {Clr.SMAG}{Clr.BLD}{name}{Clr.RST}{Clr.BLD}...{Clr.RST}\n'
+ + '⎯' * 80
+ + '\n'
+ )
+
+ found_something = False
+ for path in paths_to_delete:
+ if os.path.exists(path):
+ found_something = True
+ print(f' Deleting {Clr.MAG}{path}{Clr.RST}')
+ subprocess.run(['rm', '-rf', path], check=True)
+ if not found_something:
+ print(
+ f' {Clr.WHT}No feature-set components found;'
+ f' nothing to be done.{Clr.RST}'
+ )
+
+ print(
+ f"\n{Clr.GRN}{Clr.BLD}Job's done!{Clr.RST}\n"
+ f'{Clr.BLD}Next, run'
+ f' {Clr.BLU}`make update`{Clr.RST}{Clr.BLD} to update project'
+ f' files to reflect these changes.{Clr.RST}'
+ )
+
+
+def _do_featureset_copy() -> None:
+ # pylint: disable=too-many-locals
+ from efrotools import extract_flag
+
+ from batools.featureset import FeatureSet
+
+ args = sys.argv[2:]
+
+ force = extract_flag(args, '--force')
+
+ if len(args) != 2:
+ raise CleanError('Expected a src and dst featureset name.')
+
+ src = args[0]
+ dst = args[1]
+
+ if not os.path.exists('config/featuresets'):
+ raise CleanError('Cannot run from this directory.')
+
+ # This will make sure both feature-set names are valid and give us
+ # name variations. Load src from the project to pick up custom title
+ # variations/etc.
+ fsets = {f.name: f for f in FeatureSet.get_all_for_project('.')}
+ if src not in fsets:
+ raise CleanError('src feature-set {src} not found.')
+ srcfs = fsets[src]
+ # Just go with defaults for dst. Note that this means any custom
+ # title forms in src's config script will get filtered to be setting
+ # the default form of dst, which is redundant. Maybe we could filter that
+ # out.
+ dstfs = FeatureSet(dst)
+
+ # Make sure src *does* exist.
+ if not os.path.exists(f'config/featuresets/featureset_{src}.py'):
+ raise CleanError(f"Src feature-set '{src}' not found.")
+
+ # Make sure dst does *not* exist (unless we're forcing).
+ if os.path.exists(f'config/featuresets/featureset_{dst}.py') and not force:
+ raise CleanError(
+ f"Dst feature-set '{dst}' already exists."
+ ' Use --force to blow it away.'
+ )
+
+ paths_to_copy: list[tuple[str, str]] = []
+ for path in _fset_paths():
+ paths_to_copy.append(
+ (_filter_fset_path(path, srcfs), _filter_fset_path(path, dstfs))
+ )
+
+ # Replace variations of our name. Note that we don't have to include
+ # stuff like name_python_package_meta here because that is covered
+ # by our base name replacement. Also note that we include upper()
+ # for C/C++ header #ifndefs.
+ subs = [
+ (srcfs.name, dstfs.name),
+ (srcfs.name_compact, dstfs.name_compact),
+ (srcfs.name_title, dstfs.name_title),
+ (srcfs.name_camel, dstfs.name_camel),
+ (srcfs.name.upper(), dstfs.name.upper()),
+ ]
+
+ # Sanity check: we don't currently support renaming subdirs, so error
+ # if that would happen.
+ for srcpath, _dstpath in paths_to_copy:
+ for root, dirs, _fnames in os.walk(srcpath):
+ for dname in dirs:
+ if any(sub[0] in dname for sub in subs):
+ raise CleanError(
+ 'Directory name filtering is not supported'
+ f" (would filter '{root}/{dname}')."
+ )
+
+ # ------------------------------------------------------------------------
+ # Ok, at this point we get started working and assume things will succeed.
+ # If anything fails at this point we should add a pre-check for it above.
+ print(
+ '\n' + '⎯' * 80 + f'\n{Clr.BLD}Copying feature-set{Clr.RST}'
+ f' {Clr.SMAG}{Clr.BLD}{src}{Clr.RST}'
+ f' {Clr.BLD}to{Clr.RST}'
+ f' {Clr.SMAG}{Clr.BLD}{dst}{Clr.RST}'
+ f'{Clr.BLD}...{Clr.RST}\n' + '⎯' * 80
+ )
+
+ print(f'\n{Clr.BLD}Will filter the following text:{Clr.RST}')
+ for subsrc, subdst in subs:
+ print(
+ f' {Clr.MAG}{subsrc}{Clr.RST}'
+ f' {Clr.BLD}->{Clr.RST} {Clr.MAG}{subdst}{Clr.RST}'
+ )
+
+ print(f'\n{Clr.BLD}Copying/filtering files...{Clr.RST}')
+
+ for srcpath, dstpath in paths_to_copy:
+ _do_featureset_copy_dir(srcpath, dstpath, subs, force)
+
+ print(
+ f"\n{Clr.GRN}{Clr.BLD}Job's done!{Clr.RST}\n"
+ f'{Clr.BLD}Next, run'
+ f' {Clr.BLU}`make update`{Clr.RST}{Clr.BLD} to update project'
+ f' files to reflect these changes.{Clr.RST}'
+ )
+
+
+def _do_featureset_copy_dir(
+ srcpath: str, dstpath: str, subs: list[tuple[str, str]], force: bool
+) -> None:
+ # pylint: disable=too-many-locals
+ # pylint: disable=too-many-branches
+
+ # This feature-set might not have this component. No biggie.
+ if not os.path.exists(srcpath):
+ return
+
+ if force:
+ subprocess.run(['rm', '-rf', dstpath], check=True)
+
+ if not os.path.exists(srcpath):
+ raise CleanError(f'src path {srcpath} is not a dir.')
+ if os.path.exists(dstpath):
+ raise CleanError(f'dst path {srcpath} already exists.')
+
+ filtered_exts = ['.cc', '.h', '.py', '.md', '.inc']
+
+ # Eww; reinventing the wheel here; should tap into existing
+ # spinoff logic or something.
+ cruft_names = ['.DS_Store', 'mgen', '_mgen']
+
+ # We currently just copy the full dir and then rename/filter
+ # individual files. If we need to filter subdir names at some point
+ # we'll need fancier code.
+ subprocess.run(['cp', '-r', srcpath, dstpath], check=True)
+ for root, dnames, fnames in os.walk(dstpath, topdown=True):
+ for dname in dnames:
+ if dname in cruft_names:
+ # Prevent us from recursing into it and blow it away.
+ dnames.remove(dname)
+ subprocess.run(
+ ['rm', '-rf', os.path.join(root, dname)], check=True
+ )
+ for fname in fnames:
+ if fname in cruft_names:
+ os.unlink(os.path.join(root, fname))
+ continue
+ fnamefilt = fname
+ for subsrc, subdst in subs:
+ fnamefilt = fnamefilt.replace(subsrc, subdst)
+ if fnamefilt != fname:
+ subprocess.run(
+ [
+ 'mv',
+ os.path.join(root, fname),
+ os.path.join(root, fnamefilt),
+ ],
+ check=True,
+ )
+ # Now filter contents.
+ if not any(fname.endswith(ext) for ext in filtered_exts):
+ print(
+ f'{Clr.YLW}WARNING:'
+ f' not filtering file with unrecognized extension:'
+ f" '{fname}'{Clr.RST}"
+ )
+ continue
+ with open(
+ os.path.join(root, fnamefilt), encoding='utf-8'
+ ) as infile:
+ contents = infile.read()
+ for subsrc, subdst in subs:
+ contents = contents.replace(subsrc, subdst)
+ with open(
+ os.path.join(root, fnamefilt), 'w', encoding='utf-8'
+ ) as outfile:
+ outfile.write(contents)
+
+ print(
+ f' {Clr.MAG}{srcpath}{Clr.RST} {Clr.BLD}->{Clr.RST}'
+ f' {Clr.MAG}{dstpath}{Clr.RST}'
+ )
+
+
def _do_override(src_root: str | None, dst_root: str) -> None:
if src_root is None:
raise CleanError('This only works on dst projects.')
@@ -364,7 +619,14 @@ def _print_available_commands() -> None:
' The same can be\n'
' achieved by passing --submodule-parent to'
' the \'create\'\n'
- ' command.'
+ ' command.\n'
+ f' {bgn}fset-copy [src, dst]{end} Copy feature-set src to dst.'
+ ' Replaces variations of src\n'
+ ' feature-set name with dst equivalent,'
+ ' though may need\n'
+ ' some manual correction afterwards to be'
+ ' functional.\n'
+ f' {bgn}fset-delete [name]{end} Delete a feature-set.'
),
file=sys.stderr,
)