mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-24 08:33:41 +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:
@@ -105,11 +105,107 @@ type QuestForcedEquipJSON struct {
|
||||
Waist [4]uint16 `json:"waist,omitempty"`
|
||||
}
|
||||
|
||||
// QuestMinionSpawnJSON is one minion spawn entry within a map section.
|
||||
type QuestMinionSpawnJSON struct {
|
||||
Monster uint8 `json:"monster"`
|
||||
SpawnToggle uint16 `json:"spawn_toggle"`
|
||||
SpawnAmount uint32 `json:"spawn_amount"`
|
||||
X float32 `json:"x"`
|
||||
Y float32 `json:"y"`
|
||||
Z float32 `json:"z"`
|
||||
}
|
||||
|
||||
// QuestMapSectionJSON defines one map section with its minion spawns.
|
||||
// Each section corresponds to a loaded stage area.
|
||||
type QuestMapSectionJSON struct {
|
||||
LoadedStage uint32 `json:"loaded_stage"`
|
||||
SpawnMonsters []uint8 `json:"spawn_monsters,omitempty"` // monster IDs for spawn type list
|
||||
MinionSpawns []QuestMinionSpawnJSON `json:"minion_spawns,omitempty"`
|
||||
}
|
||||
|
||||
// QuestAreaTransitionJSON is one zone transition (floatSet).
|
||||
type QuestAreaTransitionJSON struct {
|
||||
TargetStageID1 int16 `json:"target_stage_id"`
|
||||
StageVariant int16 `json:"stage_variant"`
|
||||
CurrentX float32 `json:"current_x"`
|
||||
CurrentY float32 `json:"current_y"`
|
||||
CurrentZ float32 `json:"current_z"`
|
||||
TransitionBox [5]float32 `json:"transition_box"`
|
||||
TargetX float32 `json:"target_x"`
|
||||
TargetY float32 `json:"target_y"`
|
||||
TargetZ float32 `json:"target_z"`
|
||||
TargetRotation [2]int16 `json:"target_rotation"`
|
||||
}
|
||||
|
||||
// QuestAreaTransitionsJSON holds the transitions for one area zone entry.
|
||||
// The pointer may be null (empty transitions list) for zones without transitions.
|
||||
type QuestAreaTransitionsJSON struct {
|
||||
Transitions []QuestAreaTransitionJSON `json:"transitions,omitempty"`
|
||||
}
|
||||
|
||||
// QuestAreaMappingJSON defines coordinate mappings between area and base map.
|
||||
// Layout: 32 bytes per entry (Area_xPos, Area_zPos, pad8, Base_xPos, Base_zPos, kn_Pos, pad4).
|
||||
type QuestAreaMappingJSON struct {
|
||||
AreaX float32 `json:"area_x"`
|
||||
AreaZ float32 `json:"area_z"`
|
||||
BaseX float32 `json:"base_x"`
|
||||
BaseZ float32 `json:"base_z"`
|
||||
KnPos float32 `json:"kn_pos"`
|
||||
}
|
||||
|
||||
// QuestMapInfoJSON contains the map ID and return base camp ID.
|
||||
type QuestMapInfoJSON struct {
|
||||
MapID uint32 `json:"map_id"`
|
||||
ReturnBCID uint32 `json:"return_bc_id"`
|
||||
}
|
||||
|
||||
// QuestGatheringPointJSON is one gathering point (24 bytes).
|
||||
type QuestGatheringPointJSON struct {
|
||||
X float32 `json:"x"`
|
||||
Y float32 `json:"y"`
|
||||
Z float32 `json:"z"`
|
||||
Range float32 `json:"range"`
|
||||
GatheringID uint16 `json:"gathering_id"`
|
||||
MaxCount uint16 `json:"max_count"`
|
||||
MinCount uint16 `json:"min_count"`
|
||||
}
|
||||
|
||||
// QuestAreaGatheringJSON holds up to 4 gathering points for one area zone entry.
|
||||
// A nil/empty list means the pointer is null for this zone.
|
||||
type QuestAreaGatheringJSON struct {
|
||||
Points []QuestGatheringPointJSON `json:"points,omitempty"`
|
||||
}
|
||||
|
||||
// QuestFacilityPointJSON is one facility point (24 bytes, facPoint in hexpat).
|
||||
type QuestFacilityPointJSON struct {
|
||||
Type uint16 `json:"type"` // SpecAc: 1=cooking, 2=fishing, 3=bluebox, etc.
|
||||
X float32 `json:"x"`
|
||||
Y float32 `json:"y"`
|
||||
Z float32 `json:"z"`
|
||||
Range float32 `json:"range"`
|
||||
ID uint16 `json:"id"`
|
||||
}
|
||||
|
||||
// QuestAreaFacilitiesJSON holds the facilities block for one area zone entry.
|
||||
// A nil/empty list means the pointer is null for this zone.
|
||||
type QuestAreaFacilitiesJSON struct {
|
||||
Points []QuestFacilityPointJSON `json:"points,omitempty"`
|
||||
}
|
||||
|
||||
// QuestGatherItemJSON is one entry in a gathering table.
|
||||
type QuestGatherItemJSON struct {
|
||||
Rate uint16 `json:"rate"`
|
||||
Item uint16 `json:"item"`
|
||||
}
|
||||
|
||||
// QuestGatheringTableJSON is one gathering loot table.
|
||||
type QuestGatheringTableJSON struct {
|
||||
Items []QuestGatherItemJSON `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// QuestJSON is the human-readable quest definition.
|
||||
// Time values: TimeLimitMinutes is converted to frames (×30×60) in the binary.
|
||||
// Strings: encoded as UTF-8 here, converted to Shift-JIS in the binary.
|
||||
// All pointer-based sections (gathering, area transitions, facilities) are
|
||||
// omitted — those fields are set to null pointers so the client uses defaults.
|
||||
type QuestJSON struct {
|
||||
// Quest identification
|
||||
QuestID uint16 `json:"quest_id"`
|
||||
@@ -174,6 +270,32 @@ type QuestJSON struct {
|
||||
|
||||
// Forced equipment (optional)
|
||||
ForcedEquipment *QuestForcedEquipJSON `json:"forced_equipment,omitempty"`
|
||||
|
||||
// Map sections with minion spawns (questAreaPtr)
|
||||
MapSections []QuestMapSectionJSON `json:"map_sections,omitempty"`
|
||||
|
||||
// Area transitions per zone (areaTransitionsPtr); one entry per zone.
|
||||
// Length determines area1Zones in generalQuestProperties.
|
||||
AreaTransitions []QuestAreaTransitionsJSON `json:"area_transitions,omitempty"`
|
||||
|
||||
// Area coordinate mappings (areaMappingPtr)
|
||||
AreaMappings []QuestAreaMappingJSON `json:"area_mappings,omitempty"`
|
||||
|
||||
// Map info: map ID + return base camp ID (mapInfoPtr)
|
||||
MapInfo *QuestMapInfoJSON `json:"map_info,omitempty"`
|
||||
|
||||
// Per-zone gathering points (gatheringPointsPtr); one entry per zone.
|
||||
GatheringPoints []QuestAreaGatheringJSON `json:"gathering_points,omitempty"`
|
||||
|
||||
// Per-zone area facilities (areaFacilitiesPtr); one entry per zone.
|
||||
AreaFacilities []QuestAreaFacilitiesJSON `json:"area_facilities,omitempty"`
|
||||
|
||||
// Additional metadata strings (someStringsPtr / unk30). Optional.
|
||||
SomeString string `json:"some_string,omitempty"`
|
||||
QuestType string `json:"quest_type_string,omitempty"`
|
||||
|
||||
// Gathering loot tables (gatheringTablesPtr)
|
||||
GatheringTables []QuestGatheringTableJSON `json:"gathering_tables,omitempty"`
|
||||
}
|
||||
|
||||
// toShiftJIS converts a UTF-8 string to a null-terminated Shift-JIS byte slice.
|
||||
@@ -194,6 +316,11 @@ func writeUint16LE(buf *bytes.Buffer, v uint16) {
|
||||
buf.Write(b[:])
|
||||
}
|
||||
|
||||
// writeInt16LE writes a little-endian int16 to buf.
|
||||
func writeInt16LE(buf *bytes.Buffer, v int16) {
|
||||
writeUint16LE(buf, uint16(v))
|
||||
}
|
||||
|
||||
// writeUint32LE writes a little-endian uint32 to buf.
|
||||
func writeUint32LE(buf *bytes.Buffer, v uint32) {
|
||||
b := [4]byte{}
|
||||
@@ -213,6 +340,29 @@ func pad(buf *bytes.Buffer, n int) {
|
||||
buf.Write(make([]byte, n))
|
||||
}
|
||||
|
||||
// questBuilder is a small helper for building a quest binary with pointer patching.
|
||||
// All pointers are absolute offsets from the start of the buffer (file start).
|
||||
type questBuilder struct {
|
||||
out *bytes.Buffer
|
||||
}
|
||||
|
||||
// reserve writes a u32(0) placeholder and returns its offset in the buffer.
|
||||
func (b *questBuilder) reserve() int {
|
||||
off := b.out.Len()
|
||||
writeUint32LE(b.out, 0)
|
||||
return off
|
||||
}
|
||||
|
||||
// patch writes the current buffer length as a u32 at the previously reserved offset.
|
||||
func (b *questBuilder) patch(reservedOff int) {
|
||||
binary.LittleEndian.PutUint32(b.out.Bytes()[reservedOff:], uint32(b.out.Len()))
|
||||
}
|
||||
|
||||
// patchValue writes a specific uint32 value at a previously reserved offset.
|
||||
func (b *questBuilder) patchValue(reservedOff int, v uint32) {
|
||||
binary.LittleEndian.PutUint32(b.out.Bytes()[reservedOff:], v)
|
||||
}
|
||||
|
||||
// objectiveBytes serialises one QuestObjectiveJSON to 8 bytes.
|
||||
// Layout per hexpat objective.hexpat:
|
||||
//
|
||||
@@ -267,13 +417,30 @@ func objectiveBytes(obj QuestObjectiveJSON) ([]byte, error) {
|
||||
// 0x044–0x085 generalQuestProperties (66 bytes)
|
||||
// 0x086–0x1C5 mainQuestProperties (320 bytes, questBodyLenZZ)
|
||||
// 0x1C6+ QuestText pointer table (32 bytes) + strings (Shift-JIS)
|
||||
// aligned+ stages, supply box, reward tables, monster spawns
|
||||
// aligned+ stages, supply box, reward tables, monster spawns,
|
||||
// map sections, area mappings, area transitions,
|
||||
// map info, gathering points, area facilities,
|
||||
// some strings, gathering tables
|
||||
func CompileQuestJSON(data []byte) ([]byte, error) {
|
||||
var q QuestJSON
|
||||
if err := json.Unmarshal(data, &q); err != nil {
|
||||
return nil, fmt.Errorf("parse quest JSON: %w", err)
|
||||
}
|
||||
|
||||
// ── Compute counts before writing generalQuestProperties ─────────────
|
||||
numZones := len(q.AreaTransitions)
|
||||
numGatheringTables := len(q.GatheringTables)
|
||||
|
||||
// Validate zone-length consistency.
|
||||
if len(q.GatheringPoints) != 0 && len(q.GatheringPoints) != numZones {
|
||||
return nil, fmt.Errorf("GatheringPoints len (%d) must equal AreaTransitions len (%d) or be 0",
|
||||
len(q.GatheringPoints), numZones)
|
||||
}
|
||||
if len(q.AreaFacilities) != 0 && len(q.AreaFacilities) != numZones {
|
||||
return nil, fmt.Errorf("AreaFacilities len (%d) must equal AreaTransitions len (%d) or be 0",
|
||||
len(q.AreaFacilities), numZones)
|
||||
}
|
||||
|
||||
// ── Section offsets (computed as we build) ──────────────────────────
|
||||
const (
|
||||
headerSize = 68 // 0x44
|
||||
@@ -335,88 +502,101 @@ func CompileQuestJSON(data []byte) ([]byte, error) {
|
||||
// Large monster spawns: each is 60 bytes + 1-byte terminator.
|
||||
largeMonsterPtr := afterRewards
|
||||
monsterBuf := buildMonsterSpawns(q.LargeMonsters)
|
||||
afterMonsters := align4(largeMonsterPtr + uint32(len(monsterBuf)))
|
||||
|
||||
// ── Assemble file ────────────────────────────────────────────────────
|
||||
out := &bytes.Buffer{}
|
||||
qb := &questBuilder{out: &bytes.Buffer{}}
|
||||
|
||||
// ── Header (68 bytes) ────────────────────────────────────────────────
|
||||
writeUint32LE(out, questTypeFlagsPtr) // 0x00 questTypeFlagsPtr
|
||||
writeUint32LE(out, loadedStagesPtr) // 0x04 loadedStagesPtr
|
||||
writeUint32LE(out, supplyBoxPtr) // 0x08 supplyBoxPtr
|
||||
writeUint32LE(out, rewardPtr) // 0x0C rewardPtr
|
||||
writeUint16LE(out, 0) // 0x10 subSupplyBoxPtr (unused)
|
||||
out.WriteByte(0) // 0x12 hidden
|
||||
out.WriteByte(0) // 0x13 subSupplyBoxLen
|
||||
writeUint32LE(out, 0) // 0x14 questAreaPtr (null)
|
||||
writeUint32LE(out, largeMonsterPtr) // 0x18 largeMonsterPtr
|
||||
writeUint32LE(out, 0) // 0x1C areaTransitionsPtr (null)
|
||||
writeUint32LE(out, 0) // 0x20 areaMappingPtr (null)
|
||||
writeUint32LE(out, 0) // 0x24 mapInfoPtr (null)
|
||||
writeUint32LE(out, 0) // 0x28 gatheringPointsPtr (null)
|
||||
writeUint32LE(out, 0) // 0x2C areaFacilitiesPtr (null)
|
||||
writeUint32LE(out, 0) // 0x30 someStringsPtr (null)
|
||||
writeUint32LE(out, unk34Ptr) // 0x34 fixedCoords1Ptr (stages end)
|
||||
writeUint32LE(out, 0) // 0x38 gatheringTablesPtr (null)
|
||||
writeUint32LE(out, 0) // 0x3C fixedCoords2Ptr (null)
|
||||
writeUint32LE(out, 0) // 0x40 fixedInfoPtr (null)
|
||||
// ── Header placeholders (68 bytes) ────────────────────────────────────
|
||||
// We'll write the header now with known values; variable section pointers
|
||||
// that depend on the preceding variable sections are also known at this
|
||||
// point because we computed them above. The new sections (area, gathering,
|
||||
// etc.) will be appended after the monster spawns and patched in.
|
||||
hdrQuestAreaOff := 0x14 // questAreaPtr placeholder
|
||||
hdrAreaTransOff := 0x1C // areaTransitionsPtr placeholder
|
||||
hdrAreaMappingOff := 0x20 // areaMappingPtr placeholder
|
||||
hdrMapInfoOff := 0x24 // mapInfoPtr placeholder
|
||||
hdrGatherPtsOff := 0x28 // gatheringPointsPtr placeholder
|
||||
hdrFacilitiesOff := 0x2C // areaFacilitiesPtr placeholder
|
||||
hdrSomeStringsOff := 0x30 // someStringsPtr placeholder
|
||||
hdrGatherTablesOff := 0x38 // gatheringTablesPtr placeholder
|
||||
|
||||
if out.Len() != headerSize {
|
||||
return nil, fmt.Errorf("header size mismatch: got %d want %d", out.Len(), headerSize)
|
||||
writeUint32LE(qb.out, questTypeFlagsPtr) // 0x00 questTypeFlagsPtr
|
||||
writeUint32LE(qb.out, loadedStagesPtr) // 0x04 loadedStagesPtr
|
||||
writeUint32LE(qb.out, supplyBoxPtr) // 0x08 supplyBoxPtr
|
||||
writeUint32LE(qb.out, rewardPtr) // 0x0C rewardPtr
|
||||
writeUint16LE(qb.out, 0) // 0x10 subSupplyBoxPtr (unused)
|
||||
qb.out.WriteByte(0) // 0x12 hidden
|
||||
qb.out.WriteByte(0) // 0x13 subSupplyBoxLen
|
||||
writeUint32LE(qb.out, 0) // 0x14 questAreaPtr (patched later)
|
||||
writeUint32LE(qb.out, largeMonsterPtr) // 0x18 largeMonsterPtr
|
||||
writeUint32LE(qb.out, 0) // 0x1C areaTransitionsPtr (patched later)
|
||||
writeUint32LE(qb.out, 0) // 0x20 areaMappingPtr (patched later)
|
||||
writeUint32LE(qb.out, 0) // 0x24 mapInfoPtr (patched later)
|
||||
writeUint32LE(qb.out, 0) // 0x28 gatheringPointsPtr (patched later)
|
||||
writeUint32LE(qb.out, 0) // 0x2C areaFacilitiesPtr (patched later)
|
||||
writeUint32LE(qb.out, 0) // 0x30 someStringsPtr (patched later)
|
||||
writeUint32LE(qb.out, unk34Ptr) // 0x34 fixedCoords1Ptr (stages end)
|
||||
writeUint32LE(qb.out, 0) // 0x38 gatheringTablesPtr (patched later)
|
||||
writeUint32LE(qb.out, 0) // 0x3C fixedCoords2Ptr (null)
|
||||
writeUint32LE(qb.out, 0) // 0x40 fixedInfoPtr (null)
|
||||
|
||||
if qb.out.Len() != headerSize {
|
||||
return nil, fmt.Errorf("header size mismatch: got %d want %d", qb.out.Len(), headerSize)
|
||||
}
|
||||
|
||||
// ── General Quest Properties (66 bytes, 0x44–0x85) ──────────────────
|
||||
writeUint16LE(out, q.MonsterSizeMulti) // 0x44 monsterSizeMulti
|
||||
writeUint16LE(out, q.SizeRange) // 0x46 sizeRange
|
||||
writeUint32LE(out, q.StatTable1) // 0x48 statTable1
|
||||
writeUint32LE(out, q.MainRankPoints) // 0x4C mainRankPoints
|
||||
writeUint32LE(out, 0) // 0x50 unknown
|
||||
writeUint32LE(out, q.SubARankPoints) // 0x54 subARankPoints
|
||||
writeUint32LE(out, q.SubBRankPoints) // 0x58 subBRankPoints
|
||||
writeUint32LE(out, 0) // 0x5C questTypeID / unknown
|
||||
out.WriteByte(0) // 0x60 padding
|
||||
out.WriteByte(q.StatTable2) // 0x61 statTable2
|
||||
pad(out, 0x11) // 0x62–0x72 padding
|
||||
out.WriteByte(0) // 0x73 questKn1
|
||||
writeUint16LE(out, 0) // 0x74 questKn2
|
||||
writeUint16LE(out, 0) // 0x76 questKn3
|
||||
writeUint16LE(out, 0) // 0x78 gatheringTablesQty
|
||||
writeUint16LE(out, 0) // 0x7A unknown
|
||||
out.WriteByte(0) // 0x7C area1Zones
|
||||
out.WriteByte(0) // 0x7D area2Zones
|
||||
out.WriteByte(0) // 0x7E area3Zones
|
||||
out.WriteByte(0) // 0x7F area4Zones
|
||||
writeUint16LE(out, 0) // 0x80 unknown
|
||||
writeUint16LE(out, 0) // 0x82 unknown
|
||||
writeUint16LE(out, 0) // 0x84 unknown
|
||||
writeUint16LE(qb.out, q.MonsterSizeMulti) // 0x44 monsterSizeMulti
|
||||
writeUint16LE(qb.out, q.SizeRange) // 0x46 sizeRange
|
||||
writeUint32LE(qb.out, q.StatTable1) // 0x48 statTable1
|
||||
writeUint32LE(qb.out, q.MainRankPoints) // 0x4C mainRankPoints
|
||||
writeUint32LE(qb.out, 0) // 0x50 unknown
|
||||
writeUint32LE(qb.out, q.SubARankPoints) // 0x54 subARankPoints
|
||||
writeUint32LE(qb.out, q.SubBRankPoints) // 0x58 subBRankPoints
|
||||
writeUint32LE(qb.out, 0) // 0x5C questTypeID / unknown
|
||||
qb.out.WriteByte(0) // 0x60 padding
|
||||
qb.out.WriteByte(q.StatTable2) // 0x61 statTable2
|
||||
pad(qb.out, 0x11) // 0x62–0x72 padding
|
||||
qb.out.WriteByte(0) // 0x73 questKn1
|
||||
writeUint16LE(qb.out, 0) // 0x74 questKn2
|
||||
writeUint16LE(qb.out, 0) // 0x76 questKn3
|
||||
writeUint16LE(qb.out, uint16(numGatheringTables)) // 0x78 gatheringTablesQty
|
||||
writeUint16LE(qb.out, 0) // 0x7A unknown
|
||||
qb.out.WriteByte(uint8(numZones)) // 0x7C area1Zones
|
||||
qb.out.WriteByte(0) // 0x7D area2Zones
|
||||
qb.out.WriteByte(0) // 0x7E area3Zones
|
||||
qb.out.WriteByte(0) // 0x7F area4Zones
|
||||
writeUint16LE(qb.out, 0) // 0x80 unknown
|
||||
writeUint16LE(qb.out, 0) // 0x82 unknown
|
||||
writeUint16LE(qb.out, 0) // 0x84 unknown
|
||||
|
||||
if out.Len() != headerSize+genPropSize {
|
||||
return nil, fmt.Errorf("genProp size mismatch: got %d want %d", out.Len(), headerSize+genPropSize)
|
||||
if qb.out.Len() != headerSize+genPropSize {
|
||||
return nil, fmt.Errorf("genProp size mismatch: got %d want %d", qb.out.Len(), headerSize+genPropSize)
|
||||
}
|
||||
|
||||
// ── Main Quest Properties (320 bytes, 0x86–0x1C5) ───────────────────
|
||||
// Matches mainQuestProperties struct in questfile.bin.hexpat.
|
||||
mainStart := out.Len()
|
||||
out.WriteByte(0) // +0x00 unknown
|
||||
out.WriteByte(0) // +0x01 musicMode
|
||||
out.WriteByte(0) // +0x02 localeFlags
|
||||
out.WriteByte(0) // +0x03 unknown
|
||||
out.WriteByte(0) // +0x04 rankingID
|
||||
out.WriteByte(0) // +0x05 unknown
|
||||
writeUint16LE(out, 0) // +0x06 unknown
|
||||
writeUint16LE(out, q.RankBand) // +0x08 rankBand
|
||||
writeUint16LE(out, 0) // +0x0A questTypeID
|
||||
writeUint32LE(out, q.Fee) // +0x0C questFee
|
||||
writeUint32LE(out, q.RewardMain) // +0x10 rewardMain
|
||||
writeUint32LE(out, 0) // +0x14 cartsOrReduction
|
||||
writeUint16LE(out, q.RewardSubA) // +0x18 rewardA
|
||||
writeUint16LE(out, 0) // +0x1A padding
|
||||
writeUint16LE(out, q.RewardSubB) // +0x1C rewardB
|
||||
writeUint16LE(out, q.HardHRReq) // +0x1E hardHRReq
|
||||
writeUint32LE(out, q.TimeLimitMinutes*60*30) // +0x20 questTime (frames at 30Hz)
|
||||
writeUint32LE(out, q.Map) // +0x24 questMap
|
||||
writeUint32LE(out, questStringsTablePtr) // +0x28 questStringsPtr
|
||||
writeUint16LE(out, 0) // +0x2C unknown
|
||||
writeUint16LE(out, q.QuestID) // +0x2E questID
|
||||
mainStart := qb.out.Len()
|
||||
qb.out.WriteByte(0) // +0x00 unknown
|
||||
qb.out.WriteByte(0) // +0x01 musicMode
|
||||
qb.out.WriteByte(0) // +0x02 localeFlags
|
||||
qb.out.WriteByte(0) // +0x03 unknown
|
||||
qb.out.WriteByte(0) // +0x04 rankingID
|
||||
qb.out.WriteByte(0) // +0x05 unknown
|
||||
writeUint16LE(qb.out, 0) // +0x06 unknown
|
||||
writeUint16LE(qb.out, q.RankBand) // +0x08 rankBand
|
||||
writeUint16LE(qb.out, 0) // +0x0A questTypeID
|
||||
writeUint32LE(qb.out, q.Fee) // +0x0C questFee
|
||||
writeUint32LE(qb.out, q.RewardMain) // +0x10 rewardMain
|
||||
writeUint32LE(qb.out, 0) // +0x14 cartsOrReduction
|
||||
writeUint16LE(qb.out, q.RewardSubA) // +0x18 rewardA
|
||||
writeUint16LE(qb.out, 0) // +0x1A padding
|
||||
writeUint16LE(qb.out, q.RewardSubB) // +0x1C rewardB
|
||||
writeUint16LE(qb.out, q.HardHRReq) // +0x1E hardHRReq
|
||||
writeUint32LE(qb.out, q.TimeLimitMinutes*60*30) // +0x20 questTime (frames at 30Hz)
|
||||
writeUint32LE(qb.out, q.Map) // +0x24 questMap
|
||||
writeUint32LE(qb.out, questStringsTablePtr) // +0x28 questStringsPtr
|
||||
writeUint16LE(qb.out, 0) // +0x2C unknown
|
||||
writeUint16LE(qb.out, q.QuestID) // +0x2E questID
|
||||
|
||||
// +0x30 objectives[3] (8 bytes each)
|
||||
for _, obj := range []QuestObjectiveJSON{q.ObjectiveMain, q.ObjectiveSubA, q.ObjectiveSubB} {
|
||||
@@ -424,18 +604,18 @@ func CompileQuestJSON(data []byte) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out.Write(b)
|
||||
qb.out.Write(b)
|
||||
}
|
||||
|
||||
// +0x48 post-objectives fields
|
||||
out.WriteByte(0) // +0x48 unknown
|
||||
out.WriteByte(0) // +0x49 unknown
|
||||
writeUint16LE(out, 0) // +0x4A padding
|
||||
writeUint16LE(out, q.JoinRankMin) // +0x4C joinRankMin
|
||||
writeUint16LE(out, q.JoinRankMax) // +0x4E joinRankMax
|
||||
writeUint16LE(out, q.PostRankMin) // +0x50 postRankMin
|
||||
writeUint16LE(out, q.PostRankMax) // +0x52 postRankMax
|
||||
pad(out, 8) // +0x54 padding[8]
|
||||
qb.out.WriteByte(0) // +0x48 unknown
|
||||
qb.out.WriteByte(0) // +0x49 unknown
|
||||
writeUint16LE(qb.out, 0) // +0x4A padding
|
||||
writeUint16LE(qb.out, q.JoinRankMin) // +0x4C joinRankMin
|
||||
writeUint16LE(qb.out, q.JoinRankMax) // +0x4E joinRankMax
|
||||
writeUint16LE(qb.out, q.PostRankMin) // +0x50 postRankMin
|
||||
writeUint16LE(qb.out, q.PostRankMax) // +0x52 postRankMax
|
||||
pad(qb.out, 8) // +0x54 padding[8]
|
||||
|
||||
// +0x5C forced equipment (6 slots × 4 u16 = 48 bytes)
|
||||
eq := q.ForcedEquipment
|
||||
@@ -444,90 +624,88 @@ func CompileQuestJSON(data []byte) ([]byte, error) {
|
||||
}
|
||||
for _, slot := range [][4]uint16{eq.Legs, eq.Weapon, eq.Head, eq.Chest, eq.Arms, eq.Waist} {
|
||||
for _, v := range slot {
|
||||
writeUint16LE(out, v)
|
||||
writeUint16LE(qb.out, v)
|
||||
}
|
||||
}
|
||||
|
||||
// +0x8C unknown u32
|
||||
writeUint32LE(out, 0)
|
||||
writeUint32LE(qb.out, 0)
|
||||
|
||||
// +0x90 monster variants[3] + mapVariant
|
||||
out.WriteByte(0) // monsterVariants[0]
|
||||
out.WriteByte(0) // monsterVariants[1]
|
||||
out.WriteByte(0) // monsterVariants[2]
|
||||
out.WriteByte(0) // mapVariant
|
||||
qb.out.WriteByte(0) // monsterVariants[0]
|
||||
qb.out.WriteByte(0) // monsterVariants[1]
|
||||
qb.out.WriteByte(0) // monsterVariants[2]
|
||||
qb.out.WriteByte(0) // mapVariant
|
||||
|
||||
// +0x94 requiredItemType (ItemID = u16), requiredItemCount
|
||||
writeUint16LE(out, 0)
|
||||
out.WriteByte(0) // requiredItemCount
|
||||
writeUint16LE(qb.out, 0)
|
||||
qb.out.WriteByte(0) // requiredItemCount
|
||||
|
||||
// +0x97 questVariants
|
||||
out.WriteByte(q.QuestVariant1)
|
||||
out.WriteByte(q.QuestVariant2)
|
||||
out.WriteByte(q.QuestVariant3)
|
||||
out.WriteByte(q.QuestVariant4)
|
||||
qb.out.WriteByte(q.QuestVariant1)
|
||||
qb.out.WriteByte(q.QuestVariant2)
|
||||
qb.out.WriteByte(q.QuestVariant3)
|
||||
qb.out.WriteByte(q.QuestVariant4)
|
||||
|
||||
// +0x9B padding[5]
|
||||
pad(out, 5)
|
||||
pad(qb.out, 5)
|
||||
|
||||
// +0xA0 allowedEquipBitmask, points
|
||||
writeUint32LE(out, 0) // allowedEquipBitmask
|
||||
writeUint32LE(out, 0) // mainPoints
|
||||
writeUint32LE(out, 0) // subAPoints
|
||||
writeUint32LE(out, 0) // subBPoints
|
||||
writeUint32LE(qb.out, 0) // allowedEquipBitmask
|
||||
writeUint32LE(qb.out, 0) // mainPoints
|
||||
writeUint32LE(qb.out, 0) // subAPoints
|
||||
writeUint32LE(qb.out, 0) // subBPoints
|
||||
|
||||
// +0xB0 rewardItems[3] (ItemID = u16, 3 items = 6 bytes)
|
||||
pad(out, 6)
|
||||
pad(qb.out, 6)
|
||||
|
||||
// +0xB6 interception section (non-SlayAll path: padding[3] + MonsterID[1] = 4 bytes)
|
||||
pad(out, 4)
|
||||
pad(qb.out, 4)
|
||||
|
||||
// +0xBA padding[0xA] = 10 bytes
|
||||
pad(out, 10)
|
||||
pad(qb.out, 10)
|
||||
|
||||
// +0xC4 questClearsAllowed
|
||||
writeUint32LE(out, 0)
|
||||
writeUint32LE(qb.out, 0)
|
||||
|
||||
// +0xC8 = 200 bytes so far for documented fields. ZZ body = 320 bytes.
|
||||
// Zero-pad the remaining unknown ZZ-specific fields.
|
||||
writtenInMain := out.Len() - mainStart
|
||||
writtenInMain := qb.out.Len() - mainStart
|
||||
if writtenInMain < mainPropSize {
|
||||
pad(out, mainPropSize-writtenInMain)
|
||||
pad(qb.out, mainPropSize-writtenInMain)
|
||||
} else if writtenInMain > mainPropSize {
|
||||
return nil, fmt.Errorf("mainQuestProperties overflowed: wrote %d, max %d", writtenInMain, mainPropSize)
|
||||
}
|
||||
|
||||
if out.Len() != int(questTypeFlagsPtr)+mainPropSize {
|
||||
return nil, fmt.Errorf("main prop end mismatch: at %d, want %d", out.Len(), int(questTypeFlagsPtr)+mainPropSize)
|
||||
if qb.out.Len() != int(questTypeFlagsPtr)+mainPropSize {
|
||||
return nil, fmt.Errorf("main prop end mismatch: at %d, want %d", qb.out.Len(), int(questTypeFlagsPtr)+mainPropSize)
|
||||
}
|
||||
|
||||
// ── QuestText pointer table (32 bytes) ───────────────────────────────
|
||||
for _, ptr := range stringPtrs {
|
||||
writeUint32LE(out, ptr)
|
||||
writeUint32LE(qb.out, ptr)
|
||||
}
|
||||
|
||||
// ── String data ──────────────────────────────────────────────────────
|
||||
for _, s := range sjisStrings {
|
||||
out.Write(s)
|
||||
qb.out.Write(s)
|
||||
}
|
||||
|
||||
// Pad to afterStrings alignment.
|
||||
for uint32(out.Len()) < afterStrings {
|
||||
out.WriteByte(0)
|
||||
for uint32(qb.out.Len()) < afterStrings {
|
||||
qb.out.WriteByte(0)
|
||||
}
|
||||
|
||||
// ── Stages ───────────────────────────────────────────────────────────
|
||||
// Each Stage: u32 stageID + 12 bytes padding = 16 bytes.
|
||||
for _, st := range q.Stages {
|
||||
writeUint32LE(out, st.StageID)
|
||||
pad(out, 12)
|
||||
writeUint32LE(qb.out, st.StageID)
|
||||
pad(qb.out, 12)
|
||||
}
|
||||
for uint32(out.Len()) < afterStages {
|
||||
out.WriteByte(0)
|
||||
for uint32(qb.out.Len()) < afterStages {
|
||||
qb.out.WriteByte(0)
|
||||
}
|
||||
|
||||
// ── Supply Box ───────────────────────────────────────────────────────
|
||||
// Three sections: main (24 slots), subA (8 slots), subB (8 slots).
|
||||
type slot struct {
|
||||
items []QuestSupplyItemJSON
|
||||
max int
|
||||
@@ -542,29 +720,323 @@ func CompileQuestJSON(data []byte) ([]byte, error) {
|
||||
if written >= section.max {
|
||||
break
|
||||
}
|
||||
writeUint16LE(out, item.Item)
|
||||
writeUint16LE(out, item.Quantity)
|
||||
writeUint16LE(qb.out, item.Item)
|
||||
writeUint16LE(qb.out, item.Quantity)
|
||||
written++
|
||||
}
|
||||
// Pad remaining slots with zeros.
|
||||
for written < section.max {
|
||||
writeUint32LE(out, 0)
|
||||
writeUint32LE(qb.out, 0)
|
||||
written++
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reward Tables ────────────────────────────────────────────────────
|
||||
// Written immediately after the supply box (at rewardPtr), then padded
|
||||
// to 4-byte alignment before the monster spawn list.
|
||||
out.Write(rewardBuf)
|
||||
for uint32(out.Len()) < largeMonsterPtr {
|
||||
out.WriteByte(0)
|
||||
qb.out.Write(rewardBuf)
|
||||
for uint32(qb.out.Len()) < largeMonsterPtr {
|
||||
qb.out.WriteByte(0)
|
||||
}
|
||||
|
||||
// ── Large Monster Spawns ─────────────────────────────────────────────
|
||||
out.Write(monsterBuf)
|
||||
qb.out.Write(monsterBuf)
|
||||
for uint32(qb.out.Len()) < afterMonsters {
|
||||
qb.out.WriteByte(0)
|
||||
}
|
||||
|
||||
return out.Bytes(), nil
|
||||
// ── Variable sections: map sections, area mappings, transitions, etc. ──
|
||||
// All written at afterMonsters and beyond, pointers patched into header.
|
||||
|
||||
// ── Map Sections (questAreaPtr) ──────────────────────────────────────
|
||||
// Layout:
|
||||
// u32 ptr[0], u32 ptr[1], ..., u32(0) terminator
|
||||
// For each section:
|
||||
// mapSection: u32 loadedStage, u32 unk, u32 spawnTypesPtr, u32 spawnStatsPtr
|
||||
// u32(0) gap, u16 unk (= 6 bytes after mapSection)
|
||||
// spawnTypes data: (MonsterID u8 + pad[3]) per entry, terminated by 0xFFFF
|
||||
// spawnStats data: MinionSpawn (60 bytes) per entry, terminated by 0xFFFF
|
||||
if len(q.MapSections) > 0 {
|
||||
questAreaOff := qb.out.Len()
|
||||
qb.patchValue(hdrQuestAreaOff, uint32(questAreaOff))
|
||||
|
||||
// Write pointer array (one u32 per section + terminator).
|
||||
sectionPtrOffs := make([]int, len(q.MapSections))
|
||||
for i := range q.MapSections {
|
||||
sectionPtrOffs[i] = qb.reserve()
|
||||
}
|
||||
writeUint32LE(qb.out, 0) // terminator
|
||||
|
||||
// Write each mapSection block.
|
||||
type sectionPtrs struct {
|
||||
spawnTypesOff int
|
||||
spawnStatsOff int
|
||||
}
|
||||
internalPtrs := make([]sectionPtrs, len(q.MapSections))
|
||||
|
||||
for i, ms := range q.MapSections {
|
||||
// Patch the pointer-array entry to point here.
|
||||
qb.patch(sectionPtrOffs[i])
|
||||
|
||||
// mapSection: loadedStage, unk, spawnTypesPtr, spawnStatsPtr
|
||||
writeUint32LE(qb.out, ms.LoadedStage)
|
||||
writeUint32LE(qb.out, 0) // unk
|
||||
internalPtrs[i].spawnTypesOff = qb.reserve()
|
||||
internalPtrs[i].spawnStatsOff = qb.reserve()
|
||||
|
||||
// u32(0) gap + u16 unk immediately after the 16-byte mapSection.
|
||||
writeUint32LE(qb.out, 0)
|
||||
writeUint16LE(qb.out, 0)
|
||||
}
|
||||
|
||||
// Write spawn data for each section.
|
||||
for i, ms := range q.MapSections {
|
||||
// spawnTypes: varPaddT<MonsterID,3> = u8 monster + pad[3] per entry.
|
||||
// Terminated by first 2 bytes == 0xFFFF.
|
||||
qb.patch(internalPtrs[i].spawnTypesOff)
|
||||
for _, monID := range ms.SpawnMonsters {
|
||||
qb.out.WriteByte(monID)
|
||||
pad(qb.out, 3)
|
||||
}
|
||||
writeUint16LE(qb.out, 0xFFFF) // terminator
|
||||
|
||||
// Align to 4 bytes before spawnStats.
|
||||
for qb.out.Len()%4 != 0 {
|
||||
qb.out.WriteByte(0)
|
||||
}
|
||||
|
||||
// spawnStats: MinionSpawn per entry (60 bytes), terminated by 0xFFFF.
|
||||
qb.patch(internalPtrs[i].spawnStatsOff)
|
||||
for _, ms2 := range ms.MinionSpawns {
|
||||
qb.out.WriteByte(ms2.Monster)
|
||||
qb.out.WriteByte(0) // padding[1]
|
||||
writeUint16LE(qb.out, ms2.SpawnToggle) // spawnToggle
|
||||
writeUint32LE(qb.out, ms2.SpawnAmount) // spawnAmount
|
||||
writeUint32LE(qb.out, 0) // unk u32
|
||||
pad(qb.out, 0x10) // padding[16]
|
||||
writeUint32LE(qb.out, 0) // unk u32
|
||||
writeFloat32LE(qb.out, ms2.X)
|
||||
writeFloat32LE(qb.out, ms2.Y)
|
||||
writeFloat32LE(qb.out, ms2.Z)
|
||||
pad(qb.out, 0x10) // padding[16]
|
||||
}
|
||||
writeUint16LE(qb.out, 0xFFFF) // terminator
|
||||
|
||||
// Align for next section.
|
||||
for qb.out.Len()%4 != 0 {
|
||||
qb.out.WriteByte(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Area Mappings (areaMappingPtr) ────────────────────────────────────
|
||||
// Written BEFORE area transitions so the parser can use
|
||||
// "read until areaTransitionsPtr" to know the count.
|
||||
// Layout: AreaMappings[n] × 32 bytes each, back-to-back.
|
||||
// float area_xPos, float area_zPos, pad[8],
|
||||
// float base_xPos, float base_zPos, float kn_Pos, pad[4]
|
||||
if len(q.AreaMappings) > 0 {
|
||||
areaMappingOff := qb.out.Len()
|
||||
qb.patchValue(hdrAreaMappingOff, uint32(areaMappingOff))
|
||||
|
||||
for _, am := range q.AreaMappings {
|
||||
writeFloat32LE(qb.out, am.AreaX)
|
||||
writeFloat32LE(qb.out, am.AreaZ)
|
||||
pad(qb.out, 8)
|
||||
writeFloat32LE(qb.out, am.BaseX)
|
||||
writeFloat32LE(qb.out, am.BaseZ)
|
||||
writeFloat32LE(qb.out, am.KnPos)
|
||||
pad(qb.out, 4)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Area Transitions (areaTransitionsPtr) ─────────────────────────────
|
||||
// Layout: playerAreaChange[area1Zones] = u32 ptr per zone.
|
||||
// Then floatSet arrays for each zone with transitions.
|
||||
if numZones > 0 {
|
||||
areaTransOff := qb.out.Len()
|
||||
qb.patchValue(hdrAreaTransOff, uint32(areaTransOff))
|
||||
|
||||
// Write pointer array.
|
||||
zonePtrOffs := make([]int, numZones)
|
||||
for i := range q.AreaTransitions {
|
||||
zonePtrOffs[i] = qb.reserve()
|
||||
}
|
||||
|
||||
// Write floatSet arrays for non-empty zones.
|
||||
for i, zone := range q.AreaTransitions {
|
||||
if len(zone.Transitions) == 0 {
|
||||
// Null pointer — leave as 0.
|
||||
continue
|
||||
}
|
||||
qb.patch(zonePtrOffs[i])
|
||||
for _, tr := range zone.Transitions {
|
||||
writeInt16LE(qb.out, tr.TargetStageID1)
|
||||
writeInt16LE(qb.out, tr.StageVariant)
|
||||
writeFloat32LE(qb.out, tr.CurrentX)
|
||||
writeFloat32LE(qb.out, tr.CurrentY)
|
||||
writeFloat32LE(qb.out, tr.CurrentZ)
|
||||
for _, f := range tr.TransitionBox {
|
||||
writeFloat32LE(qb.out, f)
|
||||
}
|
||||
writeFloat32LE(qb.out, tr.TargetX)
|
||||
writeFloat32LE(qb.out, tr.TargetY)
|
||||
writeFloat32LE(qb.out, tr.TargetZ)
|
||||
for _, r := range tr.TargetRotation {
|
||||
writeInt16LE(qb.out, r)
|
||||
}
|
||||
}
|
||||
// Terminate with s16(-1).
|
||||
writeInt16LE(qb.out, -1)
|
||||
// Align.
|
||||
for qb.out.Len()%4 != 0 {
|
||||
qb.out.WriteByte(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Map Info (mapInfoPtr) ─────────────────────────────────────────────
|
||||
if q.MapInfo != nil {
|
||||
mapInfoOff := qb.out.Len()
|
||||
qb.patchValue(hdrMapInfoOff, uint32(mapInfoOff))
|
||||
writeUint32LE(qb.out, q.MapInfo.MapID)
|
||||
writeUint32LE(qb.out, q.MapInfo.ReturnBCID)
|
||||
}
|
||||
|
||||
// ── Gathering Points (gatheringPointsPtr) ─────────────────────────────
|
||||
// Layout: ptGatheringPoint[area1Zones] = u32 ptr per zone.
|
||||
// Each non-null ptr points to gatheringPoint[4] terminated by xPos=-1.0.
|
||||
if numZones > 0 && len(q.GatheringPoints) > 0 {
|
||||
gatherPtsOff := qb.out.Len()
|
||||
qb.patchValue(hdrGatherPtsOff, uint32(gatherPtsOff))
|
||||
|
||||
// Write pointer array.
|
||||
gpPtrOffs := make([]int, numZones)
|
||||
for i := range q.GatheringPoints {
|
||||
gpPtrOffs[i] = qb.reserve()
|
||||
}
|
||||
|
||||
// Write gathering point arrays for non-empty zones.
|
||||
for i, zone := range q.GatheringPoints {
|
||||
if len(zone.Points) == 0 {
|
||||
continue
|
||||
}
|
||||
qb.patch(gpPtrOffs[i])
|
||||
for _, gp := range zone.Points {
|
||||
writeFloat32LE(qb.out, gp.X)
|
||||
writeFloat32LE(qb.out, gp.Y)
|
||||
writeFloat32LE(qb.out, gp.Z)
|
||||
writeFloat32LE(qb.out, gp.Range)
|
||||
writeUint16LE(qb.out, gp.GatheringID)
|
||||
writeUint16LE(qb.out, gp.MaxCount)
|
||||
pad(qb.out, 2)
|
||||
writeUint16LE(qb.out, gp.MinCount)
|
||||
}
|
||||
// Terminator: xPos == -1.0 (0xBF800000).
|
||||
writeFloat32LE(qb.out, float32(math.Float32frombits(0xBF800000)))
|
||||
// Pad terminator entry to 24 bytes total (only wrote 4).
|
||||
pad(qb.out, 20)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Area Facilities (areaFacilitiesPtr) ───────────────────────────────
|
||||
// Layout: ptVar<facPointBlock>[area1Zones] = u32 ptr per zone.
|
||||
// Each non-null ptr points to a facPointBlock.
|
||||
// facPoint: pad[2] + SpecAc(u16) + xPos + yPos + zPos + range + id(u16) + pad[2] = 24 bytes
|
||||
// facPointBlock: facPoints[] terminated by (xPos-at-$+4 == 0xBF800000) + pad[0xC] + float + float
|
||||
// Terminator layout: write pad[2]+type[2] then float32(-1.0) to trigger termination,
|
||||
// then block footer: pad[0xC] + float(0) + float(0).
|
||||
if numZones > 0 && len(q.AreaFacilities) > 0 {
|
||||
facOff := qb.out.Len()
|
||||
qb.patchValue(hdrFacilitiesOff, uint32(facOff))
|
||||
|
||||
facPtrOffs := make([]int, numZones)
|
||||
for i := range q.AreaFacilities {
|
||||
facPtrOffs[i] = qb.reserve()
|
||||
}
|
||||
|
||||
for i, zone := range q.AreaFacilities {
|
||||
if len(zone.Points) == 0 {
|
||||
continue
|
||||
}
|
||||
qb.patch(facPtrOffs[i])
|
||||
|
||||
for _, fp := range zone.Points {
|
||||
pad(qb.out, 2) // pad[2]
|
||||
writeUint16LE(qb.out, fp.Type) // SpecAc type
|
||||
writeFloat32LE(qb.out, fp.X)
|
||||
writeFloat32LE(qb.out, fp.Y)
|
||||
writeFloat32LE(qb.out, fp.Z)
|
||||
writeFloat32LE(qb.out, fp.Range)
|
||||
writeUint16LE(qb.out, fp.ID)
|
||||
pad(qb.out, 2) // pad[2]
|
||||
}
|
||||
|
||||
// Terminator: the while condition checks read_unsigned($+4,4).
|
||||
// Write 4 bytes header (pad[2]+type[2]) then float32(-1.0).
|
||||
pad(qb.out, 2)
|
||||
writeUint16LE(qb.out, 0)
|
||||
writeFloat32LE(qb.out, float32(math.Float32frombits(0xBF800000)))
|
||||
|
||||
// Block footer: padding[0xC] + float(0) + float(0) = 20 bytes.
|
||||
pad(qb.out, 0xC)
|
||||
writeFloat32LE(qb.out, 0)
|
||||
writeFloat32LE(qb.out, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Some Strings (someStringsPtr / unk30) ─────────────────────────────
|
||||
// Layout at unk30: ptr someStringPtr, ptr questTypePtr (8 bytes),
|
||||
// then the string data.
|
||||
hasSomeStrings := q.SomeString != "" || q.QuestType != ""
|
||||
if hasSomeStrings {
|
||||
someStringsOff := qb.out.Len()
|
||||
qb.patchValue(hdrSomeStringsOff, uint32(someStringsOff))
|
||||
|
||||
// Two pointer slots.
|
||||
someStrPtrOff := qb.reserve()
|
||||
questTypePtrOff := qb.reserve()
|
||||
|
||||
if q.SomeString != "" {
|
||||
qb.patch(someStrPtrOff)
|
||||
b, err := toShiftJIS(q.SomeString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
qb.out.Write(b)
|
||||
}
|
||||
|
||||
if q.QuestType != "" {
|
||||
qb.patch(questTypePtrOff)
|
||||
b, err := toShiftJIS(q.QuestType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
qb.out.Write(b)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gathering Tables (gatheringTablesPtr) ─────────────────────────────
|
||||
// Layout: ptVar<gatheringTable>[gatheringTablesQty] = u32 ptr per table.
|
||||
// Each ptr points to GatherItem[] terminated by u16(0xFFFF).
|
||||
// GatherItem: u16 rate + u16 item = 4 bytes.
|
||||
if numGatheringTables > 0 {
|
||||
gatherTablesOff := qb.out.Len()
|
||||
qb.patchValue(hdrGatherTablesOff, uint32(gatherTablesOff))
|
||||
|
||||
tblPtrOffs := make([]int, numGatheringTables)
|
||||
for i := range q.GatheringTables {
|
||||
tblPtrOffs[i] = qb.reserve()
|
||||
}
|
||||
|
||||
for i, tbl := range q.GatheringTables {
|
||||
qb.patch(tblPtrOffs[i])
|
||||
for _, item := range tbl.Items {
|
||||
writeUint16LE(qb.out, item.Rate)
|
||||
writeUint16LE(qb.out, item.Item)
|
||||
}
|
||||
writeUint16LE(qb.out, 0xFFFF) // terminator
|
||||
}
|
||||
}
|
||||
|
||||
return qb.out.Bytes(), nil
|
||||
}
|
||||
|
||||
// buildRewardTables serialises the reward table array and all reward item lists.
|
||||
@@ -587,7 +1059,6 @@ func buildRewardTables(tables []QuestRewardTableJSON) []byte {
|
||||
|
||||
for _, t := range tables {
|
||||
// tableOffset is relative to the start of rewardPtr in the file.
|
||||
// We compute it as headerArraySize + offset into itemData.
|
||||
tableOffset := headerArraySize + uint32(itemData.Len())
|
||||
|
||||
headers.WriteByte(t.TableID)
|
||||
|
||||
Reference in New Issue
Block a user