mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
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.
845 lines
26 KiB
Go
845 lines
26 KiB
Go
package channelserver
|
||
|
||
import (
|
||
"encoding/binary"
|
||
"fmt"
|
||
"math"
|
||
|
||
"golang.org/x/text/encoding/japanese"
|
||
"golang.org/x/text/transform"
|
||
)
|
||
|
||
// ParseQuestBinary reads a MHF quest binary (ZZ/G10 layout, little-endian)
|
||
// and returns a QuestJSON ready for re-compilation with CompileQuestJSON.
|
||
//
|
||
// The binary layout is described in quest_json.go (CompileQuestJSON).
|
||
// Sections guarded by null pointers in the header are skipped; the
|
||
// corresponding QuestJSON slices will be nil/empty.
|
||
func ParseQuestBinary(data []byte) (*QuestJSON, error) {
|
||
if len(data) < 0x86 {
|
||
return nil, fmt.Errorf("quest binary too short: %d bytes (minimum 0x86)", len(data))
|
||
}
|
||
|
||
// ── Helper closures ──────────────────────────────────────────────────
|
||
u8 := func(off int) uint8 {
|
||
return data[off]
|
||
}
|
||
u16 := func(off int) uint16 {
|
||
return binary.LittleEndian.Uint16(data[off:])
|
||
}
|
||
i16 := func(off int) int16 {
|
||
return int16(binary.LittleEndian.Uint16(data[off:]))
|
||
}
|
||
u32 := func(off int) uint32 {
|
||
return binary.LittleEndian.Uint32(data[off:])
|
||
}
|
||
f32 := func(off int) float32 {
|
||
return math.Float32frombits(binary.LittleEndian.Uint32(data[off:]))
|
||
}
|
||
|
||
// check bounds-checks a read of n bytes at off.
|
||
check := func(off, n int, ctx string) error {
|
||
if off < 0 || off+n > len(data) {
|
||
return fmt.Errorf("%s: offset 0x%X len %d out of bounds (file len %d)", ctx, off, n, len(data))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// readSJIS reads a null-terminated Shift-JIS string starting at off.
|
||
readSJIS := func(off int) (string, error) {
|
||
if off < 0 || off >= len(data) {
|
||
return "", fmt.Errorf("string offset 0x%X out of bounds", off)
|
||
}
|
||
end := off
|
||
for end < len(data) && data[end] != 0 {
|
||
end++
|
||
}
|
||
sjis := data[off:end]
|
||
if len(sjis) == 0 {
|
||
return "", nil
|
||
}
|
||
dec := japanese.ShiftJIS.NewDecoder()
|
||
utf8, _, err := transform.Bytes(dec, sjis)
|
||
if err != nil {
|
||
return "", fmt.Errorf("shift-jis decode at 0x%X: %w", off, err)
|
||
}
|
||
return string(utf8), nil
|
||
}
|
||
|
||
q := &QuestJSON{}
|
||
|
||
// ── Header (0x00–0x43) ───────────────────────────────────────────────
|
||
questTypeFlagsPtr := int(u32(0x00))
|
||
loadedStagesPtr := int(u32(0x04))
|
||
supplyBoxPtr := int(u32(0x08))
|
||
rewardPtr := int(u32(0x0C))
|
||
questAreaPtr := int(u32(0x14))
|
||
largeMonsterPtr := int(u32(0x18))
|
||
areaTransitionsPtr := int(u32(0x1C))
|
||
areaMappingPtr := int(u32(0x20))
|
||
mapInfoPtr := int(u32(0x24))
|
||
gatheringPointsPtr := int(u32(0x28))
|
||
areaFacilitiesPtr := int(u32(0x2C))
|
||
someStringsPtr := int(u32(0x30))
|
||
unk34Ptr := int(u32(0x34)) // stages-end sentinel
|
||
gatheringTablesPtr := int(u32(0x38))
|
||
|
||
// ── General Quest Properties (0x44–0x85) ────────────────────────────
|
||
q.MonsterSizeMulti = u16(0x44)
|
||
q.SizeRange = u16(0x46)
|
||
q.StatTable1 = u32(0x48)
|
||
q.MainRankPoints = u32(0x4C)
|
||
// 0x50 unknown u32 — skipped
|
||
q.SubARankPoints = u32(0x54)
|
||
q.SubBRankPoints = u32(0x58)
|
||
// 0x5C questTypeID/unknown — skipped
|
||
// 0x60 padding
|
||
q.StatTable2 = u8(0x61)
|
||
// 0x62–0x72 padding
|
||
// 0x73 questKn1, 0x74 questKn2, 0x76 questKn3 — skipped
|
||
gatheringTablesQty := int(u16(0x78))
|
||
// 0x7A unknown
|
||
area1Zones := int(u8(0x7C))
|
||
// 0x7D–0x7F area2–4Zones (not needed for parsing)
|
||
|
||
// ── Main Quest Properties (at questTypeFlagsPtr, 320 bytes) ─────────
|
||
if questTypeFlagsPtr == 0 {
|
||
return nil, fmt.Errorf("questTypeFlagsPtr is null; cannot read main quest properties")
|
||
}
|
||
if err := check(questTypeFlagsPtr, questBodyLenZZ, "mainQuestProperties"); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
mp := questTypeFlagsPtr // shorthand
|
||
|
||
q.RankBand = u16(mp + 0x08)
|
||
q.Fee = u32(mp + 0x0C)
|
||
q.RewardMain = u32(mp + 0x10)
|
||
q.RewardSubA = u16(mp + 0x18)
|
||
q.RewardSubB = u16(mp + 0x1C)
|
||
q.HardHRReq = u16(mp + 0x1E)
|
||
questFrames := u32(mp + 0x20)
|
||
q.TimeLimitMinutes = questFrames / (60 * 30)
|
||
q.Map = u32(mp + 0x24)
|
||
questStringsPtr := int(u32(mp + 0x28))
|
||
q.QuestID = u16(mp + 0x2E)
|
||
|
||
// +0x30 objectives[3] (8 bytes each)
|
||
objectives, err := parseObjectives(data, mp+0x30)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
q.ObjectiveMain = objectives[0]
|
||
q.ObjectiveSubA = objectives[1]
|
||
q.ObjectiveSubB = objectives[2]
|
||
|
||
// +0x4C joinRankMin/Max, postRankMin/Max
|
||
q.JoinRankMin = u16(mp + 0x4C)
|
||
q.JoinRankMax = u16(mp + 0x4E)
|
||
q.PostRankMin = u16(mp + 0x50)
|
||
q.PostRankMax = u16(mp + 0x52)
|
||
|
||
// +0x5C forced equipment (6 slots × 4 × u16 = 48 bytes)
|
||
eq, hasEquip := parseForcedEquip(data, mp+0x5C)
|
||
if hasEquip {
|
||
q.ForcedEquipment = eq
|
||
}
|
||
|
||
// +0x97 questVariants
|
||
q.QuestVariant1 = u8(mp + 0x97)
|
||
q.QuestVariant2 = u8(mp + 0x98)
|
||
q.QuestVariant3 = u8(mp + 0x99)
|
||
q.QuestVariant4 = u8(mp + 0x9A)
|
||
|
||
// ── QuestText strings ────────────────────────────────────────────────
|
||
if questStringsPtr != 0 {
|
||
if err := check(questStringsPtr, 32, "questTextTable"); err != nil {
|
||
return nil, err
|
||
}
|
||
strPtrs := make([]int, 8)
|
||
for i := range strPtrs {
|
||
strPtrs[i] = int(u32(questStringsPtr + i*4))
|
||
}
|
||
texts := make([]string, 8)
|
||
for i, ptr := range strPtrs {
|
||
if ptr == 0 {
|
||
continue
|
||
}
|
||
s, err := readSJIS(ptr)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("string[%d]: %w", i, err)
|
||
}
|
||
texts[i] = s
|
||
}
|
||
q.Title = texts[0]
|
||
q.TextMain = texts[1]
|
||
q.TextSubA = texts[2]
|
||
q.TextSubB = texts[3]
|
||
q.SuccessCond = texts[4]
|
||
q.FailCond = texts[5]
|
||
q.Contractor = texts[6]
|
||
q.Description = texts[7]
|
||
}
|
||
|
||
// ── Stages ───────────────────────────────────────────────────────────
|
||
if loadedStagesPtr != 0 && unk34Ptr > loadedStagesPtr {
|
||
off := loadedStagesPtr
|
||
for off+16 <= unk34Ptr {
|
||
if err := check(off, 16, "stage"); err != nil {
|
||
return nil, err
|
||
}
|
||
stageID := u32(off)
|
||
q.Stages = append(q.Stages, QuestStageJSON{StageID: stageID})
|
||
off += 16
|
||
}
|
||
}
|
||
|
||
// ── Supply Box ───────────────────────────────────────────────────────
|
||
if supplyBoxPtr != 0 {
|
||
const supplyBoxSize = (24 + 8 + 8) * 4
|
||
if err := check(supplyBoxPtr, supplyBoxSize, "supplyBox"); err != nil {
|
||
return nil, err
|
||
}
|
||
q.SupplyMain = readSupplySlots(data, supplyBoxPtr, 24)
|
||
q.SupplySubA = readSupplySlots(data, supplyBoxPtr+24*4, 8)
|
||
q.SupplySubB = readSupplySlots(data, supplyBoxPtr+24*4+8*4, 8)
|
||
}
|
||
|
||
// ── Reward Tables ────────────────────────────────────────────────────
|
||
if rewardPtr != 0 {
|
||
tables, err := parseRewardTables(data, rewardPtr)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
q.Rewards = tables
|
||
}
|
||
|
||
// ── Large Monster Spawns ─────────────────────────────────────────────
|
||
if largeMonsterPtr != 0 {
|
||
monsters, err := parseMonsterSpawns(data, largeMonsterPtr, f32)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
q.LargeMonsters = monsters
|
||
}
|
||
|
||
// ── Map Sections (questAreaPtr) ──────────────────────────────────────
|
||
// Layout: u32 ptr[] terminated by u32(0), then each mapSection:
|
||
// u32 loadedStage, u32 unk, u32 spawnTypesPtr, u32 spawnStatsPtr,
|
||
// u32(0) gap, u16 unk — then spawnTypes and spawnStats data.
|
||
if questAreaPtr != 0 {
|
||
sections, err := parseMapSections(data, questAreaPtr, u32, u16, f32)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
q.MapSections = sections
|
||
}
|
||
|
||
// ── Area Mappings (areaMappingPtr) ────────────────────────────────────
|
||
// Read AreaMappings until reaching areaTransitionsPtr (or end of file
|
||
// if areaTransitionsPtr is null). Each entry is 32 bytes.
|
||
if areaMappingPtr != 0 {
|
||
endOff := len(data)
|
||
if areaTransitionsPtr != 0 {
|
||
endOff = areaTransitionsPtr
|
||
}
|
||
mappings, err := parseAreaMappings(data, areaMappingPtr, endOff, f32)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
q.AreaMappings = mappings
|
||
}
|
||
|
||
// ── Area Transitions (areaTransitionsPtr) ─────────────────────────────
|
||
// playerAreaChange[area1Zones]: one u32 ptr per zone.
|
||
if areaTransitionsPtr != 0 && area1Zones > 0 {
|
||
transitions, err := parseAreaTransitions(data, areaTransitionsPtr, area1Zones, u32, i16, f32)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
q.AreaTransitions = transitions
|
||
}
|
||
|
||
// ── Map Info (mapInfoPtr) ─────────────────────────────────────────────
|
||
if mapInfoPtr != 0 {
|
||
if err := check(mapInfoPtr, 8, "mapInfo"); err != nil {
|
||
return nil, err
|
||
}
|
||
q.MapInfo = &QuestMapInfoJSON{
|
||
MapID: u32(mapInfoPtr),
|
||
ReturnBCID: u32(mapInfoPtr + 4),
|
||
}
|
||
}
|
||
|
||
// ── Gathering Points (gatheringPointsPtr) ─────────────────────────────
|
||
// ptGatheringPoint[area1Zones]: one u32 ptr per zone.
|
||
if gatheringPointsPtr != 0 && area1Zones > 0 {
|
||
gatherPts, err := parseGatheringPoints(data, gatheringPointsPtr, area1Zones, u32, u16, f32)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
q.GatheringPoints = gatherPts
|
||
}
|
||
|
||
// ── Area Facilities (areaFacilitiesPtr) ───────────────────────────────
|
||
// ptVar<facPointBlock>[area1Zones]: one u32 ptr per zone.
|
||
if areaFacilitiesPtr != 0 && area1Zones > 0 {
|
||
facilities, err := parseAreaFacilities(data, areaFacilitiesPtr, area1Zones, u32, u16, f32)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
q.AreaFacilities = facilities
|
||
}
|
||
|
||
// ── Some Strings (someStringsPtr / unk30) ─────────────────────────────
|
||
// Layout: ptr someStringPtr, ptr questTypePtr (8 bytes at someStringsPtr).
|
||
if someStringsPtr != 0 {
|
||
if err := check(someStringsPtr, 8, "someStrings"); err != nil {
|
||
return nil, err
|
||
}
|
||
someStrP := int(u32(someStringsPtr))
|
||
questTypeP := int(u32(someStringsPtr + 4))
|
||
if someStrP != 0 {
|
||
s, err := readSJIS(someStrP)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("someString: %w", err)
|
||
}
|
||
q.SomeString = s
|
||
}
|
||
if questTypeP != 0 {
|
||
s, err := readSJIS(questTypeP)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("questTypeString: %w", err)
|
||
}
|
||
q.QuestType = s
|
||
}
|
||
}
|
||
|
||
// ── Gathering Tables (gatheringTablesPtr) ─────────────────────────────
|
||
// ptVar<gatheringTable>[gatheringTablesQty]: one u32 ptr per table.
|
||
// GatherItem: u16 rate + u16 item, terminated by u16(0xFFFF).
|
||
if gatheringTablesPtr != 0 && gatheringTablesQty > 0 {
|
||
tables, err := parseGatheringTables(data, gatheringTablesPtr, gatheringTablesQty, u32, u16)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
q.GatheringTables = tables
|
||
}
|
||
|
||
return q, nil
|
||
}
|
||
|
||
// ── Section parsers ──────────────────────────────────────────────────────────
|
||
|
||
// parseObjectives reads the three 8-byte objective entries at off.
|
||
func parseObjectives(data []byte, off int) ([3]QuestObjectiveJSON, error) {
|
||
var objs [3]QuestObjectiveJSON
|
||
for i := range objs {
|
||
base := off + i*8
|
||
if base+8 > len(data) {
|
||
return objs, fmt.Errorf("objective[%d] at 0x%X out of bounds", i, base)
|
||
}
|
||
goalType := binary.LittleEndian.Uint32(data[base:])
|
||
typeName, ok := objTypeToString(goalType)
|
||
if !ok {
|
||
typeName = "none"
|
||
}
|
||
obj := QuestObjectiveJSON{Type: typeName}
|
||
|
||
if goalType != questObjNone {
|
||
switch goalType {
|
||
case questObjHunt, questObjCapture, questObjSlay, questObjDamage,
|
||
questObjSlayOrDamage, questObjBreakPart:
|
||
obj.Target = uint16(data[base+4])
|
||
// data[base+5] is padding
|
||
default:
|
||
obj.Target = binary.LittleEndian.Uint16(data[base+4:])
|
||
}
|
||
|
||
secondary := binary.LittleEndian.Uint16(data[base+6:])
|
||
if goalType == questObjBreakPart {
|
||
obj.Part = secondary
|
||
} else {
|
||
obj.Count = secondary
|
||
}
|
||
}
|
||
objs[i] = obj
|
||
}
|
||
return objs, nil
|
||
}
|
||
|
||
// parseForcedEquip reads 6 slots × 4 uint16 at off.
|
||
// Returns nil, false if all values are zero (no forced equipment).
|
||
func parseForcedEquip(data []byte, off int) (*QuestForcedEquipJSON, bool) {
|
||
eq := &QuestForcedEquipJSON{}
|
||
slots := []*[4]uint16{&eq.Legs, &eq.Weapon, &eq.Head, &eq.Chest, &eq.Arms, &eq.Waist}
|
||
anyNonZero := false
|
||
for _, slot := range slots {
|
||
for j := range slot {
|
||
v := binary.LittleEndian.Uint16(data[off:])
|
||
slot[j] = v
|
||
if v != 0 {
|
||
anyNonZero = true
|
||
}
|
||
off += 2
|
||
}
|
||
}
|
||
if !anyNonZero {
|
||
return nil, false
|
||
}
|
||
return eq, true
|
||
}
|
||
|
||
// readSupplySlots reads n supply item slots (each 4 bytes: u16 item + u16 qty)
|
||
// starting at off and returns only non-empty entries (item != 0).
|
||
func readSupplySlots(data []byte, off, n int) []QuestSupplyItemJSON {
|
||
var out []QuestSupplyItemJSON
|
||
for i := 0; i < n; i++ {
|
||
base := off + i*4
|
||
item := binary.LittleEndian.Uint16(data[base:])
|
||
qty := binary.LittleEndian.Uint16(data[base+2:])
|
||
if item == 0 {
|
||
continue
|
||
}
|
||
out = append(out, QuestSupplyItemJSON{Item: item, Quantity: qty})
|
||
}
|
||
return out
|
||
}
|
||
|
||
// parseRewardTables reads the reward table array starting at baseOff.
|
||
// Header array: {u8 tableId, u8 pad, u16 pad, u32 tableOffset} per entry,
|
||
// terminated by int16(-1). tableOffset is relative to baseOff.
|
||
// Each item list: {u16 rate, u16 item, u16 quantity} terminated by int16(-1).
|
||
func parseRewardTables(data []byte, baseOff int) ([]QuestRewardTableJSON, error) {
|
||
var tables []QuestRewardTableJSON
|
||
off := baseOff
|
||
for {
|
||
if off+2 > len(data) {
|
||
return nil, fmt.Errorf("reward table header truncated at 0x%X", off)
|
||
}
|
||
if binary.LittleEndian.Uint16(data[off:]) == 0xFFFF {
|
||
break
|
||
}
|
||
if off+8 > len(data) {
|
||
return nil, fmt.Errorf("reward table header entry truncated at 0x%X", off)
|
||
}
|
||
tableID := data[off]
|
||
tableOff := int(binary.LittleEndian.Uint32(data[off+4:])) + baseOff
|
||
off += 8
|
||
|
||
items, err := parseRewardItems(data, tableOff)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("reward table %d items: %w", tableID, err)
|
||
}
|
||
tables = append(tables, QuestRewardTableJSON{TableID: tableID, Items: items})
|
||
}
|
||
return tables, nil
|
||
}
|
||
|
||
// parseRewardItems reads a null-terminated reward item list at off.
|
||
func parseRewardItems(data []byte, off int) ([]QuestRewardItemJSON, error) {
|
||
var items []QuestRewardItemJSON
|
||
for {
|
||
if off+2 > len(data) {
|
||
return nil, fmt.Errorf("reward item list truncated at 0x%X", off)
|
||
}
|
||
if binary.LittleEndian.Uint16(data[off:]) == 0xFFFF {
|
||
break
|
||
}
|
||
if off+6 > len(data) {
|
||
return nil, fmt.Errorf("reward item entry truncated at 0x%X", off)
|
||
}
|
||
rate := binary.LittleEndian.Uint16(data[off:])
|
||
item := binary.LittleEndian.Uint16(data[off+2:])
|
||
qty := binary.LittleEndian.Uint16(data[off+4:])
|
||
items = append(items, QuestRewardItemJSON{Rate: rate, Item: item, Quantity: qty})
|
||
off += 6
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
// parseMonsterSpawns reads large monster spawn entries at baseOff.
|
||
// Each entry is 60 bytes; the list is terminated by a 0xFF byte.
|
||
func parseMonsterSpawns(data []byte, baseOff int, f32fn func(int) float32) ([]QuestMonsterJSON, error) {
|
||
var monsters []QuestMonsterJSON
|
||
off := baseOff
|
||
const entrySize = 60
|
||
for {
|
||
if off >= len(data) {
|
||
return nil, fmt.Errorf("monster spawn list unterminated at end of file")
|
||
}
|
||
if data[off] == 0xFF {
|
||
break
|
||
}
|
||
if off+entrySize > len(data) {
|
||
return nil, fmt.Errorf("monster spawn entry at 0x%X truncated", off)
|
||
}
|
||
m := QuestMonsterJSON{
|
||
ID: data[off],
|
||
SpawnAmount: binary.LittleEndian.Uint32(data[off+4:]),
|
||
SpawnStage: binary.LittleEndian.Uint32(data[off+8:]),
|
||
// +0x0C padding[16]
|
||
Orientation: binary.LittleEndian.Uint32(data[off+0x1C:]),
|
||
X: f32fn(off + 0x20),
|
||
Y: f32fn(off + 0x24),
|
||
Z: f32fn(off + 0x28),
|
||
// +0x2C padding[16]
|
||
}
|
||
monsters = append(monsters, m)
|
||
off += entrySize
|
||
}
|
||
return monsters, nil
|
||
}
|
||
|
||
// parseMapSections reads the MapZones structure at baseOff.
|
||
// Layout: u32 ptr[] terminated by u32(0); each ptr points to a mapSection:
|
||
//
|
||
// u32 loadedStage, u32 unk, u32 spawnTypesPtr, u32 spawnStatsPtr.
|
||
//
|
||
// After the 16-byte mapSection: u32(0) gap + u16 unk (2 bytes).
|
||
// spawnTypes: varPaddT<MonsterID,3> = u8+pad[3] per entry, terminated by 0xFFFF.
|
||
// spawnStats: MinionSpawn (60 bytes) per entry, terminated by 0xFFFF in first 2 bytes.
|
||
func parseMapSections(data []byte, baseOff int,
|
||
u32fn func(int) uint32,
|
||
u16fn func(int) uint16,
|
||
f32fn func(int) float32,
|
||
) ([]QuestMapSectionJSON, error) {
|
||
var sections []QuestMapSectionJSON
|
||
|
||
// Read pointer array (terminated by u32(0)).
|
||
off := baseOff
|
||
for {
|
||
if off+4 > len(data) {
|
||
return nil, fmt.Errorf("mapSection pointer array truncated at 0x%X", off)
|
||
}
|
||
ptr := int(u32fn(off))
|
||
off += 4
|
||
if ptr == 0 {
|
||
break
|
||
}
|
||
|
||
// Read mapSection at ptr.
|
||
if ptr+16 > len(data) {
|
||
return nil, fmt.Errorf("mapSection at 0x%X truncated", ptr)
|
||
}
|
||
loadedStage := u32fn(ptr)
|
||
// ptr+4 is unk u32 — skip
|
||
spawnTypesPtr := int(u32fn(ptr + 8))
|
||
spawnStatsPtr := int(u32fn(ptr + 12))
|
||
|
||
ms := QuestMapSectionJSON{LoadedStage: loadedStage}
|
||
|
||
// Read spawnTypes: varPaddT<MonsterID,3> terminated by 0xFFFF.
|
||
if spawnTypesPtr != 0 {
|
||
stOff := spawnTypesPtr
|
||
for {
|
||
if stOff+2 > len(data) {
|
||
return nil, fmt.Errorf("spawnTypes at 0x%X truncated", stOff)
|
||
}
|
||
if u16fn(stOff) == 0xFFFF {
|
||
break
|
||
}
|
||
if stOff+4 > len(data) {
|
||
return nil, fmt.Errorf("spawnType entry at 0x%X truncated", stOff)
|
||
}
|
||
monID := data[stOff]
|
||
ms.SpawnMonsters = append(ms.SpawnMonsters, monID)
|
||
stOff += 4 // u8 + pad[3]
|
||
}
|
||
}
|
||
|
||
// Read spawnStats: MinionSpawn terminated by 0xFFFF in first 2 bytes.
|
||
if spawnStatsPtr != 0 {
|
||
const minionSize = 60
|
||
ssOff := spawnStatsPtr
|
||
for {
|
||
if ssOff+2 > len(data) {
|
||
return nil, fmt.Errorf("spawnStats at 0x%X truncated", ssOff)
|
||
}
|
||
// Terminator: first 2 bytes == 0xFFFF.
|
||
if u16fn(ssOff) == 0xFFFF {
|
||
break
|
||
}
|
||
if ssOff+minionSize > len(data) {
|
||
return nil, fmt.Errorf("minionSpawn at 0x%X truncated", ssOff)
|
||
}
|
||
spawn := QuestMinionSpawnJSON{
|
||
Monster: data[ssOff],
|
||
// ssOff+1 padding
|
||
SpawnToggle: u16fn(ssOff + 2),
|
||
SpawnAmount: u32fn(ssOff + 4),
|
||
// +8 unk u32, +0xC pad[16], +0x1C unk u32
|
||
X: f32fn(ssOff + 0x20),
|
||
Y: f32fn(ssOff + 0x24),
|
||
Z: f32fn(ssOff + 0x28),
|
||
}
|
||
ms.MinionSpawns = append(ms.MinionSpawns, spawn)
|
||
ssOff += minionSize
|
||
}
|
||
}
|
||
|
||
sections = append(sections, ms)
|
||
}
|
||
|
||
return sections, nil
|
||
}
|
||
|
||
// parseAreaMappings reads AreaMappings entries at baseOff until endOff.
|
||
// Each entry is 32 bytes: float areaX, float areaZ, pad[8],
|
||
// float baseX, float baseZ, float knPos, pad[4].
|
||
func parseAreaMappings(data []byte, baseOff, endOff int, f32fn func(int) float32) ([]QuestAreaMappingJSON, error) {
|
||
var mappings []QuestAreaMappingJSON
|
||
const entrySize = 32
|
||
off := baseOff
|
||
for off+entrySize <= endOff {
|
||
if off+entrySize > len(data) {
|
||
return nil, fmt.Errorf("areaMapping at 0x%X truncated", off)
|
||
}
|
||
am := QuestAreaMappingJSON{
|
||
AreaX: f32fn(off),
|
||
AreaZ: f32fn(off + 4),
|
||
// off+8: pad[8]
|
||
BaseX: f32fn(off + 16),
|
||
BaseZ: f32fn(off + 20),
|
||
KnPos: f32fn(off + 24),
|
||
// off+28: pad[4]
|
||
}
|
||
mappings = append(mappings, am)
|
||
off += entrySize
|
||
}
|
||
return mappings, nil
|
||
}
|
||
|
||
// parseAreaTransitions reads playerAreaChange[numZones] at baseOff.
|
||
// Each entry is a u32 pointer to a floatSet array terminated by s16(-1).
|
||
// floatSet: s16 targetStageId + s16 stageVariant + float[3] current + float[5] box +
|
||
// float[3] target + s16[2] rotation = 52 bytes.
|
||
func parseAreaTransitions(data []byte, baseOff, numZones int,
|
||
u32fn func(int) uint32,
|
||
i16fn func(int) int16,
|
||
f32fn func(int) float32,
|
||
) ([]QuestAreaTransitionsJSON, error) {
|
||
result := make([]QuestAreaTransitionsJSON, numZones)
|
||
|
||
if baseOff+numZones*4 > len(data) {
|
||
return nil, fmt.Errorf("areaTransitions pointer array at 0x%X truncated", baseOff)
|
||
}
|
||
|
||
for i := 0; i < numZones; i++ {
|
||
ptr := int(u32fn(baseOff + i*4))
|
||
if ptr == 0 {
|
||
// Null pointer — no transitions for this zone.
|
||
continue
|
||
}
|
||
|
||
// Read floatSet entries until targetStageId1 == -1.
|
||
var transitions []QuestAreaTransitionJSON
|
||
off := ptr
|
||
for {
|
||
if off+2 > len(data) {
|
||
return nil, fmt.Errorf("floatSet at 0x%X truncated", off)
|
||
}
|
||
targetStageID := i16fn(off)
|
||
if targetStageID == -1 {
|
||
break
|
||
}
|
||
// Each floatSet is 52 bytes:
|
||
// s16 targetStageId1 + s16 stageVariant = 4
|
||
// float[3] current = 12
|
||
// float[5] transitionBox = 20
|
||
// float[3] target = 12
|
||
// s16[2] rotation = 4
|
||
// Total = 52
|
||
const floatSetSize = 52
|
||
if off+floatSetSize > len(data) {
|
||
return nil, fmt.Errorf("floatSet at 0x%X truncated (need %d bytes)", off, floatSetSize)
|
||
}
|
||
tr := QuestAreaTransitionJSON{
|
||
TargetStageID1: targetStageID,
|
||
StageVariant: i16fn(off + 2),
|
||
CurrentX: f32fn(off + 4),
|
||
CurrentY: f32fn(off + 8),
|
||
CurrentZ: f32fn(off + 12),
|
||
TargetX: f32fn(off + 36),
|
||
TargetY: f32fn(off + 40),
|
||
TargetZ: f32fn(off + 44),
|
||
}
|
||
for j := 0; j < 5; j++ {
|
||
tr.TransitionBox[j] = f32fn(off + 16 + j*4)
|
||
}
|
||
tr.TargetRotation[0] = i16fn(off + 48)
|
||
tr.TargetRotation[1] = i16fn(off + 50)
|
||
transitions = append(transitions, tr)
|
||
off += floatSetSize
|
||
}
|
||
result[i] = QuestAreaTransitionsJSON{Transitions: transitions}
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// parseGatheringPoints reads ptGatheringPoint[numZones] at baseOff.
|
||
// Each entry is a u32 pointer to gatheringPoint[4] terminated by xPos==-1.0.
|
||
// gatheringPoint: float xPos, yPos, zPos, range, u16 gatheringID, u16 maxCount, pad[2], u16 minCount = 24 bytes.
|
||
func parseGatheringPoints(data []byte, baseOff, numZones int,
|
||
u32fn func(int) uint32,
|
||
u16fn func(int) uint16,
|
||
f32fn func(int) float32,
|
||
) ([]QuestAreaGatheringJSON, error) {
|
||
result := make([]QuestAreaGatheringJSON, numZones)
|
||
|
||
if baseOff+numZones*4 > len(data) {
|
||
return nil, fmt.Errorf("gatheringPoints pointer array at 0x%X truncated", baseOff)
|
||
}
|
||
|
||
const sentinel = uint32(0xBF800000) // float32(-1.0)
|
||
const pointSize = 24
|
||
|
||
for i := 0; i < numZones; i++ {
|
||
ptr := int(u32fn(baseOff + i*4))
|
||
if ptr == 0 {
|
||
continue
|
||
}
|
||
|
||
var points []QuestGatheringPointJSON
|
||
off := ptr
|
||
for {
|
||
if off+4 > len(data) {
|
||
return nil, fmt.Errorf("gatheringPoint at 0x%X truncated", off)
|
||
}
|
||
// Terminator: xPos bit pattern == 0xBF800000 (-1.0f).
|
||
if binary.LittleEndian.Uint32(data[off:]) == sentinel {
|
||
break
|
||
}
|
||
if off+pointSize > len(data) {
|
||
return nil, fmt.Errorf("gatheringPoint entry at 0x%X truncated", off)
|
||
}
|
||
gp := QuestGatheringPointJSON{
|
||
X: f32fn(off),
|
||
Y: f32fn(off + 4),
|
||
Z: f32fn(off + 8),
|
||
Range: f32fn(off + 12),
|
||
GatheringID: u16fn(off + 16),
|
||
MaxCount: u16fn(off + 18),
|
||
// off+20 pad[2]
|
||
MinCount: u16fn(off + 22),
|
||
}
|
||
points = append(points, gp)
|
||
off += pointSize
|
||
}
|
||
result[i] = QuestAreaGatheringJSON{Points: points}
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// parseAreaFacilities reads ptVar<facPointBlock>[numZones] at baseOff.
|
||
// Each entry is a u32 pointer to a facPointBlock.
|
||
// facPoint: pad[2] + SpecAc(u16) + xPos + yPos + zPos + range + id(u16) + pad[2] = 24 bytes.
|
||
// Termination: the loop condition checks read_unsigned($+4,4) != 0xBF800000.
|
||
// So a facPoint whose xPos (at offset +4 from start of that potential entry) == -1.0 terminates.
|
||
// After all facPoints: padding[0xC] + float + float = 20 bytes (block footer, not parsed into JSON).
|
||
func parseAreaFacilities(data []byte, baseOff, numZones int,
|
||
u32fn func(int) uint32,
|
||
u16fn func(int) uint16,
|
||
f32fn func(int) float32,
|
||
) ([]QuestAreaFacilitiesJSON, error) {
|
||
result := make([]QuestAreaFacilitiesJSON, numZones)
|
||
|
||
if baseOff+numZones*4 > len(data) {
|
||
return nil, fmt.Errorf("areaFacilities pointer array at 0x%X truncated", baseOff)
|
||
}
|
||
|
||
const sentinel = uint32(0xBF800000)
|
||
const pointSize = 24
|
||
|
||
for i := 0; i < numZones; i++ {
|
||
ptr := int(u32fn(baseOff + i*4))
|
||
if ptr == 0 {
|
||
continue
|
||
}
|
||
|
||
var points []QuestFacilityPointJSON
|
||
off := ptr
|
||
for off+8 <= len(data) {
|
||
// Check: read_unsigned($+4, 4) == sentinel means terminate.
|
||
// $+4 is the xPos field of the potential next facPoint.
|
||
if binary.LittleEndian.Uint32(data[off+4:]) == sentinel {
|
||
break
|
||
}
|
||
if off+pointSize > len(data) {
|
||
return nil, fmt.Errorf("facPoint at 0x%X truncated", off)
|
||
}
|
||
fp := QuestFacilityPointJSON{
|
||
// off+0: pad[2]
|
||
Type: u16fn(off + 2),
|
||
X: f32fn(off + 4),
|
||
Y: f32fn(off + 8),
|
||
Z: f32fn(off + 12),
|
||
Range: f32fn(off + 16),
|
||
ID: u16fn(off + 20),
|
||
// off+22: pad[2]
|
||
}
|
||
points = append(points, fp)
|
||
off += pointSize
|
||
}
|
||
result[i] = QuestAreaFacilitiesJSON{Points: points}
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// parseGatheringTables reads ptVar<gatheringTable>[count] at baseOff.
|
||
// Each entry is a u32 pointer to GatherItem[] terminated by u16(0xFFFF).
|
||
// GatherItem: u16 rate + u16 item = 4 bytes.
|
||
func parseGatheringTables(data []byte, baseOff, count int,
|
||
u32fn func(int) uint32,
|
||
u16fn func(int) uint16,
|
||
) ([]QuestGatheringTableJSON, error) {
|
||
result := make([]QuestGatheringTableJSON, count)
|
||
|
||
if baseOff+count*4 > len(data) {
|
||
return nil, fmt.Errorf("gatheringTables pointer array at 0x%X truncated", baseOff)
|
||
}
|
||
|
||
for i := 0; i < count; i++ {
|
||
ptr := int(u32fn(baseOff + i*4))
|
||
if ptr == 0 {
|
||
continue
|
||
}
|
||
|
||
var items []QuestGatherItemJSON
|
||
off := ptr
|
||
for {
|
||
if off+2 > len(data) {
|
||
return nil, fmt.Errorf("gatheringTable at 0x%X truncated", off)
|
||
}
|
||
if u16fn(off) == 0xFFFF {
|
||
break
|
||
}
|
||
if off+4 > len(data) {
|
||
return nil, fmt.Errorf("gatherItem at 0x%X truncated", off)
|
||
}
|
||
items = append(items, QuestGatherItemJSON{
|
||
Rate: u16fn(off),
|
||
Item: u16fn(off + 2),
|
||
})
|
||
off += 4
|
||
}
|
||
result[i] = QuestGatheringTableJSON{Items: items}
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// objTypeToString maps a uint32 goal type to its JSON string name.
|
||
// Returns "", false for unknown types.
|
||
func objTypeToString(t uint32) (string, bool) {
|
||
for name, v := range questObjTypeMap {
|
||
if v == t {
|
||
return name, true
|
||
}
|
||
}
|
||
return "", false
|
||
}
|