Skip objects whose bodies won't parse instead of failing the load - #31
Merged
Merged
Conversation
A modded save serialises property shapes this parser has never seen. One such object sank the whole save: the payload/index build latched the first re-parse error and returned Err, so 106 Modular Load Balancer buildables out of 37,958 objects produced "Failed to load save: object re-parse failed during index build: ... String decode failure". The mod's mIndexMapping is a Map<ObjectProperty, StructProperty> whose LBBalancerIndexing value is a native struct written as 12 raw bytes, not a tagged property list. StructProperty and ArrayProperty already fall back to opaque bytes for unknown modded structs; the map-value path had no such fallback and parsed those bytes as a property name/type pair. Rather than teach the parser one more shape, stop treating an unparsable body as fatal. Every collector already skips a None object, and buildings render from the header (typePath/position/rotation), so all 159 balancers still draw -- only their body-derived contents are missing. The set of skipped objects is reported on the payload as `unreadableObjects` and noted in the load status line. Edits stay strictly checked: the failure set rides on MapIndex (so it survives the CBOR handoff to the lean worker) and finish_edit refuses an edit that makes a previously-parsable object stop parsing -- that is this editor corrupting the save, which the rebuild still exists to catch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two follow-ups from testing the skip on the reporter's save. The edit gate now lives on MapIndex and only rejects objects that EXISTED and parsed before the edit. Copy/paste splices an object's bytes under a fresh instanceName, so duplicating an unreadable modded buildable produces a new unreadable object; the earlier name-set diff read that as corruption and blocked a working operation. A mis-computed splice damaging a neighbour -- the case the check exists for -- still trips it. Editor ops parse their target eagerly, so editing an unreadable object now reports "String decode failure at offset 24369724 of length 3670015". That was unreachable before (the save did not load at all) and says nothing about what the user did. All fourteen op call sites reach an object through apply::fetch, so one wrap there names the object and explains it can be viewed but not edited. Adds tests/unparsable_objects.rs: a clobbered body must not fail the build, must be reported on the payload, must still render as a building, and the edit gate must fire on exactly the newly-broken pre-existing objects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 13, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #30.
The bug
The reported save is modded (Modular Load Balancers, FicsitWiremod and others). 106 of its 37,958 objects would not re-parse, and a single one of those sank the entire load:
All 106 are Modular Load Balancer buildables (80
Filtered_Output, 24Input, 2Output). Each carries anLBBalancerDatastruct holdingmIndexMapping, aMapPropertyofObjectProperty→StructProperty("LBBalancerIndexing"). That value is a native struct written as 12 raw bytes (threeint32s), not the tagged property list the parser expects:start + 115lands precisely on the next property, so the map body is well formed — the value is simply opaque.parse_propertiesread those bytes as a property name/type pair:ff ff ff ffdecoded as an empty UTF-16 string, then the next length came out offf ff 37 00=0x0037FFFF= 3670015, which fails UTF-8 validation. (The twoOutputfailures are the same bug with a different payload —0a 00 00 00 ...reads as a plausible length 10, hence the different message.)StructPropertyandArrayPropertyalready fall back to opaque bytes for unknown modded structs. The map-value path had no such fallback.The fix
Rather than teach the parser one more shape, stop treating an unparsable body as fatal. That fixes this class of bug for every mod rather than this one struct.
The skip machinery already existed — all 44
scan.parse_objectcall sites doelse { continue }. Only tworeturn Errchecks inbuild_all_jsonmade it fatal. Failed objects are now collected by instanceName, carried onMapIndex(so the set survives the CBOR handoff to the lean worker), and reported on the payload asunreadableObjects: { count, samples }. The load status line says so; type paths and byte offsets go to the console.Nothing is lost visually.
collect_buildingsis generic and header-only — it keeps any type path containing/Buildable/or/Build_and draws fromposition/rotation/instanceName, never parsing the body. All 159 balancers still render, under "Unknown", with correct labels and positions. Only their body-derived contents are missing.Edits stay strictly checked.
MapIndex::reject_new_parse_failuresrefuses an edit that makes a previously-parsable object stop parsing — a mis-computed splice damaging its neighbours still trips it. It is deliberately scoped to objects that existed before the edit: copy/paste splices bytes under a fresh instanceName, so duplicating an unreadable modded buildable legitimately produces a new unreadable object, and an earlier version of this check blocked that working operation.Clearer edit errors. Editor ops parse their target eagerly, so editing an unreadable object reported the raw
String decode failure at offset 24369724. That was unreachable before (the save did not load at all). All 14 op call sites reach an object throughapply::fetch, so one wrap there now names the object and explains it can be viewed but not edited.Verification
main(up to 379 MB) — no regression, and nounreadableObjectskey on a clean save.tests/unparsable_objects.rs: a clobbered body must not fail the build, must be reported, must still render as a building, and the edit gate must fire on exactly the newly-broken pre-existing objects.Not included
The opaque-map fallback that would let these buildables show their real contents. They render and can be selected, but their inventories are missing and they cannot be edited. Worth a follow-up if modded map-value structs turn up more often.
🤖 Generated with Claude Code