Extending

Adding an asset class

Anything whose payload lives in the native tail can get a serializer. Implement NativeSerializer in a new src/native/<class>.rs:

pub struct MyAssetSer;

impl NativeSerializer for MyAssetSer {
    fn class_name(&self) -> &'static str { "MyAsset" }

    fn read(&self, ctx: &NativeReadCtx) -> Result<NativeRead> {
        // ctx.blob   — the native tail (everything after the "None" property)
        // ctx.props  — the already-decoded tagged properties
        // ctx.ver    — package version, for version gates
        // ctx.db     — schema database, if --game-root was given
        // ctx.self_ref / ctx.class_ref — this export, and its class
        let payload = /* parse ctx.blob */;
        Ok(NativeRead::just(NativePayload::MyAsset(payload)))
    }

    fn emit_external(&self, payload: &NativePayload, dir: &Path, stem: &str)
        -> Result<Vec<PathBuf>>
    {
        // write <stem>.<ext> into dir, return the paths — they become @sidecar lines
    }

    fn inject_external(&self, ctx: &mut NativeInjectCtx) -> Result<bool> {
        // find your extension in ctx.sidecars, read it from ctx.sidecar_dir,
        // and rewrite ctx.native_tail (or ctx.props, for property-backed payloads)
        // return true if you changed anything
    }
}

Then:

  1. Add a variant to NativePayload and a label in variant_label().
  2. Register it in NativeRegistry::standard().
  3. Add a rendering arm in pseudo::render_native so the @native(...) block prints something useful.

Registration is by class name, matched through the chain

A serializer registered for a base class automatically covers subclasses, because the registry walks the class chain before falling back to the export's literal class name. An alias for a second class name can be inserted into the registry map directly — that's how GFxMovieInfo reuses the SwfMovie implementation.

If the payload is a property, not a tail

SwfMovie is the model: read() pulls the bytes out of ctx.props and reports the property name in consumed_props, which makes the emitter print RawData = @sidecar instead of a giant byte array. inject_external() then writes back into ctx.props rather than ctx.native_tail.

Supporting another engine version

All version knowledge lives in versions.rs as VER_* constants compared against p_ver. To support a build whose layout differs:

  1. Add a named constant for the version the change landed in.
  2. Gate the field where it's read, and mirror the gate in the corresponding write path — headers and properties both have one.

Serialization order matters more than the constant's value; when in doubt, compare against the UE3 source for the two versions that bracket the target.

Cooked-for-console layouts are not exercised

SchemaParseCtx carries a cooked_for_console flag and the meta-class deserializers honour it by omitting editor-only fields, but it is currently always constructed as false. Console packages will need that wired to a real detection before their script objects parse correctly.

Adding a compression codec

utils/decompress.rs handles the chunk framing generically — only the block-level call is codec-specific. Add an arm to decompress_chunk for CompressionMethod::Zlib or Lzx; the surrounding two-level chunk walk, byte-swap detection and size bookkeeping already work.

Adding a metatype

If a UE3 reflection class isn't handled, parse_export_schema returns None for it and the database stores an OpaqueChild — the children walk steps over it but its contents are invisible. To support it:

  1. Write a reader following the UField / UStruct header conventions in Schema System.
  2. Add a SchemaEntry variant and wire it into as_struct_header() and next() so the graph walks still work.
  3. Add a dispatch arm keyed on the class name.
  4. Add a summarize_entry arm so schema-dump prints it.

Code conventions

  • All binary I/O goes through byteorder with an explicit endianness.
  • Errors are std::io::Result enriched with context strings — no error framework in the hot path.
  • Anything version-dependent is a named constant, never a bare number.
  • Count-prefixed reads validate the count against remaining bytes before allocating; keep that habit, it's the main defence against a mis-parse allocating gigabytes.
  • Decoders that can't verify their result should degrade to Raw rather than guess. Round-trip safety beats prettier output.