FOnline Engine
Current master GitHub
Documentation Docs/en/reference/resource-pack-format.md

Resource Pack Format

A logical resource pack selects one full Pack.fores and, optionally, one writable Pack.patch.fores. The patch carries new payloads and a complete current catalog. Each entry selects bytes from the base or patch; deleted paths disappear from that catalog. There are no patch chains, resource-set files or parts. Resources.foindex is a disposable merged lookup cache over the selected pairs.

See ClientUpdater.md for synchronization, ConfigurationAndDataSources.md for mounting, and Essentials.md for filesystem primitives.

Encoding and identities

All integer fields are explicitly sized little-endian values. Formats require exactly version 2.0; there are no readers for previous versions. Offsets and lengths are 64-bit. Catalog string-pool offsets, path lengths and entry counts are 32-bit; decoded catalogs cannot exceed UINT32_MAX bytes.

Paths are unique, sorted by UTF-8 bytes and relative. Components cannot be empty, . or ..; backslashes, colons and NUL are rejected. Writers normalize input backslashes to / before validation. Payloads may share an extent, including when a rename reuses existing content.

All hashes use FNV-1a 64, seed 0xcbf29ce484222325, prime 0x100000001b3:

  • PackHash: physical bytes after the full base’s header, including its encoded catalog.
  • FileContentHash: decoded bytes of one resource.
  • ContentHash: uint32 entry_count, then, for each sorted entry, uint32 path_byte_length, uint64 decoded_size, uint64 FileContentHash, and the UTF-8 path bytes.
  • Header, catalog and footer checksums cover the byte ranges described below.

ContentHash excludes codec, source and offsets. Recompression changes physical identity without changing logical identity. A patch binds to physical BasePackHash, because its base offsets must match that exact artifact. These are integrity/change-detection hashes, not authentication.

Full base

[80-byte header][payload blobs][complete encoded catalog]

The header is uncompressed:

Offset Bytes Field
0 4 Magic 0x53524F46 (FORS)
4 2 Major = 2
6 2 Minor = 0
8 8 PackHash, FNV over [80, EOF)
16 8 IndexOffset
24 8 IndexStoredSize
32 8 IndexDecodedSize
40 4 IndexCodec
44 4 EntryCount
48 8 DataOffset = 80
56 8 DataSize
64 8 ContentHash
72 8 Header checksum, FNV over [0, 72)

IndexOffset = DataOffset + DataSize, and the index ends exactly at EOF. There is no padding or reserved space. Packaging streams encoded blobs and the physical hash, appends the catalog, then writes the header. Canonical packaging also adds payloads in sorted path order. No timestamps are serialized.

Catalog entries

A decoded catalog is Entry[EntryCount], 48 bytes each, followed by its UTF-8 string pool without terminators.

Entry offset Bytes Field
0 4 Path offset into the decoded catalog
4 4 Path byte length
8 8 Encoded payload offset in the selected file
16 8 Stored size
24 8 Decoded size
32 4 Codec: 0 = Stored, 1 = Deflate
36 4 Source: 0 = Base, 1 = Patch; other values rejected
40 8 FileContentHash

Full-base entries require Source 0. A patch catalog can use either source. Base references fit the base payload region; patch references start after its 32-byte header and end before the selected catalog. References cannot reach the current catalog/footer or an uncommitted suffix. Stored resources must declare equal stored/decoded sizes. Reads verify the exact decoded size and FileContentHash.

ResourcePackSource owns the current entry list and path strings and builds a lookup map whose keys reference those strings. Decoded catalog bytes are released after parsing. File enumeration follows sorted paths. All entries report the effective patch’s modification time when a committed patch is selected, including entries backed by base bytes. Without a patch they report the base time.

Append-only patch

[32-byte patch header]
[payloads 1][complete catalog 1][80-byte footer 1]
[payloads 2][complete catalog 2][80-byte footer 2]
...

The fixed header is never rewritten to publish an update:

