mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-26 09:33:02 +01:00
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.
This commit is contained in:
270
server/channelserver/rengoku_build.go
Normal file
270
server/channelserver/rengoku_build.go
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
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:
|
||||||
|
0x00–0x13 header (20 bytes: magic + version + zeros)
|
||||||
|
0x14–0x2B multiDef RoadMode (24 bytes)
|
||||||
|
0x2C–0x43 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
|
||||||
|
}
|
||||||
217
server/channelserver/rengoku_build_test.go
Normal file
217
server/channelserver/rengoku_build_test.go
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
package channelserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sampleRengokuConfig returns a small but complete RengokuConfig for tests.
|
||||||
|
func sampleRengokuConfig() RengokuConfig {
|
||||||
|
spawnTables := []SpawnTableConfig{
|
||||||
|
{Monster1ID: 101, Monster1Variant: 0, Monster2ID: 102, Monster2Variant: 1,
|
||||||
|
StatTable: 3, SpawnWeighting: 10},
|
||||||
|
{Monster1ID: 103, Monster1Variant: 2, Monster2ID: 104, Monster2Variant: 0,
|
||||||
|
SpawnWeighting: 20},
|
||||||
|
}
|
||||||
|
floors := []FloorConfig{
|
||||||
|
{FloorNumber: 1, SpawnTableIndex: 0, PointMulti1: 1.0, PointMulti2: 1.5},
|
||||||
|
{FloorNumber: 2, SpawnTableIndex: 1, PointMulti1: 1.2, PointMulti2: 2.0},
|
||||||
|
{FloorNumber: 3, SpawnTableIndex: 0, PointMulti1: 1.5, PointMulti2: 2.5, FinalLoop: 1},
|
||||||
|
}
|
||||||
|
soloFloors := []FloorConfig{
|
||||||
|
{FloorNumber: 1, SpawnTableIndex: 0, PointMulti1: 1.0, PointMulti2: 1.5},
|
||||||
|
{FloorNumber: 2, SpawnTableIndex: 0, PointMulti1: 1.2, PointMulti2: 2.0},
|
||||||
|
}
|
||||||
|
return RengokuConfig{
|
||||||
|
MultiRoad: RoadConfig{Floors: floors, SpawnTables: spawnTables},
|
||||||
|
SoloRoad: RoadConfig{Floors: soloFloors, SpawnTables: spawnTables[1:]},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBuildRengokuBinary_RoundTrip builds a binary from a config and verifies
|
||||||
|
// that parseRengokuBinary accepts it and reports the expected summary.
|
||||||
|
func TestBuildRengokuBinary_RoundTrip(t *testing.T) {
|
||||||
|
cfg := sampleRengokuConfig()
|
||||||
|
|
||||||
|
bin, err := BuildRengokuBinary(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildRengokuBinary: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := parseRengokuBinary(bin)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseRengokuBinary on built binary: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.MultiFloors != len(cfg.MultiRoad.Floors) {
|
||||||
|
t.Errorf("MultiFloors = %d, want %d", info.MultiFloors, len(cfg.MultiRoad.Floors))
|
||||||
|
}
|
||||||
|
if info.MultiSpawnTables != len(cfg.MultiRoad.SpawnTables) {
|
||||||
|
t.Errorf("MultiSpawnTables = %d, want %d", info.MultiSpawnTables, len(cfg.MultiRoad.SpawnTables))
|
||||||
|
}
|
||||||
|
if info.SoloFloors != len(cfg.SoloRoad.Floors) {
|
||||||
|
t.Errorf("SoloFloors = %d, want %d", info.SoloFloors, len(cfg.SoloRoad.Floors))
|
||||||
|
}
|
||||||
|
if info.SoloSpawnTables != len(cfg.SoloRoad.SpawnTables) {
|
||||||
|
t.Errorf("SoloSpawnTables = %d, want %d", info.SoloSpawnTables, len(cfg.SoloRoad.SpawnTables))
|
||||||
|
}
|
||||||
|
// Unique monsters: multi has 101,102,103,104; solo has 103,104 → 4 total
|
||||||
|
if info.UniqueMonsters != 4 {
|
||||||
|
t.Errorf("UniqueMonsters = %d, want 4", info.UniqueMonsters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBuildRengokuBinary_FloatFields verifies that PointMulti1/2 values
|
||||||
|
// survive the binary encoding intact.
|
||||||
|
func TestBuildRengokuBinary_FloatFields(t *testing.T) {
|
||||||
|
cfg := RengokuConfig{
|
||||||
|
MultiRoad: RoadConfig{
|
||||||
|
Floors: []FloorConfig{
|
||||||
|
{FloorNumber: 1, SpawnTableIndex: 0, PointMulti1: 1.25, PointMulti2: 3.75},
|
||||||
|
},
|
||||||
|
SpawnTables: []SpawnTableConfig{{Monster1ID: 1}},
|
||||||
|
},
|
||||||
|
SoloRoad: RoadConfig{
|
||||||
|
Floors: []FloorConfig{{FloorNumber: 1, SpawnTableIndex: 0}},
|
||||||
|
SpawnTables: []SpawnTableConfig{{Monster1ID: 2}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
bin, err := BuildRengokuBinary(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildRengokuBinary: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-parse the binary and check that we can read back the float fields.
|
||||||
|
// The floor stats for multiDef start at rengokuMinSize (0x44).
|
||||||
|
// Layout: floorNumber(4) + spawnTableIndex(4) + unk0(4) + pointMulti1(4) + pointMulti2(4)
|
||||||
|
floorBase := rengokuMinSize // 0x44
|
||||||
|
pm1Bits := uint32(bin[floorBase+12]) | uint32(bin[floorBase+13])<<8 |
|
||||||
|
uint32(bin[floorBase+14])<<16 | uint32(bin[floorBase+15])<<24
|
||||||
|
pm2Bits := uint32(bin[floorBase+16]) | uint32(bin[floorBase+17])<<8 |
|
||||||
|
uint32(bin[floorBase+18])<<16 | uint32(bin[floorBase+19])<<24
|
||||||
|
|
||||||
|
if got := math.Float32frombits(pm1Bits); got != 1.25 {
|
||||||
|
t.Errorf("PointMulti1 = %f, want 1.25", got)
|
||||||
|
}
|
||||||
|
if got := math.Float32frombits(pm2Bits); got != 3.75 {
|
||||||
|
t.Errorf("PointMulti2 = %f, want 3.75", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBuildRengokuBinary_ValidationErrors verifies that out-of-range
|
||||||
|
// spawn_table_index values are caught before the binary is built.
|
||||||
|
func TestBuildRengokuBinary_ValidationErrors(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
cfg RengokuConfig
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "multi_index_out_of_range",
|
||||||
|
cfg: RengokuConfig{
|
||||||
|
MultiRoad: RoadConfig{
|
||||||
|
Floors: []FloorConfig{{FloorNumber: 1, SpawnTableIndex: 5}},
|
||||||
|
SpawnTables: []SpawnTableConfig{{Monster1ID: 1}},
|
||||||
|
},
|
||||||
|
SoloRoad: RoadConfig{
|
||||||
|
Floors: []FloorConfig{{FloorNumber: 1, SpawnTableIndex: 0}},
|
||||||
|
SpawnTables: []SpawnTableConfig{{Monster1ID: 2}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "multi_road",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "solo_index_out_of_range",
|
||||||
|
cfg: RengokuConfig{
|
||||||
|
MultiRoad: RoadConfig{
|
||||||
|
Floors: []FloorConfig{{FloorNumber: 1, SpawnTableIndex: 0}},
|
||||||
|
SpawnTables: []SpawnTableConfig{{Monster1ID: 1}},
|
||||||
|
},
|
||||||
|
SoloRoad: RoadConfig{
|
||||||
|
Floors: []FloorConfig{{FloorNumber: 1, SpawnTableIndex: 99}},
|
||||||
|
SpawnTables: []SpawnTableConfig{{Monster1ID: 2}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "solo_road",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
_, err := BuildRengokuBinary(tc.cfg)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tc.wantErr) {
|
||||||
|
t.Errorf("error %q does not contain %q", err.Error(), tc.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLoadRengokuBinary_JSONPreferredOverBin writes both a JSON file and a
|
||||||
|
// .bin file and verifies that the JSON source is used (different monster IDs).
|
||||||
|
func TestLoadRengokuBinary_JSONPreferredOverBin(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
logger, _ := zap.NewDevelopment()
|
||||||
|
|
||||||
|
// Write a valid rengoku_data.json
|
||||||
|
cfg := sampleRengokuConfig()
|
||||||
|
jsonBytes, err := json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "rengoku_data.json"), jsonBytes, 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also write a minimal (but incompletely valid) rengoku_data.bin that
|
||||||
|
// would be returned if JSON loading was skipped.
|
||||||
|
binData := make([]byte, 16) // 16-byte ECD header, zero payload
|
||||||
|
binData[0], binData[1], binData[2], binData[3] = 0x65, 0x63, 0x64, 0x1A
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "rengoku_data.bin"), binData, 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := loadRengokuBinary(dir, logger)
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("expected non-nil result from JSON loading")
|
||||||
|
}
|
||||||
|
// The JSON-built binary is longer than the 16-byte stub .bin.
|
||||||
|
if len(result) <= 16 {
|
||||||
|
t.Errorf("result is %d bytes — looks like .bin was used instead of JSON", len(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLoadRengokuBinary_JSONFallsThroughOnBadJSON verifies that a malformed
|
||||||
|
// JSON file causes loadRengokuBinary to fall back to the .bin file.
|
||||||
|
func TestLoadRengokuBinary_JSONFallsThroughOnBadJSON(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
logger, _ := zap.NewDevelopment()
|
||||||
|
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "rengoku_data.json"), []byte("{invalid json"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a valid minimal .bin
|
||||||
|
binData := make([]byte, 16)
|
||||||
|
binData[0], binData[1], binData[2], binData[3] = 0x65, 0x63, 0x64, 0x1A
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "rengoku_data.bin"), binData, 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := loadRengokuBinary(dir, logger)
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("expected fallback to .bin, got nil")
|
||||||
|
}
|
||||||
|
if len(result) != 16 {
|
||||||
|
t.Errorf("expected 16-byte .bin result, got %d bytes", len(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -450,12 +450,15 @@ func (s *Server) Season() uint8 {
|
|||||||
return uint8(((TimeAdjusted().Unix() / secsPerDay) + sid) % 3)
|
return uint8(((TimeAdjusted().Unix() / secsPerDay) + sid) % 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadRengokuBinary reads, validates, and caches rengoku_data.bin from binPath.
|
// loadRengokuBinary loads and caches Hunting Road config. It prefers
|
||||||
// The file is served to clients as-is (ECD-encrypted); decryption and parsing
|
// rengoku_data.json (human-readable, built on the fly) and falls back to the
|
||||||
// are performed only for structural validation and startup logging.
|
// pre-encrypted rengoku_data.bin. Returns ECD-encrypted bytes ready to serve,
|
||||||
// Returns the raw encrypted bytes on success, or nil if the file is
|
// or nil if no valid source is found.
|
||||||
// missing or structurally invalid.
|
|
||||||
func loadRengokuBinary(binPath string, logger *zap.Logger) []byte {
|
func loadRengokuBinary(binPath string, logger *zap.Logger) []byte {
|
||||||
|
if enc := loadRengokuFromJSON(binPath, logger); enc != nil {
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
|
||||||
path := filepath.Join(binPath, "rengoku_data.bin")
|
path := filepath.Join(binPath, "rengoku_data.bin")
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user