Skip to content

Commit 2d86ddd

Browse files
committed
Fix missing-texture-fallback crash; replay hardening (seek asset-table fix, disk-spooled seek snapshots, parse robustness); clearer errors for unserializable app-config values; iOS/tvOS config-dir groundwork.
1 parent 8df22ca commit 2d86ddd

11 files changed

Lines changed: 323 additions & 89 deletions

File tree

.efrocachemap

Lines changed: 44 additions & 44 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
### 1.8.0 (build 22971, api 9, 2026-08-11)
1+
### 1.8.0 (build 22972, api 9, 2026-08-11)
22
- Fully implemented asset packages (more on this soon)
33
- Upgraded to Python 3.14. This gives us a few nice useful bits such as zstd
44
compression to help speed up online stuff and also means we can get rid of all

docs/design/spinoff.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,18 @@ that should disappear with the feature-set, wrap it in
113113
`__SPINOFF_REQUIRE_FOO_BEGIN__``_END__`. Don't relocate the
114114
host file just to satisfy the strip.
115115

116+
**Keep the stripped result format-stable (C++).** The stripped file
117+
must still be a fixed point of the destination's `clang-format`, or
118+
`pubsync pull` flags a bogus manual-merge: a function whose body is
119+
mostly one strip section can collapse to a single line under the
120+
public side's formatter, which then reads back as a public-side
121+
edit. When a strip section would leave behind a one-statement
122+
function body, keep a comment line *inside the function but outside
123+
the markers* so the stripped form stays multi-line. Precedent:
124+
`ReportHost()` in
125+
`src/ballistica/shared/foundation/fatal_error_report.cc`
126+
(hit 2026-08-11).
127+
116128
## Structuring code that crosses boundaries
117129

118130
The spinoff system's most subtle rule: **a file's location is its

pconfig/projectconfig.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"bauiv1lib": "buil"
2424
},
2525
"efrocache_repository_url": "https://files.ballistica.net/cache/ba1",
26-
"engine_build_number": 22971,
26+
"engine_build_number": 22972,
2727
"name": "BallisticaKit",
2828
"public": true,
2929
"python_paths": [
@@ -43,5 +43,5 @@
4343
"tests",
4444
"config"
4545
],
46-
"version": "1.8.0a79"
46+
"version": "1.8.0a80"
4747
}

src/assets/ba_data/python/baenv.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@
5555

5656
# Build number and version of the ballistica binary we expect to be
5757
# using.
58-
TARGET_BALLISTICA_BUILD = 22971
59-
TARGET_BALLISTICA_VERSION = '1.8.0a79'
58+
TARGET_BALLISTICA_BUILD = 22972
59+
TARGET_BALLISTICA_VERSION = '1.8.0a80'
6060

6161

6262
@dataclass

src/ballistica/base/assets/texture_asset.cc

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,35 @@ static constexpr bool kForceUncompressedDDS = false;
3535

3636
TextureAsset::TextureAsset() = default;
3737

38+
/// Derive the loader container hint from a resolved asset path. CAS
39+
/// blobs resolve to bare content-hash file names with no extension
40+
/// (all texture flavors are KTX2 containers), while every legacy
41+
/// on-disk path — including the headless ``.nop`` dummy — carries an
42+
/// extension for the loader's path-suffix sniff. Keying on the
43+
/// *resolved* path shape (rather than whether the *requested* name
44+
/// was a qualified ``<apverid>:<name>`` ref) matters because bare
45+
/// legacy names can still resolve to CAS blobs: a missing texture
46+
/// falls back to the builtin package's ``textures/white``, and that
47+
/// fallback must load rather than fail the asset (which is fatal if
48+
/// the render path later touches it).
49+
static auto DeriveContainerHint(const std::string& path) -> std::string {
50+
auto slash_pos = path.find_last_of("/\\");
51+
auto dot_pos =
52+
path.find('.', slash_pos == std::string::npos ? 0 : slash_pos + 1);
53+
if (dot_pos == std::string::npos) {
54+
return ".ktx2";
55+
}
56+
return {};
57+
}
58+
3859
TextureAsset::TextureAsset(const std::string& file_in, TextureType type_in,
3960
TextureMinQuality min_quality_in)
4061
: file_name_(file_in), type_(type_in), min_quality_(min_quality_in) {
4162
file_name_full_ = g_base->assets->FindAssetFile(
4263
type_ == TextureType::kCubeMap ? Assets::FileType::kCubeMapTexture
4364
: Assets::FileType::kTexture,
4465
file_in);
45-
// CAS-form ref (``<apverid>:<asset_name>``) resolves to a CAS blob
46-
// whose on-disk name is just a hash — no extension. Set an
47-
// explicit container hint so the loader can dispatch without
48-
// sniffing the path. Hardcoded to ``.ktx2`` (FALLBACK_V1 produces
49-
// KTX2 per initiative decision #12); Phase 3 construct-mode
50-
// replaces this with per-profile dispatch. Headless mode resolves
51-
// to a ``.nop`` dummy path which has its own loader branch, so
52-
// we leave the container empty in that case and let the matcher's
53-
// path-suffix fallback pick the right branch.
54-
if (file_in.find(':') != std::string::npos
55-
&& !file_name_full_.ends_with(".nop")) {
56-
container_ = ".ktx2";
57-
}
66+
container_ = DeriveContainerHint(file_name_full_);
5867
valid_ = true;
5968
}
6069

