mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-24 00:23:39 +01:00
feat(quests): implement all remaining binary sections in JSON format
Implement the 8 pointer-addressed sections that were previously written as null pointers in the quest JSON compiler and parser: - questAreaPtr → MapSections (ptMapSection pointer array + minion spawns) - areaTransitionsPtr → AreaTransitions (per-zone floatSet arrays, 52B each) - areaMappingPtr → AreaMappings (32-byte coordinate mapping entries) - mapInfoPtr → MapInfo (qMapID + returnBC_ID, 8 bytes) - gatheringPointsPtr → GatheringPoints (per-zone 24-byte gatheringPoint entries) - areaFacilitiesPtr → AreaFacilities (per-zone facPoint blocks with sentinel) - someStringsPtr → SomeString/QuestTypeString (two u32 ptrs + Shift-JIS data) - gatheringTablesPtr → GatheringTables (pointer array → GatherItem[] lists) Also set gatheringTablesQty and area1Zones in generalQuestProperties from JSON data, and validate zone-length consistency between AreaTransitions, GatheringPoints, and AreaFacilities arrays. Round-trip tests cover all new sections to ensure compile(parse(bin)) == bin.
This commit is contained in:
@@ -520,6 +520,246 @@ func TestRoundTrip_EmptyQuest(t *testing.T) {
|
||||
roundTrip(t, "empty quest", string(b))
|
||||
}
|
||||
|
||||
// ── New section round-trip tests ─────────────────────────────────────────────
|
||||
|
||||
func TestRoundTrip_MapSections(t *testing.T) {
|
||||
var q QuestJSON
|
||||
_ = json.Unmarshal([]byte(minimalQuestJSON), &q)
|
||||
q.MapSections = []QuestMapSectionJSON{
|
||||
{
|
||||
LoadedStage: 5,
|
||||
SpawnMonsters: []uint8{0x0F, 0x33}, // Khezu, Blangonga
|
||||
MinionSpawns: []QuestMinionSpawnJSON{
|
||||
{Monster: 0x0F, SpawnToggle: 1, SpawnAmount: 3, X: 100.0, Y: 0.0, Z: -200.0},
|
||||
{Monster: 0x33, SpawnToggle: 1, SpawnAmount: 2, X: 250.0, Y: 5.0, Z: 300.0},
|
||||
},
|
||||
},
|
||||
}
|
||||
b, _ := json.Marshal(q)
|
||||
roundTrip(t, "map sections", string(b))
|
||||
}
|
||||
|
||||
func TestRoundTrip_MapSectionsMultiple(t *testing.T) {
|
||||
var q QuestJSON
|
||||
_ = json.Unmarshal([]byte(minimalQuestJSON), &q)
|
||||
q.MapSections = []QuestMapSectionJSON{
|
||||
{
|
||||
LoadedStage: 2,
|
||||
SpawnMonsters: []uint8{0x06},
|
||||
MinionSpawns: []QuestMinionSpawnJSON{
|
||||
{Monster: 0x06, SpawnToggle: 1, SpawnAmount: 4, X: 50.0, Y: 0.0, Z: 50.0},
|
||||
},
|
||||
},
|
||||
{
|
||||
LoadedStage: 3,
|
||||
SpawnMonsters: nil,
|
||||
MinionSpawns: nil,
|
||||
},
|
||||
}
|
||||
b, _ := json.Marshal(q)
|
||||
roundTrip(t, "map sections multiple", string(b))
|
||||
}
|
||||
|
||||
func TestRoundTrip_AreaTransitions(t *testing.T) {
|
||||
var q QuestJSON
|
||||
_ = json.Unmarshal([]byte(minimalQuestJSON), &q)
|
||||
q.AreaTransitions = []QuestAreaTransitionsJSON{
|
||||
{
|
||||
Transitions: []QuestAreaTransitionJSON{
|
||||
{
|
||||
TargetStageID1: 3,
|
||||
StageVariant: 0,
|
||||
CurrentX: 100.0,
|
||||
CurrentY: 0.0,
|
||||
CurrentZ: 50.0,
|
||||
TransitionBox: [5]float32{10.0, 5.0, 10.0, 0.0, 0.0},
|
||||
TargetX: -100.0,
|
||||
TargetY: 0.0,
|
||||
TargetZ: -50.0,
|
||||
TargetRotation: [2]int16{90, 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Zone 2: no transitions (null pointer).
|
||||
Transitions: nil,
|
||||
},
|
||||
}
|
||||
b, _ := json.Marshal(q)
|
||||
roundTrip(t, "area transitions", string(b))
|
||||
}
|
||||
|
||||
func TestRoundTrip_AreaMappings(t *testing.T) {
|
||||
var q QuestJSON
|
||||
_ = json.Unmarshal([]byte(minimalQuestJSON), &q)
|
||||
// AreaMappings without AreaTransitions: the parser reads until areaTransitionsPtr,
|
||||
// which will be null, so it reads until end of file's mapping section. To make
|
||||
// this round-trip cleanly, add both together.
|
||||
q.AreaTransitions = []QuestAreaTransitionsJSON{{}, {}}
|
||||
q.AreaMappings = []QuestAreaMappingJSON{
|
||||
{AreaX: 100.0, AreaZ: 200.0, BaseX: 10.0, BaseZ: 20.0, KnPos: 5.0},
|
||||
{AreaX: 300.0, AreaZ: 400.0, BaseX: 30.0, BaseZ: 40.0, KnPos: 7.5},
|
||||
}
|
||||
b, _ := json.Marshal(q)
|
||||
roundTrip(t, "area mappings", string(b))
|
||||
}
|
||||
|
||||
func TestRoundTrip_MapInfo(t *testing.T) {
|
||||
var q QuestJSON
|
||||
_ = json.Unmarshal([]byte(minimalQuestJSON), &q)
|
||||
q.MapInfo = &QuestMapInfoJSON{
|
||||
MapID: 2,
|
||||
ReturnBCID: 1,
|
||||
}
|
||||
b, _ := json.Marshal(q)
|
||||
roundTrip(t, "map info", string(b))
|
||||
}
|
||||
|
||||
func TestRoundTrip_GatheringPoints(t *testing.T) {
|
||||
var q QuestJSON
|
||||
_ = json.Unmarshal([]byte(minimalQuestJSON), &q)
|
||||
q.AreaTransitions = []QuestAreaTransitionsJSON{{}, {}}
|
||||
q.GatheringPoints = []QuestAreaGatheringJSON{
|
||||
{
|
||||
Points: []QuestGatheringPointJSON{
|
||||
{X: 50.0, Y: 0.0, Z: 100.0, Range: 3.0, GatheringID: 5, MaxCount: 3, MinCount: 1},
|
||||
{X: 150.0, Y: 0.0, Z: 200.0, Range: 3.0, GatheringID: 6, MaxCount: 2, MinCount: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Zone 2: no gathering points (null pointer).
|
||||
Points: nil,
|
||||
},
|
||||
}
|
||||
b, _ := json.Marshal(q)
|
||||
roundTrip(t, "gathering points", string(b))
|
||||
}
|
||||
|
||||
func TestRoundTrip_AreaFacilities(t *testing.T) {
|
||||
var q QuestJSON
|
||||
_ = json.Unmarshal([]byte(minimalQuestJSON), &q)
|
||||
q.AreaTransitions = []QuestAreaTransitionsJSON{{}, {}}
|
||||
q.AreaFacilities = []QuestAreaFacilitiesJSON{
|
||||
{
|
||||
Points: []QuestFacilityPointJSON{
|
||||
{Type: 1, X: 10.0, Y: 0.0, Z: -5.0, Range: 2.0, ID: 1}, // cooking
|
||||
{Type: 7, X: 20.0, Y: 0.0, Z: -10.0, Range: 3.0, ID: 2}, // red box
|
||||
},
|
||||
},
|
||||
{
|
||||
// Zone 2: no facilities (null pointer).
|
||||
Points: nil,
|
||||
},
|
||||
}
|
||||
b, _ := json.Marshal(q)
|
||||
roundTrip(t, "area facilities", string(b))
|
||||
}
|
||||
|
||||
func TestRoundTrip_SomeStrings(t *testing.T) {
|
||||
var q QuestJSON
|
||||
_ = json.Unmarshal([]byte(minimalQuestJSON), &q)
|
||||
q.SomeString = "extra info"
|
||||
q.QuestType = "standard"
|
||||
b, _ := json.Marshal(q)
|
||||
roundTrip(t, "some strings", string(b))
|
||||
}
|
||||
|
||||
func TestRoundTrip_SomeStringOnly(t *testing.T) {
|
||||
var q QuestJSON
|
||||
_ = json.Unmarshal([]byte(minimalQuestJSON), &q)
|
||||
q.SomeString = "only this string"
|
||||
b, _ := json.Marshal(q)
|
||||
roundTrip(t, "some string only", string(b))
|
||||
}
|
||||
|
||||
func TestRoundTrip_GatheringTables(t *testing.T) {
|
||||
var q QuestJSON
|
||||
_ = json.Unmarshal([]byte(minimalQuestJSON), &q)
|
||||
q.GatheringTables = []QuestGatheringTableJSON{
|
||||
{
|
||||
Items: []QuestGatherItemJSON{
|
||||
{Rate: 50, Item: 100},
|
||||
{Rate: 30, Item: 101},
|
||||
{Rate: 20, Item: 102},
|
||||
},
|
||||
},
|
||||
{
|
||||
Items: []QuestGatherItemJSON{
|
||||
{Rate: 100, Item: 200},
|
||||
},
|
||||
},
|
||||
}
|
||||
b, _ := json.Marshal(q)
|
||||
roundTrip(t, "gathering tables", string(b))
|
||||
}
|
||||
|
||||
func TestRoundTrip_AllSections(t *testing.T) {
|
||||
var q QuestJSON
|
||||
_ = json.Unmarshal([]byte(minimalQuestJSON), &q)
|
||||
|
||||
q.MapSections = []QuestMapSectionJSON{
|
||||
{
|
||||
LoadedStage: 5,
|
||||
SpawnMonsters: []uint8{0x0F},
|
||||
MinionSpawns: []QuestMinionSpawnJSON{
|
||||
{Monster: 0x0F, SpawnToggle: 1, SpawnAmount: 2, X: 100.0, Y: 0.0, Z: -100.0},
|
||||
},
|
||||
},
|
||||
}
|
||||
q.AreaTransitions = []QuestAreaTransitionsJSON{
|
||||
{
|
||||
Transitions: []QuestAreaTransitionJSON{
|
||||
{
|
||||
TargetStageID1: 2,
|
||||
StageVariant: 0,
|
||||
CurrentX: 50.0,
|
||||
CurrentY: 0.0,
|
||||
CurrentZ: 25.0,
|
||||
TransitionBox: [5]float32{5.0, 5.0, 5.0, 0.0, 0.0},
|
||||
TargetX: -50.0,
|
||||
TargetY: 0.0,
|
||||
TargetZ: -25.0,
|
||||
TargetRotation: [2]int16{180, 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
{Transitions: nil},
|
||||
}
|
||||
q.AreaMappings = []QuestAreaMappingJSON{
|
||||
{AreaX: 100.0, AreaZ: 200.0, BaseX: 10.0, BaseZ: 20.0, KnPos: 1.0},
|
||||
}
|
||||
q.MapInfo = &QuestMapInfoJSON{MapID: 2, ReturnBCID: 0}
|
||||
q.GatheringPoints = []QuestAreaGatheringJSON{
|
||||
{
|
||||
Points: []QuestGatheringPointJSON{
|
||||
{X: 75.0, Y: 0.0, Z: 150.0, Range: 2.5, GatheringID: 3, MaxCount: 3, MinCount: 1},
|
||||
},
|
||||
},
|
||||
{Points: nil},
|
||||
}
|
||||
q.AreaFacilities = []QuestAreaFacilitiesJSON{
|
||||
{
|
||||
Points: []QuestFacilityPointJSON{
|
||||
{Type: 3, X: 5.0, Y: 0.0, Z: -5.0, Range: 2.0, ID: 10},
|
||||
},
|
||||
},
|
||||
{Points: nil},
|
||||
}
|
||||
q.SomeString = "test string"
|
||||
q.QuestType = "hunt"
|
||||
q.GatheringTables = []QuestGatheringTableJSON{
|
||||
{
|
||||
Items: []QuestGatherItemJSON{
|
||||
{Rate: 60, Item: 300},
|
||||
{Rate: 40, Item: 301},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(q)
|
||||
roundTrip(t, "all sections", string(b))
|
||||
}
|
||||
|
||||
// ── Golden file test ─────────────────────────────────────────────────────────
|
||||
//
|
||||
// This test manually constructs expected binary bytes at specific offsets and
|
||||
@@ -527,11 +767,11 @@ func TestRoundTrip_EmptyQuest(t *testing.T) {
|
||||
// Hard-coded values are derived from the documented binary layout.
|
||||
//
|
||||
// Layout constants for minimalQuestJSON:
|
||||
// headerSize = 68 (0x44)
|
||||
// genPropSize = 66 (0x42)
|
||||
// mainPropOffset = 0x86 (= headerSize + genPropSize)
|
||||
// questStringsPtr = 0x1C6 (= mainPropOffset + 320)
|
||||
|
||||
//
|
||||
// headerSize = 68 (0x44)
|
||||
// genPropSize = 66 (0x42)
|
||||
// mainPropOffset = 0x86 (= headerSize + genPropSize)
|
||||
// questStringsPtr = 0x1C6 (= mainPropOffset + 320)
|
||||
func TestGolden_MinimalQuestBinaryLayout(t *testing.T) {
|
||||
data, err := CompileQuestJSON([]byte(minimalQuestJSON))
|
||||
if err != nil {
|
||||
@@ -545,9 +785,6 @@ func TestGolden_MinimalQuestBinaryLayout(t *testing.T) {
|
||||
|
||||
// ── Header (0x00–0x43) ───────────────────────────────────────────────
|
||||
assertU32(t, data, 0x00, mainPropOffset, "questTypeFlagsPtr")
|
||||
// loadedStagesPtr, supplyBoxPtr, rewardPtr, largeMonsterPtr are computed
|
||||
// offsets we don't hard-code here — they are verified by the round-trip
|
||||
// tests and the structural checks below.
|
||||
assertU16(t, data, 0x10, 0, "subSupplyBoxPtr (unused)")
|
||||
assertByte(t, data, 0x12, 0, "hidden")
|
||||
assertByte(t, data, 0x13, 0, "subSupplyBoxLen")
|
||||
@@ -562,8 +799,6 @@ func TestGolden_MinimalQuestBinaryLayout(t *testing.T) {
|
||||
assertU32(t, data, 0x3C, 0, "fixedCoords2Ptr (null)")
|
||||
assertU32(t, data, 0x40, 0, "fixedInfoPtr (null)")
|
||||
|
||||
// loadedStagesPtr and unk34Ptr must be equal (no stages would mean stagesPtr
|
||||
// points past itself — but we have 1 stage, so unk34 = loadedStagesPtr+16).
|
||||
loadedStagesPtr := binary.LittleEndian.Uint32(data[0x04:])
|
||||
unk34Ptr := binary.LittleEndian.Uint32(data[0x34:])
|
||||
if unk34Ptr != loadedStagesPtr+16 {
|
||||
@@ -620,14 +855,12 @@ func TestGolden_MinimalQuestBinaryLayout(t *testing.T) {
|
||||
assertU16(t, data, mp+0x2E, 1, "mp.questID")
|
||||
|
||||
// Objective[0]: hunt, target=11, count=1
|
||||
// goalType=0x00000001, u8(target)=0x0B, u8(pad)=0x00, u16(count)=0x0001
|
||||
assertU32(t, data, mp+0x30, questObjHunt, "obj[0].goalType")
|
||||
assertByte(t, data, mp+0x34, 11, "obj[0].target")
|
||||
assertByte(t, data, mp+0x35, 0, "obj[0].pad")
|
||||
assertU16(t, data, mp+0x36, 1, "obj[0].count")
|
||||
|
||||
// Objective[1]: deliver, target=149, count=3
|
||||
// goalType=0x00000002, u16(target)=0x0095, u16(count)=0x0003
|
||||
assertU32(t, data, mp+0x38, questObjDeliver, "obj[1].goalType")
|
||||
assertU16(t, data, mp+0x3C, 149, "obj[1].target")
|
||||
assertU16(t, data, mp+0x3E, 3, "obj[1].count")
|
||||
@@ -641,7 +874,7 @@ func TestGolden_MinimalQuestBinaryLayout(t *testing.T) {
|
||||
assertU16(t, data, mp+0x50, 0, "mp.postRankMin")
|
||||
assertU16(t, data, mp+0x52, 0, "mp.postRankMax")
|
||||
|
||||
// forced equip: 6 slots × 4 × 2 = 48 bytes, all zero (no ForcedEquipment in minimalQuestJSON)
|
||||
// forced equip: 6 slots × 4 × 2 = 48 bytes, all zero
|
||||
for i := 0; i < 48; i++ {
|
||||
assertByte(t, data, mp+0x5C+i, 0, "forced equip zero")
|
||||
}
|
||||
@@ -652,8 +885,6 @@ func TestGolden_MinimalQuestBinaryLayout(t *testing.T) {
|
||||
assertByte(t, data, mp+0x9A, 0, "mp.questVariant4")
|
||||
|
||||
// ── QuestText pointer table (0x1C6–0x1E5) ───────────────────────────
|
||||
// 8 pointers, each u32 pointing at a null-terminated Shift-JIS string.
|
||||
// All string pointers must be within the file and pointing at valid data.
|
||||
for i := 0; i < 8; i++ {
|
||||
off := int(questStringsPtr) + i*4
|
||||
strPtr := int(binary.LittleEndian.Uint32(data[off:]))
|
||||
@@ -662,7 +893,7 @@ func TestGolden_MinimalQuestBinaryLayout(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Title pointer → "Test Quest" (ASCII = valid Shift-JIS)
|
||||
// Title pointer → "Test Quest"
|
||||
titlePtr := int(binary.LittleEndian.Uint32(data[int(questStringsPtr):]))
|
||||
end := titlePtr
|
||||
for end < len(data) && data[end] != 0 {
|
||||
@@ -674,7 +905,6 @@ func TestGolden_MinimalQuestBinaryLayout(t *testing.T) {
|
||||
|
||||
// ── Stage entry (1 stage: stageID=2) ────────────────────────────────
|
||||
assertU32(t, data, int(loadedStagesPtr), 2, "stage[0].stageID")
|
||||
// padding 12 bytes after stageID must be zero
|
||||
for i := 1; i < 16; i++ {
|
||||
assertByte(t, data, int(loadedStagesPtr)+i, 0, "stage padding")
|
||||
}
|
||||
@@ -683,55 +913,43 @@ func TestGolden_MinimalQuestBinaryLayout(t *testing.T) {
|
||||
supplyBoxPtr := int(binary.LittleEndian.Uint32(data[0x08:]))
|
||||
assertU16(t, data, supplyBoxPtr, 1, "supply_main[0].item")
|
||||
assertU16(t, data, supplyBoxPtr+2, 5, "supply_main[0].quantity")
|
||||
// Remaining 23 main slots must be zero.
|
||||
for i := 1; i < 24; i++ {
|
||||
assertU32(t, data, supplyBoxPtr+i*4, 0, "supply_main slot empty")
|
||||
}
|
||||
// All 8 subA slots zero.
|
||||
subABase := supplyBoxPtr + 24*4
|
||||
for i := 0; i < 8; i++ {
|
||||
assertU32(t, data, subABase+i*4, 0, "supply_subA slot empty")
|
||||
}
|
||||
// All 8 subB slots zero.
|
||||
subBBase := subABase + 8*4
|
||||
for i := 0; i < 8; i++ {
|
||||
assertU32(t, data, subBBase+i*4, 0, "supply_subB slot empty")
|
||||
}
|
||||
|
||||
// ── Reward table ────────────────────────────────────────────────────
|
||||
// 1 table, so: header[0] = {tableID=1, pad, pad, tableOffset=10}
|
||||
// followed by 0xFFFF terminator, then item list.
|
||||
rewardPtr := int(binary.LittleEndian.Uint32(data[0x0C:]))
|
||||
assertByte(t, data, rewardPtr, 1, "reward header[0].tableID")
|
||||
assertByte(t, data, rewardPtr+1, 0, "reward header[0].pad1")
|
||||
assertU16(t, data, rewardPtr+2, 0, "reward header[0].pad2")
|
||||
// headerArraySize = 1×8 + 2 = 10
|
||||
assertU32(t, data, rewardPtr+4, 10, "reward header[0].tableOffset")
|
||||
// terminator at rewardPtr+8
|
||||
assertU16(t, data, rewardPtr+8, 0xFFFF, "reward header terminator")
|
||||
// item 0: rate=50, item=149, qty=1
|
||||
itemsBase := rewardPtr + 10
|
||||
assertU16(t, data, itemsBase, 50, "reward[0].items[0].rate")
|
||||
assertU16(t, data, itemsBase+2, 149, "reward[0].items[0].item")
|
||||
assertU16(t, data, itemsBase+4, 1, "reward[0].items[0].quantity")
|
||||
// item 1: rate=30, item=153, qty=1
|
||||
assertU16(t, data, itemsBase+6, 30, "reward[0].items[1].rate")
|
||||
assertU16(t, data, itemsBase+8, 153, "reward[0].items[1].item")
|
||||
assertU16(t, data, itemsBase+10, 1, "reward[0].items[1].quantity")
|
||||
// item list terminator
|
||||
assertU16(t, data, itemsBase+12, 0xFFFF, "reward item terminator")
|
||||
|
||||
// ── Large monster spawn ──────────────────────────────────────────────
|
||||
// {id:11, spawnAmount:1, spawnStage:5, orientation:180, x:1500.0, y:0.0, z:-2000.0}
|
||||
largeMonsterPtr := int(binary.LittleEndian.Uint32(data[0x18:]))
|
||||
assertByte(t, data, largeMonsterPtr, 11, "monster[0].id")
|
||||
// pad[3]
|
||||
assertByte(t, data, largeMonsterPtr+1, 0, "monster[0].pad1")
|
||||
assertByte(t, data, largeMonsterPtr+2, 0, "monster[0].pad2")
|
||||
assertByte(t, data, largeMonsterPtr+3, 0, "monster[0].pad3")
|
||||
assertU32(t, data, largeMonsterPtr+4, 1, "monster[0].spawnAmount")
|
||||
assertU32(t, data, largeMonsterPtr+8, 5, "monster[0].spawnStage")
|
||||
// pad[16] at +0x0C
|
||||
for i := 0; i < 16; i++ {
|
||||
assertByte(t, data, largeMonsterPtr+0x0C+i, 0, "monster[0].pad16")
|
||||
}
|
||||
@@ -739,26 +957,196 @@ func TestGolden_MinimalQuestBinaryLayout(t *testing.T) {
|
||||
assertF32(t, data, largeMonsterPtr+0x20, 1500.0, "monster[0].x")
|
||||
assertF32(t, data, largeMonsterPtr+0x24, 0.0, "monster[0].y")
|
||||
assertF32(t, data, largeMonsterPtr+0x28, -2000.0, "monster[0].z")
|
||||
// pad[16] at +0x2C
|
||||
for i := 0; i < 16; i++ {
|
||||
assertByte(t, data, largeMonsterPtr+0x2C+i, 0, "monster[0].trailing_pad")
|
||||
}
|
||||
// terminator byte after 60-byte entry
|
||||
assertByte(t, data, largeMonsterPtr+60, 0xFF, "monster list terminator")
|
||||
|
||||
// ── Total file size ──────────────────────────────────────────────────
|
||||
// Compute expected size:
|
||||
// header(68) + genProp(66) + mainProp(320) +
|
||||
// strTable(32) + strings(variable) + align +
|
||||
// stages(1×16) + supplyBox(160) + rewardBuf(10+2+12+2) + monsters(60+1)
|
||||
// The exact size depends on string byte lengths — just sanity-check it's > 0x374
|
||||
// (the last verified byte is the monster terminator at largeMonsterPtr+60).
|
||||
minExpectedLen := largeMonsterPtr + 61
|
||||
if len(data) < minExpectedLen {
|
||||
t.Errorf("file too short: len=%d, need at least %d", len(data), minExpectedLen)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Golden test: generalQuestProperties with populated sections ───────────────
|
||||
|
||||
func TestGolden_GeneralQuestPropertiesCounts(t *testing.T) {
|
||||
var q QuestJSON
|
||||
_ = json.Unmarshal([]byte(minimalQuestJSON), &q)
|
||||
q.AreaTransitions = []QuestAreaTransitionsJSON{{}, {}, {}}
|
||||
q.GatheringTables = []QuestGatheringTableJSON{
|
||||
{Items: []QuestGatherItemJSON{{Rate: 100, Item: 1}}},
|
||||
{Items: []QuestGatherItemJSON{{Rate: 100, Item: 2}}},
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(q)
|
||||
data, err := CompileQuestJSON(b)
|
||||
if err != nil {
|
||||
t.Fatalf("compile: %v", err)
|
||||
}
|
||||
|
||||
// area1Zones at 0x7C should be 3.
|
||||
assertByte(t, data, 0x7C, 3, "area1Zones")
|
||||
// gatheringTablesQty at 0x78 should be 2.
|
||||
assertU16(t, data, 0x78, 2, "gatheringTablesQty")
|
||||
}
|
||||
|
||||
// ── Golden test: map sections binary layout ───────────────────────────────────
|
||||
|
||||
func TestGolden_MapSectionsBinaryLayout(t *testing.T) {
|
||||
var q QuestJSON
|
||||
_ = json.Unmarshal([]byte(minimalQuestJSON), &q)
|
||||
q.MapSections = []QuestMapSectionJSON{
|
||||
{
|
||||
LoadedStage: 7,
|
||||
SpawnMonsters: []uint8{0x0B}, // Rathalos
|
||||
MinionSpawns: []QuestMinionSpawnJSON{
|
||||
{Monster: 0x0B, SpawnToggle: 1, SpawnAmount: 2, X: 500.0, Y: 10.0, Z: -300.0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := CompileQuestJSON(func() []byte { b, _ := json.Marshal(q); return b }())
|
||||
if err != nil {
|
||||
t.Fatalf("compile: %v", err)
|
||||
}
|
||||
|
||||
// questAreaPtr must be non-null.
|
||||
questAreaPtr := int(binary.LittleEndian.Uint32(data[0x14:]))
|
||||
if questAreaPtr == 0 {
|
||||
t.Fatal("questAreaPtr is null, expected non-null")
|
||||
}
|
||||
|
||||
// First entry in pointer array must be non-null (points to mapSection).
|
||||
sectionPtr := int(binary.LittleEndian.Uint32(data[questAreaPtr:]))
|
||||
if sectionPtr == 0 {
|
||||
t.Fatal("mapSection[0] ptr is null")
|
||||
}
|
||||
|
||||
// Terminator after the pointer.
|
||||
terminatorOff := questAreaPtr + 4
|
||||
if terminatorOff+4 > len(data) {
|
||||
t.Fatalf("terminator out of bounds")
|
||||
}
|
||||
termVal := binary.LittleEndian.Uint32(data[terminatorOff:])
|
||||
if termVal != 0 {
|
||||
t.Errorf("pointer array terminator = 0x%08X, want 0", termVal)
|
||||
}
|
||||
|
||||
// mapSection at sectionPtr: loadedStage = 7.
|
||||
if sectionPtr+16 > len(data) {
|
||||
t.Fatalf("mapSection out of bounds")
|
||||
}
|
||||
loadedStage := binary.LittleEndian.Uint32(data[sectionPtr:])
|
||||
if loadedStage != 7 {
|
||||
t.Errorf("mapSection.loadedStage = %d, want 7", loadedStage)
|
||||
}
|
||||
|
||||
// spawnTypes and spawnStats ptrs must be non-null.
|
||||
spawnTypesPtr := int(binary.LittleEndian.Uint32(data[sectionPtr+8:]))
|
||||
spawnStatsPtr := int(binary.LittleEndian.Uint32(data[sectionPtr+12:]))
|
||||
if spawnTypesPtr == 0 {
|
||||
t.Fatal("spawnTypesPtr is null")
|
||||
}
|
||||
if spawnStatsPtr == 0 {
|
||||
t.Fatal("spawnStatsPtr is null")
|
||||
}
|
||||
|
||||
// spawnTypes: first entry = Rathalos (0x0B) + pad[3], then 0xFFFF terminator.
|
||||
if spawnTypesPtr+6 > len(data) {
|
||||
t.Fatalf("spawnTypes data out of bounds")
|
||||
}
|
||||
if data[spawnTypesPtr] != 0x0B {
|
||||
t.Errorf("spawnTypes[0].monster = 0x%02X, want 0x0B", data[spawnTypesPtr])
|
||||
}
|
||||
termU16 := binary.LittleEndian.Uint16(data[spawnTypesPtr+4:])
|
||||
if termU16 != 0xFFFF {
|
||||
t.Errorf("spawnTypes terminator = 0x%04X, want 0xFFFF", termU16)
|
||||
}
|
||||
|
||||
// spawnStats: first entry monster = Rathalos (0x0B).
|
||||
if data[spawnStatsPtr] != 0x0B {
|
||||
t.Errorf("spawnStats[0].monster = 0x%02X, want 0x0B", data[spawnStatsPtr])
|
||||
}
|
||||
// spawnToggle at +2 = 1.
|
||||
spawnToggle := binary.LittleEndian.Uint16(data[spawnStatsPtr+2:])
|
||||
if spawnToggle != 1 {
|
||||
t.Errorf("spawnStats[0].spawnToggle = %d, want 1", spawnToggle)
|
||||
}
|
||||
// spawnAmount at +4 = 2.
|
||||
spawnAmount := binary.LittleEndian.Uint32(data[spawnStatsPtr+4:])
|
||||
if spawnAmount != 2 {
|
||||
t.Errorf("spawnStats[0].spawnAmount = %d, want 2", spawnAmount)
|
||||
}
|
||||
// xPos at +0x20 = 500.0.
|
||||
xBits := binary.LittleEndian.Uint32(data[spawnStatsPtr+0x20:])
|
||||
xPos := math.Float32frombits(xBits)
|
||||
if xPos != 500.0 {
|
||||
t.Errorf("spawnStats[0].x = %v, want 500.0", xPos)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Golden test: gathering tables binary layout ───────────────────────────────
|
||||
|
||||
func TestGolden_GatheringTablesBinaryLayout(t *testing.T) {
|
||||
var q QuestJSON
|
||||
_ = json.Unmarshal([]byte(minimalQuestJSON), &q)
|
||||
q.GatheringTables = []QuestGatheringTableJSON{
|
||||
{Items: []QuestGatherItemJSON{{Rate: 75, Item: 500}, {Rate: 25, Item: 501}}},
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(q)
|
||||
data, err := CompileQuestJSON(b)
|
||||
if err != nil {
|
||||
t.Fatalf("compile: %v", err)
|
||||
}
|
||||
|
||||
// gatheringTablesPtr must be non-null.
|
||||
gatherTablesPtr := int(binary.LittleEndian.Uint32(data[0x38:]))
|
||||
if gatherTablesPtr == 0 {
|
||||
t.Fatal("gatheringTablesPtr is null")
|
||||
}
|
||||
|
||||
// gatheringTablesQty at 0x78 must be 1.
|
||||
assertU16(t, data, 0x78, 1, "gatheringTablesQty")
|
||||
|
||||
// Table 0: pointer to item data.
|
||||
tblPtr := int(binary.LittleEndian.Uint32(data[gatherTablesPtr:]))
|
||||
if tblPtr == 0 {
|
||||
t.Fatal("gathering table[0] ptr is null")
|
||||
}
|
||||
|
||||
// Item 0: rate=75, item=500.
|
||||
if tblPtr+4 > len(data) {
|
||||
t.Fatalf("gathering table items out of bounds")
|
||||
}
|
||||
rate0 := binary.LittleEndian.Uint16(data[tblPtr:])
|
||||
item0 := binary.LittleEndian.Uint16(data[tblPtr+2:])
|
||||
if rate0 != 75 {
|
||||
t.Errorf("table[0].items[0].rate = %d, want 75", rate0)
|
||||
}
|
||||
if item0 != 500 {
|
||||
t.Errorf("table[0].items[0].item = %d, want 500", item0)
|
||||
}
|
||||
|
||||
// Item 1: rate=25, item=501.
|
||||
rate1 := binary.LittleEndian.Uint16(data[tblPtr+4:])
|
||||
item1 := binary.LittleEndian.Uint16(data[tblPtr+6:])
|
||||
if rate1 != 25 {
|
||||
t.Errorf("table[0].items[1].rate = %d, want 25", rate1)
|
||||
}
|
||||
if item1 != 501 {
|
||||
t.Errorf("table[0].items[1].item = %d, want 501", item1)
|
||||
}
|
||||
|
||||
// Terminator: 0xFFFF.
|
||||
term := binary.LittleEndian.Uint16(data[tblPtr+8:])
|
||||
if term != 0xFFFF {
|
||||
t.Errorf("gathering table terminator = 0x%04X, want 0xFFFF", term)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Objective encoding golden tests ─────────────────────────────────────────
|
||||
|
||||
func TestGolden_ObjectiveEncoding(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user