redguard-preservation 0.6.1

A CLI tool to preserve Redguard files
Documentation
# Native Plugin (FFI)

The library builds as a C-compatible shared library (`cdylib`) alongside the CLI binary. This lets game engines like Unity load it as a native plugin and call the conversion functions directly at runtime.

**Build:** `cargo build --release`

**Output:** `target/release/librgpre.so` (Linux), `target/release/rgpre.dll` (Windows), `target/release/librgpre.dylib` (macOS)

**C header:** [`rgpre.h`](rgpre.h) — all struct definitions and function signatures.

## Architecture

```
Unity C# (or any FFI consumer)
  |
  |  file path + scalar parameters
  v
char*  --P/Invoke--> Rust native plugin --> ByteBuffer*
                      (file I/O + parsing)      |
  <--- copy bytes <-----------------------------+
  |
  v
  rg_free_buffer(buf)
```

All asset inputs are file paths (`const char*`) plus scalar arguments (`i32`, `u16`, `u8`), or an opaque `RgWorldHandle*` returned by `rg_open_world`. No asset byte arrays cross the FFI boundary. Results are returned as `ByteBuffer*` pointers that the caller must free with `rg_free_buffer`. On error, buffer-returning functions return `NULL`, count functions return `-1`, and the message is available via `rg_last_error()`.

All structured output buffers use `#[repr(C)]` structs (defined in [`types.rs`](types.rs)) that can be directly cast via `Marshal.PtrToStructure<T>()` or `MemoryMarshal.Cast<byte, T>()` on the C# side. No manual byte parsing is needed — both sides share the same memory layout.

## Thread Safety

All functions are safe to call from any thread. Multiple threads may call FFI functions concurrently.

`rg_last_error()` is thread-local — each thread has its own error slot, so concurrent calls won't clobber each other's error state.

Each `RgWorldHandle` owns its resolved palette, world metadata, and `TextureCache` instance. Parallel calls against different world handles do not share texture decode state.

## Conventions

**Coordinate system:** Right-handed, Y-up. Matches glTF. Positions are in scaled units (original engine units ÷ 20). Scene/model conversion negates the engine X/Y axes, while RGM placement decoding negates X/Y/Z to match the current runtime/export convention. Exported faces use CCW front faces.

**Winding order:** Counter-clockwise (CCW) front faces. Standard OpenGL/glTF convention.

**UV origin:** Top-left. V increases downward. Matches glTF/DirectX convention. Model/ROB UVs are derived from raw fixed-point deltas divided by `16 × texture_dimension`, with V flipped (`1 − v`) to convert from the engine's bottom-left origin. Terrain UVs are hardcoded per-tile 0–1 coordinates with rotation variants.

**Transform matrices:** `float[16]` in column-major order (translation in elements 12–14). Matches glTF/OpenGL convention.

**String fields:** `model_name[32]`, `source_id[32]`, `segment_name[8]`, `name[32]` are null-terminated ASCII, zero-padded after the terminator.

## Memory and Errors

Every buffer-returning function allocates a `ByteBuffer` (see [`rgpre.h`](rgpre.h)). The caller must free it with `rg_free_buffer`. On failure, `NULL` is returned and the error message is available via `rg_last_error()`.

## Binary Struct Types

All structured buffers use `#[repr(C)]` layouts defined in [`types.rs`](types.rs). C# equivalents use `[StructLayout(LayoutKind.Sequential)]`. Sizes include explicit padding — no hidden compiler-inserted gaps. See [`rgpre.h`](rgpre.h) for the complete C definitions.

Reading an RGMD buffer:

```
read RgmdHeader

# Static submesh data
for i in 0..submesh_count:
    read RgmdSubmeshHeader
    read vertex_count × RgmdVertex
    read index_count × uint32_t (indices)

# Animation frame deltas (only when RgmdHeader.frame_count > 0)
for frame in 0..frame_count:
    for i in 0..submesh_count:
        read int32_t delta_vertex_count   # equals submeshes[i].vertex_count
        read delta_vertex_count × RgmdDeltaVertex
```

For animated models (`frame_count > 0`) each frame supplies per-vertex position
and face-normal deltas in the same submesh order as the static block. Add the
deltas to the static `RgmdVertex` positions/normals to obtain the displayed
geometry for that frame. Static models report `frame_count == 0` and have no
delta block. Terrain meshes use `frame_count == 0`.

Reading an RGPL buffer:

```
read RgplHeader
for i in 0..placement_count:
    read RgplPlacement
for i in 0..light_count:
    read RgplLight
```

## GLB Export

Converts 3D/3DC, ROB, RGM, and WLD files to in-memory GLB. `assets_dir` should be the game root containing `3dart/`, `fxart/`, `maps/`, and `WORLD.INI`. WLD conversion auto-discovers the companion RGM file.

## World Handle API

Use `rg_world_count` to enumerate available worlds, then `rg_open_world(assets_dir, world_id)` to create a native world context. The handle resolves the `WORLD.INI` entry, palette, scene paths, and texture cache once and keeps them together so Unity cannot accidentally mix one world's scene with another world's palette.

`rg_get_world_descriptor` returns a fixed-size `RgWorldDescriptor` struct containing the world id, whether a WLD exists, the terrain TEXBSI id when available, and the raw `WORLD.INI` paths for the RGM, WLD, and palette entries.

