Files
Eric Dalloandeca-agent b672c6498f Raise the size table entries of the files inside the mods' packs, whose heaps ran out when the game loaded them
The game loads each member of a pack (Pack/**/*.pack.zs) as a resource of its own, in a heap sized by that member's
own entry in the resource size table. The port raised the entries of the mods' loose files only, so a mod that changed
a file inside a pack and shipped no table for it left the game's entry. A crash when heavy snow started in Hebra was
that: an .ainb member of about 13 KB loaded into a heap sized for the game's smaller copy, 0x48 bytes stayed free after
the file, the frame heap gave null for the 0x68-byte object the resource builds next, and main+0x21794c stored that
object's vtable at address 0.

restbl_overlay now unpacks each pack a mod serves and the game's copy of it with the dictionaries of
Pack/ZsDic.pack.zs, and raises each member's entry as it does a loose file's: by the growth for a member the game's
copy has, by the estimate of its type for one it lacks. It never lowers an entry, a mod's own table included, and the
log names the first members it raised. --probe-mods checks restbl_pack_raise on archives of its own, and docs/mods.md
says that the files inside packs count too.

🤖 Generated with [ECA](https://eca.dev) (nubank-anthropic/opus-5.5 - max)

Co-Authored-By: eca-agent <git@eca.dev>
2026-11-05 10:49:55 -03:00

134 lines
7.7 KiB
C

// The entries of the members of the mods' packs (restbl.c restbl_pack_raise), on archives and tables of its own; part
// of --probe-mods (mods_probe.c). Needs no game files.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "restbl.h"
static int fails;
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL restbl: %s\n", what); } } while (0)
// A member of a probe archive: its name and size; a .zs member is a zstd frame header recording `content` bytes:
// fcs 4, a frame with a window descriptor and a 4-byte field; 2, a single-segment frame with a 2-byte field holding
// content - 256.
typedef struct { const char *name; size_t size, content; int fcs; } Member;
static void wr16(uint8_t *p, unsigned v) { p[0] = (uint8_t)v, p[1] = (uint8_t)(v >> 8); }
static void wr32(uint8_t *p, uint32_t v) { wr16(p, v & 0xffff), wr16(p + 2, v >> 16); }
// A SARC archive of the members m[0..n); unnamed: the index of a member without a name, -1 for none. malloc'd.
static uint8_t *sarc(const Member *m, int n, int unnamed, size_t *out_n) {
size_t names = 0, data = 0;
for (int i = 0; i < n; i++) names += (strlen(m[i].name) + 4) & ~(size_t)3, data += (m[i].size + 7) & ~(size_t)7;
size_t sfat = 0x14, sfnt = sfat + 12 + 16 * (size_t)n, start = (sfnt + 8 + names + 15) & ~(size_t)15;
size_t total = start + data;
uint8_t *p = calloc(1, total);
if (!p) return NULL;
memcpy(p, "SARC", 4), wr16(p + 4, 0x14), wr16(p + 6, 0xfeff);
wr32(p + 8, (uint32_t)total), wr32(p + 12, (uint32_t)start);
memcpy(p + sfat, "SFAT", 4), wr16(p + sfat + 4, 12), wr16(p + sfat + 6, (unsigned)n), wr32(p + sfat + 8, 0x65);
memcpy(p + sfnt, "SFNT", 4), wr16(p + sfnt + 4, 8);
size_t name_at = 0, data_at = 0;
for (int i = 0; i < n; i++) {
uint8_t *node = p + sfat + 12 + 16 * (size_t)i, *d = p + start + data_at;
wr32(node + 4, i == unnamed ? 0 : 0x01000000u | (uint32_t)(name_at / 4));
wr32(node + 8, (uint32_t)data_at), wr32(node + 12, (uint32_t)(data_at + m[i].size));
memcpy(p + sfnt + 8 + name_at, m[i].name, strlen(m[i].name));
memset(d, 0x5a, m[i].size);
if (m[i].fcs == 4) wr32(d, 0xFD2FB528u), d[4] = 0x80, d[5] = 0x58, wr32(d + 6, (uint32_t)m[i].content);
if (m[i].fcs == 2) wr32(d, 0xFD2FB528u), d[4] = 0x60, wr16(d + 5, (unsigned)(m[i].content - 256));
name_at += (strlen(m[i].name) + 4) & ~(size_t)3;
data_at += (m[i].size + 7) & ~(size_t)7;
}
*out_n = total;
return p;
}
// An empty table with the entries keys[i] = sizes[i].
static Restbl *table(const char *const *keys, const uint32_t *sizes, int n) {
uint8_t head[22] = "RESTBL";
wr32(head + 6, 1), wr32(head + 10, 160), wr32(head + 14, 0), wr32(head + 18, 0);
Restbl *t = restbl_parse(head, sizeof head);
for (int i = 0; t && i < n; i++) restbl_raise(t, keys[i], sizes[i]);
return t;
}
static uint32_t get(const Restbl *t, const char *key) {
uint32_t v = 0;
return restbl_get(t, key, &v) ? v : 0;
}
static int visited;
static void count(const char *name, const uint8_t *data, size_t size, void *ctx) {
(void)name, (void)data, (void)size, (void)ctx;
visited++;
}
static int notes;
static void note(const char *key, uint32_t was, uint32_t now, void *ctx) {
(void)key, (void)ctx;
notes += now > was;
}
int restbl_probe(void) {
fails = 0;
static const char *const keys[] = {"AI/Grow.root.ainb", "AI/Same.root.ainb", "AI/Shrink.root.ainb",
"Component/Shared.bgyml", "Model/Mesh.bfres", "AI/Zs.ainb", "AI/Zs2.ainb"};
static const uint32_t sizes[] = {13512, 5000, 6000, 9000, 30000, 4400, 3500};
// The game's copy: an .ainb member a mod grows (by 288 bytes, past its entry's slack), one it keeps, one it
// shrinks, one the table does not size, a mesh codec member and two compressed ones.
static const Member orig[] = {{"AI/Grow.root.ainb", 13088, 0, 0}, {"AI/Same.root.ainb", 4600, 0, 0},
{"AI/Shrink.root.ainb", 5600, 0, 0}, {"AI/NotSized.ainb", 100, 0, 0},
{"Model/Mesh.bfres.mc", 1000, 0, 0}, {"AI/Zs.ainb.zs", 64, 4000, 4},
{"AI/Zs2.ainb.zs", 64, 3900, 2}};
// The mod's copy: those, plus members new to the pack (one the game sizes elsewhere, one it lacks, a streamed one).
static const Member mod[] = {{"AI/Grow.root.ainb", 13376, 0, 0}, {"AI/Same.root.ainb", 4600, 0, 0},
{"AI/Shrink.root.ainb", 5000, 0, 0}, {"AI/NotSized.ainb", 300, 0, 0},
{"Component/New/X.bgyml", 1000, 0, 0}, {"Component/Shared.bgyml", 2000, 0, 0},
{"Sound/Stream.bwav", 500, 0, 0}, {"Model/Mesh.bfres.mc", 1500, 0, 0},
{"AI/Zs.ainb.zs", 64, 4500, 4}, {"AI/Zs2.ainb.zs", 64, 4200, 2}};
size_t on = 0, mn = 0;
uint8_t *op = sarc(orig, 7, -1, &on), *mp = sarc(mod, 10, 4, &mn); // the mod's New/X has no name: left alone
Restbl *base = table(keys, sizes, 7), *t = table(keys, sizes, 7);
CHECK(op && mp && base && t, "probe archives and tables");
if (op && mp && base && t) {
visited = 0;
CHECK(restbl_sarc_each(mp, mn, count, NULL) && visited == 9, "SARC members: every named one");
int r = restbl_pack_raise(t, base, mp, mn, op, on, note, NULL);
CHECK(r == 5 && notes == 5, "five entries raised, each noted");
CHECK(get(t, "AI/Grow.root.ainb") == 13824, "a member that grew: its entry plus the growth, 32-byte steps");
CHECK(get(t, "AI/Same.root.ainb") == 5000 && get(t, "AI/Shrink.root.ainb") == 6000,
"members that kept or lost bytes keep their entries");
CHECK(!get(t, "AI/NotSized.ainb"), "a member the game sizes without the table gets no entry");
CHECK(!get(t, "Component/New/X.bgyml"), "a member without a name is not looked up");
CHECK(get(t, "Component/Shared.bgyml") == 16384, "a member new to the pack: the estimate of its type");
CHECK(!get(t, "Sound/Stream.bwav"), "a streamed member new to the pack gets no entry");
CHECK(get(t, "Model/Mesh.bfres") == 45000, "a mesh codec member's entry grows in proportion");
CHECK(get(t, "AI/Zs.ainb") == 4928 && get(t, "AI/Zs2.ainb") == 3808,
"a compressed member: the growth of the size its frame records (4- and 2-byte fields)");
notes = 0;
CHECK(restbl_pack_raise(t, base, mp, mn, op, on, note, NULL) == 0 && !notes, "a second pass raises nothing");
Restbl *shipped = table(keys, (const uint32_t[]){20000, 5000, 6000, 9000, 30000, 4400, 3500}, 7);
CHECK(shipped && restbl_pack_raise(shipped, base, mp, mn, op, on, NULL, NULL) == 4 &&
get(shipped, "AI/Grow.root.ainb") == 20000,
"an entry a mod's own table raised further stays");
restbl_free(shipped);
Restbl *added = table(keys, sizes, 7);
CHECK(added && restbl_pack_raise(added, base, mp, mn, NULL, 0, NULL, NULL) == 7 &&
get(added, "AI/Grow.root.ainb") == 24576 && get(added, "AI/NotSized.ainb") == 12288 &&
get(added, "AI/Zs2.ainb") == 16384 && get(added, "Model/Mesh.bfres") == 30000,
"a pack the game lacks: every member gets the estimate of its type, an entry above it stays");
restbl_free(added);
CHECK(restbl_pack_raise(t, base, (const uint8_t *)"SARC", 4, NULL, 0, NULL, NULL) == -1 &&
restbl_pack_raise(t, base, mp, mn, mp, 0x20, NULL, NULL) == -1,
"an archive that is not one, or whose game copy is cut short, raises nothing");
}
free(op);
free(mp);
restbl_free(base);
restbl_free(t);
printf("restbl: %s\n", fails ? "FAIL" : "ok");
return fails;
}