@@ -116,10 +125,8 @@ auto TextureAsset::ReResolveSource() -> bool {
116125
return false;
117126
}
118127
file_name_full_ = new_full;
119-
// Re-derive the container hint exactly as the constructor does: CAS blobs
120-
// are extensionless KTX2; the headless ``.nop`` dummy keeps it empty so the
121-
// matcher's path-suffix fallback picks the right branch.
122-
container_ = file_name_full_.ends_with(".nop") ? "" : ".ktx2";
128+
// Re-derive the container hint exactly as the constructor does.
129+
container_ = DeriveContainerHint(file_name_full_);
123130
return true;
124131
}
125132

src/ballistica/core/platform/apple/platform_apple.cc

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,21 @@ auto PlatformApple::GetDeviceUUIDInputs() -> std::list<std::string> {
137137

138138
auto PlatformApple::DoGetConfigDirectoryMonolithicDefault()
139139
-> std::optional<std::string> {
140-
#if BA_PLATFORM_IOS_TVOS
141-
// FIXME - this doesn't seem right.
142-
printf("FIXME: get proper default-config-dir\n");
143-
return std::string(getenv("HOME")) + "/Library";
140+
#if BA_PLATFORM_TVOS
141+
// tvOS gives us no persistent writable location (only Caches and tmp
142+
// are writable), so config state lives under our cache dir and may be
143+
// purged by the OS while we're not running. That's acceptable for
144+
// now: GameCenter auto-sign-in restores the account on a fresh
145+
// launch and important state lives server-side. If it proves a
146+
// problem in practice we can look at mirroring critical bits to
147+
// NSUserDefaults/iCloud KVS (or storing more via our own cloud).
148+
return std::string(BallisticaKit::FromCpp::getCacheDirectoryPath())
149+
+ "/config";
150+
#elif BA_PLATFORM_IOS
151+
// Sandboxed per-app, but use a subdir to match our macOS layout.
152+
return std::string(
153+
BallisticaKit::FromCpp::getApplicationSupportDirectoryPath())
154+
+ "/BallisticaKit";
144155
#elif BA_PLATFORM_MACOS && BA_XCODE_BUILD
145156
return std::string(BallisticaKit::CocoaFromCpp::getApplicationSupportPath())
146157
+ "/BallisticaKit";

src/ballistica/scene_v1/support/client_session.cc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,6 +1485,17 @@ void ClientSession::GetCorrectionMessages(
14851485
}
14861486

14871487
void ClientSession::DumpFullState(SessionStream* out) {
1488+
// Declare our asset-package table first so everything below can be
1489+
// written as indexed refs (mirroring HostSession::DumpFullState).
1490+
// This matters for replay seeks: the restore path resets the session
1491+
// (clearing the table) and then resumes reading the on-disk stream
1492+
// *past* its start-of-stream declarations, so the snapshot itself
1493+
// must re-establish the table or every indexed ref after a seek
1494+
// fails against an empty table.
1495+
if (!asset_package_table().empty()) {
1496+
out->DeclareAssetPackages(asset_package_table());
1497+
}
1498+
14881499
// Add all scenes.
14891500
for (auto&& i : scenes()) {
14901501
if (Scene* sg = i.get()) {

0 commit comments

Comments
 (0)