After opening a handle, Unity can request terrain (`rg_get_world_terrain`), placements (`rg_get_world_placements`), RGM section payloads (`rg_rgm_section_count_world`, `rg_get_rgm_section_world`), and decoded TEXBSI textures (`rg_decode_texture_world`, `rg_decode_texture_all_frames_world`) without passing palette names or re-parsing `WORLD.INI`.

Release the handle with `rg_close_world` when finished.

### Path conventions

The two world-handle constructors take paths in different forms — pick the one that matches what the caller has:

| Function | Path arguments | Resolution |
|---|---|---|
| `rg_open_world(assets_dir, world_id)` | None — paths come from `WORLD.INI` | Native resolves relative paths (e.g. `FXART\ISLAND.COL`) under `assets_dir`, case-insensitive. |
| `rg_open_world_explicit(assets_dir, rgm_path, wld_path, palette_path)` | Palette path required; `rgm_path` and `wld_path` may each be null or an empty string for palette-only contexts (e.g. ModelViewer) or worlds without terrain | **Strict for supplied paths**: each non-null/non-empty path must be an absolute path to an existing file. No extension fallback, no asset-tree walking. Returns `NULL` + `rg_last_error` on missing files. Calling `rg_get_world_placements`, `rg_rgm_section_*`, etc. on a handle without an RGM yields a clear error. |

`assets_dir` is still required for the explicit constructor because the texture cache and other lookups need the game root (`3dart/`/`fxart/`, `WORLD.INI`, ...). Only the per-world scene/palette paths are caller-resolved.

## Scene Data Functions

Return pre-transformed mesh data for direct engine consumption (RGMD binary format). Vertices are in right-handed Y-up coordinates (see Conventions above), faces are fan-triangulated with CCW winding, and geometry is grouped by submesh/material. `assets_dir` is used to resolve the palette for solid-color materials. Solid-color submeshes carry resolved RGB values; no separate palette lookup is needed on the engine side.

## Texture Functions

Texture decode in the FFI API is world-handle based. Open a world first, then decode TEXBSI images through that handle so the correct palette and cache are always used for that world.

`image_id` is the TEXBSI image identifier from model/placement data, not an array index into TEXBSI entries.

## Audio Functions

SFX files are path-based: `rg_sfx_effect_count` and `rg_convert_sfx_to_wav` take a file path on every call.

RTX files use the **handle pattern** because they contain many entries and the engine looks them up by tag at runtime. Path-based access re-parsed the file on every query, which scaled badly for files like `ENGLISH.RTX` with hundreds of entries.

```
RgRtxHandle *h = rg_open_rtx("/path/to/ENGLISH.RTX");
int32_t count = rg_rtx_handle_entry_count(h);
for (int32_t i = 0; i < count; ++i) {
    int32_t tag = rg_rtx_handle_entry_tag(h, i);      /* engine SOUPDEF lookup key */
    ByteBuffer *sub = rg_rtx_handle_get_subtitle(h, i); /* always populated      */
    ByteBuffer *wav = rg_rtx_handle_convert_entry_to_wav(h, i); /* NULL for text */
    /* ... use sub/wav, then rg_free_buffer them ... */
}
rg_close_rtx(h);
```

Entries are either text-only or audio-with-embedded-subtitle. `rg_rtx_handle_get_subtitle` returns the text for either kind as a UTF-8 byte buffer (no null terminator). `rg_rtx_handle_convert_entry_to_wav` returns `NULL` for text-only entries.

The tag is the four ASCII bytes of the entry label reinterpreted as a little-endian `int32`. This is the lookup key the original engine's SOUPDEF scripts pass to `RTX(stringId)`, so the Unity side typically stores entries keyed by tag rather than by sequential index.

## Other Functions

`rg_convert_fnt_to_ttf` converts a bitmap FNT file to a TrueType font in memory.

## Maintaining the C header

[`rgpre.h`](rgpre.h) is **hand-maintained**. It was originally seeded with `cbindgen` and has since been hand-curated for readable section dividers, byte-size comments, and a consistent `typedef` style. The project has no `build.rs`, no `cbindgen.toml`, and no CI step that regenerates it — running raw `cbindgen` today would not reproduce the current layout.

When you add or change anything in [`mod.rs`](mod.rs), [`scene.rs`](scene.rs), or [`types.rs`](types.rs), update `rgpre.h` by hand to match. Checklist:

- **New `#[unsafe(no_mangle)] pub unsafe extern "C" fn rg_*`** → add a matching prototype in the relevant `── … ──` section of `rgpre.h`.
- **New or changed `#[repr(C)]` / `#[repr(C, packed)]` struct** → mirror the field layout, update or add the `/* N bytes */` size comment, and slot it under the matching section.
- **Removed or renamed FFI symbol** → remove or rename it in `rgpre.h` too.
- **Argument added / removed / reordered on an existing function** → update the prototype.

For a quick reference dump of what `cbindgen` would expose today, run:

```bash
cargo install cbindgen
cbindgen --crate redguard-preservation --lang c > /tmp/cbindgen-reference.h
```

Use the output as a sanity check (e.g. to confirm a new struct is `pub` and `#[repr(C)]`), not as a drop-in replacement.