Offset Bytes Field
0 4 Magic 0x50524F46 (FORP)
4 2 Major = 2
6 2 Minor = 0
8 8 BasePackHash
16 8 Reserved = 0
24 8 Header checksum, FNV over [0, 24)

Every commit ends with this uncompressed footer:

Offset Bytes Field
0 4 Magic 0x54524F46 (FORT)
4 2 Major = 2
6 2 Minor = 0
8 8 BasePackHash
16 8 Current ContentHash
24 8 Catalog offset
32 8 Catalog stored size
40 8 Catalog decoded size
48 8 Committed file size, including this footer
56 4 Catalog codec
60 4 Entry count
64 8 FNV of the encoded catalog
72 8 Footer checksum, FNV over [0, 72)

The catalog must immediately precede its footer. Old payloads, catalogs and footers remain unused bytes unless the current catalog references an old payload. Only the newest complete catalog is applied; earlier catalogs are never layered beneath it.

ResourcePatchWriter plans offsets, reuses matching (FileContentHash, DecodedSize) from the base/current patch, and encodes the next full catalog before writing. It validates each received resource, appends the catalog, flushes, appends the footer, and flushes again. It persists the directory entry on POSIX platforms. The writable directory is locked during mutation, through an OS lock rather than a stored selector or journal: Begin takes the lock its caller holds, so one lock can cover a longer session around the append. File writers also exclude other writers before truncation or append.

Readers retain their catalog and captured file bounds while a writer appends. An uncommitted suffix is not thrown away blindly: a plan lays its downloads out in a fixed order from the committed end, so the planner reads the suffix payload by payload and keeps each one that decodes to the entry planned at that offset (GetResumedDownloads); Begin truncates after the last kept payload and AddEncodedFile continues with the next download. The first payload that does not match - another plan’s, torn, garbage - ends the kept run. A patch with no valid commit is resumed the same way when its own header binds it to the selected base (a first append cut short); anything else is removed and recreated under the directory lock. The updater removes a stale patch whose base binding differs; the writer also recreates such a patch if a new append is needed. A patch is never truncated underneath readers of a different base generation. On POSIX, readers can retain an unlinked inode; Windows rejects deletion/replacement while incompatible readers still hold the file.

Recovery and validation

Healthy reads inspect the header, footer at EOF, and that footer’s catalog. An invalid EOF triggers a backward scan in 64 KiB windows with footer-sized overlap. Candidate magic alone is insufficient: validate version, base binding, absolute committed length, footer checksum, catalog extent/checksum and computed logical hash. The latest valid candidate wins. No valid commit means the full base remains the local view. A patch header that fails its own checks commits nothing either: a power loss can leave a created patch with its length but not its header bytes, and refusing the file would keep the client from starting while a reinstall never reaches the writable root. The base stays the view and the next update recreates the patch; Begin flushes a new header before any payload so the window stays small. A valid header bound to a different base is stale and excluded.

All extent checks subtract only after checking the minuend and widen table multiplication before use. Unknown codecs/sources, noncanonical or duplicate paths and invalid pool references are rejected. Mounting does not hash all payloads. Payload corruption is reported when read; received patch blobs are verified before publication. Full downloads also verify the physical body hash and complete catalog before promotion. ResourcePairVerifier proves a whole local pair on request, in bounded Steps: the base against its header PackHash over [80, EOF), then every committed patch extent (Source = 1, each extent once) by decoding it against its FileContentHash. A base with no readable header, or one that no longer mounts, counts as damaged; a damaged base is not followed into its patch, since replacing the base drops the patch with it. The updater runs it once per file identity, see ClientUpdater.md. The Python packager validates every decoded payload before accepting an archive, including exact stored/ decoded lengths, complete Deflate streams without trailing bytes, and each FileContentHash. It hashes and decodes payloads in bounded chunks rather than allocating their declared decoded sizes.

