Skip to content

Pluggable game support#98

Draft
oMaN-Rod wants to merge 23 commits into
trumank:masterfrom
oMaN-Rod:pluggable-game-support
Draft

Pluggable game support#98
oMaN-Rod wants to merge 23 commits into
trumank:masterfrom
oMaN-Rod:pluggable-game-support

Conversation

@oMaN-Rod

Copy link
Copy Markdown

Pluggable per-game save support, with Palworld as the first implementation

Submitting as a draft. This is a fairly large change, and I'd rather make sure the overall direction makes sense before polishing everything. Happy to change any of it.

The main question is whether this is a direction you'd be interested in taking. I think a pluggable game layer keeps the core cleaner long-term, but I don't want to assume this is the right fit for uesave without getting feedback first.

Why

Palworld saves are GVAS with two extra layers:

  • A compressed container (PLM / PLZ / CNK)
  • Game-specific data stored in RawData byte arrays that need their own parsing

My first pass added PalXxx variants directly to StructValue and StructType. That worked, but it didn't feel like the right long-term design since every new game would need more core enum variants.

Instead, this PR adds a Game trait as an extension point. Palworld is the first implementation and lives entirely under uesave/src/games/palworld/. Most of this grew out of work I was already doing for Palworld Save Pal. After a few iterations, this ended up feeling like a cleaner direction than baking Palworld-specific logic into the core types.

What it does

The Game trait provides hooks for:

  • Reading and deserializing game-specific structs
  • Converting RawData byte arrays to typed values (and back)
  • Compressing/decompressing save containers

Most methods have defaults. Games that only need a custom container format only implement those hooks. Games with custom struct parsing implement the required struct methods.

Save<G> and SaveReader<G> are now generic over a game type, and StructValue gains a Game variant via ArchiveType::Game.

There's also an optional runtime registry (behind the cli feature) so tools can select games by name (--game palworld) without referring to Rust types.

Oodle

Palworld's PLM format uses Oodle compression, which requires an external decoder. This PR adds an optional oodle feature backed by ooz-rs.

The licensing for ooz is GPL-3.0, while uesave is MIT, so enabling the oodle feature makes the resulting binary GPL as well.

That feature is now opt-in everywhere:

Build License
Default MIT
--features oodle GPL-3.0

The downside is that a default build can't read normal Palworld (PLM) saves. Instead it reports:

Oodle compression support not enabled. Rebuild with --features oodle

I think keeping the default build MIT is the better tradeoff.

Compatibility

Save<G = NoGame> and SaveReader<G = NoGame> default to a no-op implementation, so normal GVAS usage is unchanged.

I verified this by building an existing consumer against both versions without modifying any code.

There are three breaking API changes:

  • StructType gains a Game variant.
  • StructValue gains a Game variant.
  • ArchiveType gets a required associated type and method, which affects external implementors.

The enum changes are straightforward. The ArchiveType change is the one I'm less happy with, and I'm open to alternatives.

Other changes

These aren't related to game support:

  • Enabled serde_json's float_roundtrip feature for exact float parsing.
  • Fixed a bug where to-json, from-json, and edit could truncate the destination file before a parse failure.

The truncation fix currently depends on the new CLI registry, so it isn't an easy cherry-pick. If you'd rather have that fixed independently, I can split it into a separate PR.

Testing

  • 50 unit tests with --all-features (46 without oodle)
  • All passing
  • Tested against 261 real Palworld saves
  • All binary and JSON round trips were byte-identical after decompression
  • Zlib container round trips are also byte-identical

Known issue

test-resave --game palworld currently reports failures for compressed saves because it compares the original compressed file against the uncompressed output from Save::write. The round trip itself was verified as part of the Palworld save testing above.

I left this alone because the right fix depends on what test-resave is intended to verify. Comparing against the decompressed original seems like the simplest option, but I wanted to get feedback first.

Questions

  • Does the MIT-by-default / GPL-on-opt-in split seem like the right tradeoff?
  • Should the runtime registry stay in uesave, or belong in tooling?
  • Would you prefer a smaller Game trait, even if it means fewer extension points?
  • Should Palworld stay in-tree, or move into its own crate?

oMaN-Rod added 23 commits July 18, 2026 13:35
Port of the palworld branch onto the reworked archive architecture:
- games/palworld module with typed parsers for data embedded in RawData
  byte arrays (characters, items, groups, guilds, base camps, map objects)
- Pal struct types wired into StructType/StructValue via the type macros
- post-read/pre-write archive hooks convert embedded byte arrays to typed
  struct values and back, driven by the Types path registry and recorded
  schemas; nested archives share version/types/scope/schemas
- schema-driven JSON deserialization seeds for Pal structs, accepting
  byte arrays for empty/unparsed payloads
- transparent decompression of compressed saves (Palworld PLM via
  optional oodle feature), Save::write_compressed for writing them
- lossy UTF-16/UTF-8 string decoding fallback with warnings
- CLI: --palworld on to-json/edit, --compress-oodle on from-json/edit
Two concerns in one commit (shared edits in serialization.rs):
- box the larger Pal StructValue variants to keep the enum size down
- accept externally tagged byte arrays for unparsed Pal payloads when
  deserializing from JSON
Team mission assignments, breed farm and lamp fields, weapon item name,
guild roles/markers, three new modules, and a work codec covering the new
Progress_MultiType work and its assignments.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant