Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
61f2e3c
Add Palworld support
oMaN-Rod Jul 9, 2026
3071991
Enable serde_json float_roundtrip for exact float parsing
oMaN-Rod Jul 9, 2026
96fb15a
Fix Palworld type hint paths for current scope conventions
oMaN-Rod Jul 9, 2026
cd76c63
Keep embedded map object data as raw bytes unless fully parsed
oMaN-Rod Jul 9, 2026
9c2d376
Box larger Pal struct values; accept raw byte payloads in JSON
oMaN-Rod Jul 9, 2026
32b9437
Decode mission id in current CharacterTeamMissionModel layout
oMaN-Rod Jul 9, 2026
e9bad54
Add decompress example
oMaN-Rod Jul 9, 2026
51b92e2
Guard palworld registry against python skip-list paths
oMaN-Rod Jul 10, 2026
9722462
Add PlZ/CNK zlib save compression
oMaN-Rod Jul 11, 2026
a71044b
Parse Palworld 2026-07 save format
oMaN-Rod Jul 12, 2026
8cc2e0c
Add Game trait, NoGame default, and ArchiveType::Game extension point
oMaN-Rod Jul 18, 2026
622ecce
Thread game type parameter through SaveGameArchive and Save
oMaN-Rod Jul 18, 2026
9eb8cd8
Move Palworld struct types behind the Game trait and route archive hooks
oMaN-Rod Jul 18, 2026
8bc6cb1
Add game-aware JSON deserialization and make uesave_cli game-aware
oMaN-Rod Jul 18, 2026
d2300cf
Move Palworld compression formats behind the Game trait
oMaN-Rod Jul 18, 2026
4eb12a5
Harden struct-header write path and test game-struct dispatch
oMaN-Rod Jul 18, 2026
b403456
Add game registry and type-erased CLI facade
oMaN-Rod Jul 18, 2026
835522b
Put the game registry behind an optional cli feature
oMaN-Rod Jul 18, 2026
c886370
Register Palworld with the game registry
oMaN-Rod Jul 18, 2026
6b8334a
Assert Palworld registration and default types
oMaN-Rod Jul 18, 2026
16f2d80
Select games by name in uesave_cli via the game registry
oMaN-Rod Jul 18, 2026
7c252d6
Fix destination truncation on parse failure
oMaN-Rod Jul 18, 2026
a527178
Update Local_MaxFriendshipPalIds in palworld_types, Guid to Struct
oMaN-Rod Jul 19, 2026
0aaf741
Bound untrusted length pre-allocation to fix wasm32 capacity overflow
oMaN-Rod Aug 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion uesave/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,20 @@ license.workspace = true
[features]
default = []
tracing = ["dep:tracing"]
oodle = ["dep:ooz-rs"]
cli = ["dep:serde_json"]

[dependencies]
byteorder = "1.5.0"
flate2 = "1"
serde = { version = "1.0.195", features = ["derive"] }
thiserror = "1.0.56"
indexmap = { version = "2.1.0", features = ["serde"] }
bitflags = "2.6.0"
tracing = { version = "0.1.37", optional = true }
ooz-rs = { git = "https://github.com/palworld-save-pal/ooz-rs", optional = true }
serde_json = { version = "1.0", features = ["float_roundtrip"], optional = true }

[dev-dependencies]
pretty_assertions = "1.4.0"
serde_json = "1.0"
serde_json = { version = "1.0", features = ["float_roundtrip"] }
17 changes: 17 additions & 0 deletions uesave/examples/decompress/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//! Decompress a (potentially compressed) Palworld save file to plain GVAS.
//!
//! Usage: cargo run --example decompress --features oodle -- <input.sav> <output.gvas>

use uesave::games::palworld::Palworld;
use uesave::Game;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let input = args.next().ok_or("missing input path")?;
let output = args.next().ok_or("missing output path")?;

let mut reader = std::io::BufReader::new(std::fs::File::open(input)?);
let data = Palworld::decompress_save(&mut reader)?;
std::fs::write(output, data)?;
Ok(())
}
99 changes: 92 additions & 7 deletions uesave/src/archive.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use std::io::{Read, Seek, Write};

use crate::{Header, PropertyTagPartial, Result, SaveGameArchive, StructType, VersionInfo};
use crate::game::Game;
use crate::{
Header, Property, PropertyKey, PropertyTagPartial, Result, SaveGameArchive, StructType,
VersionInfo,
};

/// Defines the type system for an archive format.
///
Expand All @@ -27,6 +31,25 @@ pub trait ArchiveType: Clone + PartialEq + std::fmt::Debug + Default + serde::Se
+ std::fmt::Debug
+ serde::Serialize
+ for<'de> serde::Deserialize<'de>;

/// The game this archive type is bound to. Determines which game-specific
/// struct types and read/write hooks are used.
type Game: crate::game::Game;

