Files
Erupe/server/channelserver/rengoku_build.go
Houmgaor 34335b023d feat(rengoku): support rengoku_data.json as editable config source
Operators can now define Hunting Road configuration in a plain JSON file
(rengoku_data.json) instead of maintaining an opaque pre-encrypted binary.
The JSON is parsed, validated, assembled into the binary layout, and
ECD-encrypted at startup; rengoku_data.bin is still used as a fallback.

JSON schema covers both road modes (multi/solo) with typed floor and
spawn-table entries — floor number, spawn-table index, point multipliers,
and per-slot monster ID/variant/weighting fields. Out-of-range references
are caught at load time before any bytes are written.
2026-03-20 00:07:34 +01:00

271 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package channelserver
/*
JSON-based rengoku_data.bin builder.
Operators can place rengoku_data.json in the bin/ directory instead of
(or alongside) rengoku_data.bin. When the JSON file is found it takes
precedence: it is parsed, validated, assembled into the raw binary layout,
and ECD-encrypted before being cached. The .bin file is used as a fallback.
Binary layout produced by BuildRengokuBinary:
0x000x13 header (20 bytes: magic + version + zeros)
0x140x2B multiDef RoadMode (24 bytes)
0x2C0x43 soloDef RoadMode (24 bytes)
-- multi road data --
floorStats[] (floorStatsCount × 24 bytes)
spawnTablePtrs[] (spawnTablePtrCount × 4 bytes)
spawnCountPtrs[] (spawnTablePtrCount × 4 bytes, zeroed)
spawnTables[] (spawnTablePtrCount × 32 bytes)
-- solo road data -- (same sub-layout)
*/
import (
"encoding/binary"
"encoding/json"
"fmt"
"math"
"os"
"path/filepath"
"erupe-ce/common/decryption"
"go.uber.org/zap"
)
// ─── JSON schema ────────────────────────────────────────────────────────────
// RengokuConfig is the top-level JSON structure for rengoku_data.json.
type RengokuConfig struct {
MultiRoad RoadConfig `json:"multi_road"`
SoloRoad RoadConfig `json:"solo_road"`
}
// RoadConfig describes one road mode (multi or solo) with its floors and
// spawn tables. Floors reference spawn tables by zero-based index.
type RoadConfig struct {
Floors []FloorConfig `json:"floors"`
SpawnTables []SpawnTableConfig `json:"spawn_tables"`
}
// FloorConfig describes one floor within a road mode.
//
// - SpawnTableIndex: zero-based index into this road's SpawnTables slice,
// selecting which monster configuration is active on this floor.
// - PointMulti1/2: point multipliers applied to rewards on this floor.
// - FinalLoop: non-zero on the last floor of a loop cycle.
type FloorConfig struct {
FloorNumber uint32 `json:"floor_number"`
SpawnTableIndex uint32 `json:"spawn_table_index"`
Unk0 uint32 `json:"unk0,omitempty"`
PointMulti1 float32 `json:"point_multi_1"`
PointMulti2 float32 `json:"point_multi_2"`
FinalLoop uint32 `json:"final_loop,omitempty"`
}
// SpawnTableConfig describes the two monsters that appear together on a floor.
type SpawnTableConfig struct {
Monster1ID uint32 `json:"monster1_id"`
Monster1Variant uint32 `json:"monster1_variant,omitempty"`
Monster2ID uint32 `json:"monster2_id"`
Monster2Variant uint32 `json:"monster2_variant,omitempty"`
StatTable uint32 `json:"stat_table,omitempty"`
MapZoneOverride uint32 `json:"map_zone_override,omitempty"`
SpawnWeighting uint32 `json:"spawn_weighting,omitempty"`
AdditionalFlag uint32 `json:"additional_flag,omitempty"`
}
// ─── Builder ─────────────────────────────────────────────────────────────────
// BuildRengokuBinary assembles a raw (unencrypted, uncompressed) rengoku
// binary from a RengokuConfig. The result can be passed to EncodeECD and
// served directly to clients.
func BuildRengokuBinary(cfg RengokuConfig) ([]byte, error) {
if err := validateRengokuConfig(cfg); err != nil {
return nil, err
}
// ── Offset plan ──────────────────────────────────────────────────────────
// Fixed regions: header (0x14) + two RoadModes (2×24) = 0x44
const dataStart = uint32(rengokuMinSize) // 0x44
// Multi road sections
mFloorOff := dataStart
mFloorSz := uint32(len(cfg.MultiRoad.Floors)) * floorStatsByteSize
mPtrsOff := mFloorOff + mFloorSz
mPtrsSz := uint32(len(cfg.MultiRoad.SpawnTables)) * spawnPtrEntrySize
mCntOff := mPtrsOff + mPtrsSz
mCntSz := uint32(len(cfg.MultiRoad.SpawnTables)) * spawnPtrEntrySize
mTablesOff := mCntOff + mCntSz
mTablesSz := uint32(len(cfg.MultiRoad.SpawnTables)) * spawnTableByteSize
// Solo road sections (appended directly after multi)
sFloorOff := mTablesOff + mTablesSz
sFloorSz := uint32(len(cfg.SoloRoad.Floors)) * floorStatsByteSize
sPtrsOff := sFloorOff + sFloorSz
sPtrsSz := uint32(len(cfg.SoloRoad.SpawnTables)) * spawnPtrEntrySize
sCntOff := sPtrsOff + sPtrsSz
sCntSz := uint32(len(cfg.SoloRoad.SpawnTables)) * spawnPtrEntrySize
sTablesOff := sCntOff + sCntSz
sTablesSz := uint32(len(cfg.SoloRoad.SpawnTables)) * spawnTableByteSize
totalSize := sTablesOff + sTablesSz
buf := make([]byte, totalSize)
// ── Header ───────────────────────────────────────────────────────────────
buf[0], buf[1], buf[2], buf[3] = 'r', 'e', 'f', 0x1A
buf[4] = 1 // version
le := binary.LittleEndian
// ── RoadMode structs ─────────────────────────────────────────────────────
writeRoadMode(buf, 0x14, le, RoadModeFields{
FloorCount: uint32(len(cfg.MultiRoad.Floors)),
SpawnCount: uint32(len(cfg.MultiRoad.SpawnTables)),
TablePtrCnt: uint32(len(cfg.MultiRoad.SpawnTables)),
FloorPtr: mFloorOff,
TablePtrsPtr: mPtrsOff,
CountPtrsPtr: mCntOff,
})
writeRoadMode(buf, 0x2C, le, RoadModeFields{
FloorCount: uint32(len(cfg.SoloRoad.Floors)),
SpawnCount: uint32(len(cfg.SoloRoad.SpawnTables)),
TablePtrCnt: uint32(len(cfg.SoloRoad.SpawnTables)),
FloorPtr: sFloorOff,
TablePtrsPtr: sPtrsOff,
CountPtrsPtr: sCntOff,
})
// ── Data sections ────────────────────────────────────────────────────────
writeFloors(buf, cfg.MultiRoad.Floors, mFloorOff, le)
writeSpawnSection(buf, cfg.MultiRoad.SpawnTables, mPtrsOff, mTablesOff, le)
writeFloors(buf, cfg.SoloRoad.Floors, sFloorOff, le)
writeSpawnSection(buf, cfg.SoloRoad.SpawnTables, sPtrsOff, sTablesOff, le)
return buf, nil
}
// RoadModeFields carries the computed field values for one RoadMode struct.
type RoadModeFields struct {
FloorCount, SpawnCount, TablePtrCnt uint32
FloorPtr, TablePtrsPtr, CountPtrsPtr uint32
}
func writeRoadMode(buf []byte, offset int, le binary.ByteOrder, f RoadModeFields) {
le.PutUint32(buf[offset:], f.FloorCount)
le.PutUint32(buf[offset+4:], f.SpawnCount)
le.PutUint32(buf[offset+8:], f.TablePtrCnt)
le.PutUint32(buf[offset+12:], f.FloorPtr)
le.PutUint32(buf[offset+16:], f.TablePtrsPtr)
le.PutUint32(buf[offset+20:], f.CountPtrsPtr)
}
func writeFloors(buf []byte, floors []FloorConfig, base uint32, le binary.ByteOrder) {
for i, f := range floors {
off := base + uint32(i)*floorStatsByteSize
le.PutUint32(buf[off:], f.FloorNumber)
le.PutUint32(buf[off+4:], f.SpawnTableIndex)
le.PutUint32(buf[off+8:], f.Unk0)
le.PutUint32(buf[off+12:], math.Float32bits(f.PointMulti1))
le.PutUint32(buf[off+16:], math.Float32bits(f.PointMulti2))
le.PutUint32(buf[off+20:], f.FinalLoop)
}
}
func writeSpawnSection(buf []byte, tables []SpawnTableConfig, ptrsBase, tablesBase uint32, le binary.ByteOrder) {
for i, t := range tables {
tableOff := tablesBase + uint32(i)*spawnTableByteSize
// Pointer entry
le.PutUint32(buf[ptrsBase+uint32(i)*spawnPtrEntrySize:], tableOff)
// SpawnTable (32 bytes)
le.PutUint32(buf[tableOff:], t.Monster1ID)
le.PutUint32(buf[tableOff+4:], t.Monster1Variant)
le.PutUint32(buf[tableOff+8:], t.Monster2ID)
le.PutUint32(buf[tableOff+12:], t.Monster2Variant)
le.PutUint32(buf[tableOff+16:], t.StatTable)
le.PutUint32(buf[tableOff+20:], t.MapZoneOverride)
le.PutUint32(buf[tableOff+24:], t.SpawnWeighting)
le.PutUint32(buf[tableOff+28:], t.AdditionalFlag)
}
}
// validateRengokuConfig checks that all spawn_table_index references are
// within range for both road modes.
func validateRengokuConfig(cfg RengokuConfig) error {
for _, road := range []struct {
name string
r RoadConfig
}{{"multi_road", cfg.MultiRoad}, {"solo_road", cfg.SoloRoad}} {
n := len(road.r.SpawnTables)
for i, f := range road.r.Floors {
if int(f.SpawnTableIndex) >= n {
return fmt.Errorf("rengoku: %s floor %d: spawn_table_index %d out of range (have %d tables)",
road.name, i, f.SpawnTableIndex, n)
}
}
}
return nil
}
// ─── Shared helper ───────────────────────────────────────────────────────────
// encodeRengokuECD wraps decryption.EncodeECD with error logging.
func encodeRengokuECD(raw []byte, logger *zap.Logger) ([]byte, error) {
enc, err := decryption.EncodeECD(raw, decryption.DefaultECDKey)
if err != nil {
logger.Error("rengoku: ECD encryption failed", zap.Error(err))
}
return enc, err
}
// ─── JSON loader ─────────────────────────────────────────────────────────────
// loadRengokuFromJSON attempts to load rengoku configuration from
// rengoku_data.json in binPath. It returns the ECD-encrypted binary ready for
// caching, or nil if the file is absent or cannot be processed.
func loadRengokuFromJSON(binPath string, logger *zap.Logger) []byte {
path := filepath.Join(binPath, "rengoku_data.json")
raw, err := os.ReadFile(path)
if err != nil {
return nil // file absent — not an error
}
var cfg RengokuConfig
if err := json.Unmarshal(raw, &cfg); err != nil {
logger.Error("rengoku_data.json: JSON parse error",
zap.String("path", path), zap.Error(err))
return nil
}
bin, err := BuildRengokuBinary(cfg)
if err != nil {
logger.Error("rengoku_data.json: binary build failed",
zap.String("path", path), zap.Error(err))
return nil
}
// Validate the freshly built binary (should always pass, but good to confirm).
info, parseErr := parseRengokuBinary(bin)
if parseErr != nil {
logger.Error("rengoku_data.json: structural validation of built binary failed",
zap.String("path", path), zap.Error(parseErr))
return nil
}
enc, err := encodeRengokuECD(bin, logger)
if err != nil {
return nil
}
logger.Info("Hunting Road config (from JSON)",
zap.Int("multi_floors", info.MultiFloors),
zap.Int("multi_spawn_tables", info.MultiSpawnTables),
zap.Int("solo_floors", info.SoloFloors),
zap.Int("solo_spawn_tables", info.SoloSpawnTables),
zap.Int("unique_monsters", info.UniqueMonsters),
)
logger.Info("Loaded rengoku_data.json", zap.Int("bytes", len(enc)))
return enc
}