mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
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.
218 lines
7.1 KiB
Go
218 lines
7.1 KiB
Go
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))
|
|
}
|
|
}
|