An interrupted append resumes from the payloads it already wrote, as above, and previously committed bytes remain reusable; there is no separate resume journal. The format has no patch-size limit of its own: dead blobs and old catalogs accumulate until the updater’s cap replaces the pair with the full pack. See the updater doc for that cap and for full-base installation/repair ordering when local data is missing or unusable.

Codecs

Codec 0 stores bytes unchanged. Codec 1 is a zlib Deflate stream. Packaging takes the level from Baking.ResourcePackCompressLevel and the gain from Baking.ResourcePackMinCompressGain (default 5): a blob is deflated only when the bytes it saves exceed size * gain / 100 in integer division. Blobs smaller than 64 bytes remain stored. Catalogs use the same encoding rule. Catalogs and caches the client writes itself (patch catalogs, .foindex) use the ResourcePackWriteSettings defaults, level 6 and gain 5. Matching decoded content can reuse an existing extent even when the server encoded it differently.

Merged cache: .foindex

The cache contains references only. It records effective pairs from the configured suffix after the last Embedded entry. Earlier packs and Embedded stay at their configured positions. It never mounts a patch as an independent overriding pack.

Cache mounting verifies base headers and patch commit identities through the retained read handles. A replacement between path resolution and opening invalidates the cache rather than mixing old offsets with new backing bytes. Mounted readers retain their captured view across later appends and POSIX replacements.

The 72-byte header contains magic 0x58494F46 (FOIX), version 2.0, PackListHash at 8, index offset/stored/ decoded sizes at 16/24/32, codec at 40, entry count at 44, pack count at 48, and checksum over [0,64) at 64. Decoded contents are 32-byte pack records, 56-byte entry records, then the string pool.

A pack record stores name offset/length (4 bytes each), base PackHash (8), committed patch catalog checksum (8), and patch committed size (8). The last two fields are zero without a committed patch. PackListHash folds each name’s bytes followed by these three 64-bit values, in configured order.

An entry stores path offset/length, pack index and codec (four 4-byte fields), data offset, stored size, decoded size and content hash (four 8-byte fields), source (4), and reserved zero (4).

Later configured packs win duplicate paths. The disk table is path-sorted; runtime enumeration groups winners by descending pack precedence, then path, matching direct mounts. Entry times come from the effective pair. Freshness includes base identity and patch commit identity, so a full refresh cannot keep stale offsets merely because logical content is unchanged. Invalid cache data is discarded at the client boundary and authoritative pairs are mounted. Rebuild writes a temporary cache and replaces the old one.

Platforms and API

Android packages .fores without outer ZIP compression. The activity passes <installed APK>!/assets/<configured client resource directory> as Baking.ClientResources, and its private files directory as Common.UserWritablePath. OpenResourcePackFile locates the stored APK ZIP entry and returns a bounded, 64-bit positional fs::disk_read_file region over the APK. Compressed or encrypted outer entries are rejected. !/ separates an archive only where the text before it names an existing file, so an ordinary directory whose name ends in ! (a profile named Bob!) is never mistaken for one. No complete pack or resource tree is copied into memory or staged into app storage for mounting. Writable replacement bases and patches live under the private Resources directory.

Web retains the preloaded in-memory filesystem and can update pairs within that session. It does not persist updates across page reloads and does not build a merged cache.

The main API is in Source/Common/ResourcePack.h: ResourcePackWriter, ResourcePatchWriter, ResourcePackSource, ResourcePairVerifier, ReadResourcePackHeader, DecodeResourcePackIndex, ReadResourcePatchInfo, and VerifyResourcePackFile. GetClientPackDirs, GetClientResourcePackPath and AddClientPackSource in FileSystem.h give bootstrap, updater and runtime the same base/patch selection.

Test_ResourcePack.cpp pins the shared Python/C++ golden bytes, payload validation, repeated append, interrupted-footer recovery, resumed appends and pair verification. Test_ResourceIndex.cpp covers pair-backed cached reads and APK region bounds; Test_ClientServerIntegration.cpp exercises the real updater/backend lifecycle.

Start typing to search.