/// Deserialize a schema-aware [`crate::Properties`] map for this archive
/// type. Game structs that embed nested properties (e.g. Palworld's
/// `PalCharacterData`) route their `Properties` field through here so the
/// property tags recorded at `{path}.{field}` in `schemas` are used to
/// interpret the untyped JSON. Dispatched on `Self` so the concrete
/// [`crate::Game`] behind `Self::Game` is known.
fn deserialize_properties<'de, D>(
path: &str,
schemas: &crate::PropertySchemas,
deserializer: D,
) -> std::result::Result<crate::Properties<Self>, D::Error>
where
D: serde::Deserializer<'de>,
Self: Sized;
}

pub trait ArchiveReader: Read + Seek {
Expand Down Expand Up @@ -72,6 +95,19 @@ pub trait ArchiveReader: Read + Seek {
fn error_to_raw(&self) -> bool {
false
}

/// Post-process a freshly read property, called with the property name pushed on the scope.
/// Allows archive implementations to convert game-specific embedded data (e.g. Palworld
/// RawData byte arrays) into typed values. Implementations may update `tag` to reflect the
/// converted type; the updated tag is what gets recorded in the schemas.
fn post_process_property(
&mut self,
tag: &mut PropertyTagPartial,
value: Property<Self::ArchiveType>,
) -> Result<Property<Self::ArchiveType>> {
let _ = tag;
Ok(value)
}
}

pub trait ArchiveWriter: Write + Seek {
Expand Down Expand Up @@ -114,26 +150,57 @@ pub trait ArchiveWriter: Write + Seek {
fn log(&self) -> bool {
false
}

/// Pre-process a property about to be written, called with the property name pushed on the
/// scope. Allows archive implementations to convert typed game-specific values (e.g. Palworld
/// structs) back into their embedded representation (byte arrays). Returning `Some` replaces
/// both the tag and the property value used for writing.
#[allow(clippy::type_complexity)]
fn pre_write_property(
&mut self,
key: &PropertyKey,
tag: &PropertyTagPartial,
prop: &Property<Self::ArchiveType>,
) -> Result<Option<(PropertyTagPartial, Property<Self::ArchiveType>)>> {
let _ = (key, tag, prop);
Ok(None)
}
}

/// Archive type for save games, which use string-based object references
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize)]
pub struct SaveGameArchiveType;
#[serde(bound = "")]
pub struct SaveGameArchiveType<G: crate::game::Game = crate::game::NoGame>(
std::marker::PhantomData<G>,
);

impl ArchiveType for SaveGameArchiveType {
impl<G: crate::game::Game> ArchiveType for SaveGameArchiveType<G> {
type ObjectRef = String;
type SoftObjectPath = crate::SoftObjectPath;
type Game = G;

fn is_null_object_ref(object_ref: &Self::ObjectRef) -> bool {
object_ref.is_empty() || object_ref == "None"
}

fn deserialize_properties<'de, D>(
path: &str,
schemas: &crate::PropertySchemas,
deserializer: D,
) -> std::result::Result<crate::Properties<Self>, D::Error>
where
D: serde::Deserializer<'de>,
{
crate::serialization::deserialize_properties_seed::<D, G>(path, schemas, deserializer)
}
}

impl<R> ArchiveReader for SaveGameArchive<R>
impl<R, G> ArchiveReader for SaveGameArchive<R, G>
where
R: Read + Seek,
G: Game,
{
type ArchiveType = SaveGameArchiveType;
type ArchiveType = SaveGameArchiveType<G>;

fn version(&self) -> &dyn VersionInfo {
SaveGameArchive::version(self)
Expand Down Expand Up @@ -178,12 +245,21 @@ where
fn error_to_raw(&self) -> bool {
SaveGameArchive::error_to_raw(self)
}

fn post_process_property(
&mut self,
tag: &mut PropertyTagPartial,
value: Property<SaveGameArchiveType<G>>,
) -> Result<Property<SaveGameArchiveType<G>>> {
<<Self::ArchiveType as ArchiveType>::Game>::process_property_for_read(self, tag, value)
}
}
impl<W> ArchiveWriter for SaveGameArchive<W>
impl<W, G> ArchiveWriter for SaveGameArchive<W, G>
where
W: Write + Seek,
G: Game,
{
type ArchiveType = SaveGameArchiveType;
type ArchiveType = SaveGameArchiveType<G>;

fn version(&self) -> &dyn VersionInfo {
SaveGameArchive::version(self)
Expand Down Expand Up @@ -224,4 +300,13 @@ where
fn log(&self) -> bool {
SaveGameArchive::log(self)
}

fn pre_write_property(
&mut self,
key: &PropertyKey,
tag: &PropertyTagPartial,
prop: &Property<SaveGameArchiveType<G>>,
) -> Result<Option<(PropertyTagPartial, Property<SaveGameArchiveType<G>>)>> {
<<Self::ArchiveType as ArchiveType>::Game>::process_property_for_write(self, key, tag, prop)
}
}
Loading