From 385b974adc309af8b1556caf73cfaa02f5e3630e Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Mon, 23 Feb 2026 21:25:07 +0100 Subject: [PATCH] feat(config): register all defaults in code, shrink example config Only the database password is truly mandatory to get started, but config.example.json was 267 lines with 90+ options. Newcomers faced a wall of settings with no indication of what matters. Add registerDefaults() with all sane defaults via Viper so a minimal config (just DB credentials) produces a fully working server. Gameplay multipliers default to 1.0 instead of Go's zero value 0.0, which previously zeroed out all quest rewards for minimal configs. Uses dot-notation defaults for GameplayOptions/DebugOptions so users can override individual fields without losing other defaults. - Shrink config.example.json from 267 to 10 lines - Rename full original to config.reference.json as documentation - Simplify wizard buildDefaultConfig() from ~220 to ~12 lines - Fix latent bug: SaveDumps default used wrong key DevModeOptions - Add tests: minimal config, backward compat, single-field override - Update release workflow, README, CONTRIBUTING, docker/README --- .github/workflows/release.yml | 1 + CONTRIBUTING.md | 2 +- README.md | 2 +- config.example.json | 261 +----------------------- config.reference.json | 267 +++++++++++++++++++++++++ config/config.go | 185 ++++++++++++++++- config/config_load_test.go | 191 ++++++++++++++++++ docker/README.md | 2 +- server/channelserver/testhelpers_db.go | 2 +- server/setup/wizard.go | 224 +-------------------- server/setup/wizard_test.go | 12 +- 11 files changed, 655 insertions(+), 494 deletions(-) create mode 100644 config.reference.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ba8602508..ee56e460e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,6 +43,7 @@ jobs: mkdir -p staging cp ${{ matrix.binary }} staging/ cp config.example.json staging/ + cp config.reference.json staging/ cp -r www/ staging/www/ cp -r savedata/ staging/savedata/ # Schema is now embedded in the binary via server/migrations/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c67b44054..dc1206563 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ Thank you for your interest in contributing to Erupe! This guide will help you g ``` 3. Set up the database following the [Installation guide](README.md#installation) -4. Copy `config.example.json` to `config.json` and configure it +4. Copy `config.example.json` to `config.json` and set your database password (see `config.reference.json` for all available options) 5. Install dependencies: ```bash diff --git a/README.md b/README.md index 6f4d3273f..099f9a543 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ Edit `config.json` before starting the server. The essential settings are: | `BinPath` | Path to quest/scenario files | | `Language` | `"en"` or `"jp"` | -For the full configuration reference (gameplay multipliers, debug options, Discord integration, in-game commands, entrance/channel definitions), see [config.example.json](./config.example.json) and the [Erupe Wiki](https://github.com/Mezeporta/Erupe/wiki). +`config.example.json` is intentionally minimal — all other settings have sane defaults built into the server. For the full configuration reference (gameplay multipliers, debug options, Discord integration, in-game commands, entrance/channel definitions), see [config.reference.json](./config.reference.json) and the [Erupe Wiki](https://github.com/Mezeporta/Erupe/wiki). ## Features diff --git a/config.example.json b/config.example.json index 0e1270e88..2b95d08b0 100644 --- a/config.example.json +++ b/config.example.json @@ -1,203 +1,5 @@ { - "Host": "127.0.0.1", - "BinPath": "bin", - "Language": "en", - "DisableSoftCrash": false, - "HideLoginNotice": true, - "LoginNotices": [ - "
Welcome to Erupe SU9.3!
Erupe is experimental software, we are not liable for any
issues caused by installing the software!

■Report bugs on Discord!

■Test everything!

■Don't talk to softlocking NPCs!

■Fork the code on GitHub!

Thank you to all of the contributors,

this wouldn't exist without you." - ], - "PatchServerManifest": "", - "PatchServerFile": "", - "Screenshots":{ - "Enabled":true, - "Host":"127.0.0.1", - "Port":8080, - "OutputDir":"screenshots", - "UploadQuality":100 - }, - "DeleteOnSaveCorruption": false, - "ClientMode": "ZZ", - "QuestCacheExpiry": 300, - "CommandPrefix": "!", - "AutoCreateAccount": true, - "LoopDelay": 50, - "DefaultCourses": [1, 23, 24], - "EarthStatus": 0, - "EarthID": 0, - "EarthMonsters": [0, 0, 0, 0], - "SaveDumps": { - "Enabled": true, - "RawEnabled": false, - "OutputDir": "save-backups" - }, - "Capture": { - "Enabled": false, - "OutputDir": "captures", - "ExcludeOpcodes": [], - "CaptureSign": true, - "CaptureEntrance": true, - "CaptureChannel": true - }, - "DebugOptions": { - "CleanDB": false, - "MaxLauncherHR": false, - "LogInboundMessages": false, - "LogOutboundMessages": false, - "LogMessageData": false, - "MaxHexdumpLength": 256, - "DivaOverride": 0, - "FestaOverride": -1, - "TournamentOverride": 0, - "DisableTokenCheck": false, - "QuestTools": false, - "AutoQuestBackport": true, - "ProxyPort": 0, - "CapLink": { - "Values": [51728, 20000, 51729, 1, 20000], - "Key": "", - "Host": "", - "Port": 80 - } - }, - "GameplayOptions": { - "MinFeatureWeapons": 0, - "MaxFeatureWeapons": 1, - "MaximumNP": 100000, - "MaximumRP": 50000, - "MaximumFP": 120000, - "TreasureHuntExpiry": 604800, - "DisableLoginBoost": false, - "DisableBoostTime": false, - "BoostTimeDuration": 7200, - "ClanMealDuration": 3600, - "ClanMemberLimits": [[0, 30], [3, 40], [7, 50], [10, 60]], - "BonusQuestAllowance": 3, - "DailyQuestAllowance": 1, - "LowLatencyRaviente": false, - "RegularRavienteMaxPlayers": 8, - "ViolentRavienteMaxPlayers": 8, - "BerserkRavienteMaxPlayers": 32, - "ExtremeRavienteMaxPlayers": 32, - "SmallBerserkRavienteMaxPlayers": 8, - "GUrgentRate": 0.10, - "GCPMultiplier": 1.00, - "HRPMultiplier": 1.00, - "HRPMultiplierNC": 1.00, - "SRPMultiplier": 1.00, - "SRPMultiplierNC": 1.00, - "GRPMultiplier": 1.00, - "GRPMultiplierNC": 1.00, - "GSRPMultiplier": 1.00, - "GSRPMultiplierNC": 1.00, - "ZennyMultiplier": 1.00, - "ZennyMultiplierNC": 1.00, - "GZennyMultiplier": 1.00, - "GZennyMultiplierNC": 1.00, - "MaterialMultiplier": 1.00, - "MaterialMultiplierNC": 1.00, - "GMaterialMultiplier": 1.00, - "GMaterialMultiplierNC": 1.00, - "ExtraCarves": 0, - "ExtraCarvesNC": 0, - "GExtraCarves": 0, - "GExtraCarvesNC": 0, - "DisableHunterNavi": false, - "MezFesSoloTickets": 5, - "MezFesGroupTickets": 1, - "MezFesDuration": 172800, - "MezFesSwitchMinigame": false, - "EnableKaijiEvent": false, - "EnableHiganjimaEvent": false, - "EnableNierEvent": false, - "DisableRoad": false, - "SeasonOverride": false - }, - "Discord": { - "Enabled": false, - "BotToken": "", - "RelayChannel": { - "Enabled": false, - "MaxMessageLength": 183, - "RelayChannelID": "" - } - }, - "Commands": [ - { - "Name": "Help", - "Enabled": true, - "Description": "Show enabled chat commands", - "Prefix": "help" - }, { - "Name": "Rights", - "Enabled": false, - "Description": "Overwrite the Rights value on your account", - "Prefix": "rights" - }, { - "Name": "Raviente", - "Enabled": true, - "Description": "Various Raviente siege commands", - "Prefix": "ravi" - }, { - "Name": "Teleport", - "Enabled": false, - "Description": "Teleport to specified coordinates", - "Prefix": "tele" - }, { - "Name": "Reload", - "Enabled": true, - "Description": "Reload all players in your Land", - "Prefix": "reload" - }, { - "Name": "KeyQuest", - "Enabled": false, - "Description": "Overwrite your HR Key Quest progress", - "Prefix": "kqf" - }, { - "Name": "Course", - "Enabled": true, - "Description": "Toggle Courses on your account", - "Prefix": "course" - }, { - "Name": "PSN", - "Enabled": true, - "Description": "Link a PlayStation Network ID to your account", - "Prefix": "psn" - }, { - "Name": "Discord", - "Enabled": true, - "Description": "Generate a token to link your Discord account", - "Prefix": "discord" - }, { - "Name": "Ban", - "Enabled": false, - "Description": "Ban/Temp Ban a user", - "Prefix": "ban" - }, { - "Name": "Timer", - "Enabled": true, - "Description": "Toggle the Quest timer", - "Prefix": "timer" - }, { - "Name": "Playtime", - "Enabled": true, - "Description": "Show your playtime", - "Prefix": "playtime" - } - ], - "Courses": [ - {"Name": "HunterLife", "Enabled": true}, - {"Name": "Extra", "Enabled": true}, - {"Name": "Premium", "Enabled": true}, - {"Name": "Assist", "Enabled": false}, - {"Name": "N", "Enabled": false}, - {"Name": "Hiden", "Enabled": false}, - {"Name": "HunterSupport", "Enabled": false}, - {"Name": "NBoost", "Enabled": false}, - {"Name": "NetCafe", "Enabled": true}, - {"Name": "HLRenewing", "Enabled": true}, - {"Name": "EXRenewing", "Enabled": true} - ], + "Host": "", "Database": { "Host": "localhost", "Port": 5432, @@ -205,63 +7,6 @@ "Password": "", "Database": "erupe" }, - "Sign": { - "Enabled": true, - "Port": 53312 - }, - "API": { - "Enabled": true, - "Port": 8080, - "PatchServer": "", - "Banners": [], - "Messages": [], - "Links": [], - "LandingPage": { - "Enabled": true, - "Title": "My Frontier Server", - "Content": "

Welcome! Download the client from our Discord.

" - } - }, - "Channel": { - "Enabled": true - }, - "Entrance": { - "Enabled": true, - "Port": 53310, - "Entries": [ - { - "Name": "Newbie", "Description": "", "IP": "", "Type": 3, "Recommended": 2, "AllowedClientFlags": 0, - "Channels": [ - { "Port": 54001, "MaxPlayers": 100, "Enabled": true }, - { "Port": 54002, "MaxPlayers": 100, "Enabled": true } - ] - }, { - "Name": "Normal", "Description": "", "IP": "", "Type": 1, "Recommended": 0, "AllowedClientFlags": 0, - "Channels": [ - { "Port": 54003, "MaxPlayers": 100, "Enabled": true }, - { "Port": 54004, "MaxPlayers": 100, "Enabled": true } - ] - }, { - "Name": "Cities", "Description": "", "IP": "", "Type": 2, "Recommended": 0, "AllowedClientFlags": 0, - "Channels": [ - { "Port": 54005, "MaxPlayers": 100, "Enabled": true } - ] - }, { - "Name": "Tavern", "Description": "", "IP": "", "Type": 4, "Recommended": 0, "AllowedClientFlags": 0, - "Channels": [ - { "Port": 54006, "MaxPlayers": 100, "Enabled": true } - ] - }, { - "Name": "Return", "Description": "", "IP": "", "Type": 5, "Recommended": 0, "AllowedClientFlags": 0, - "Channels": [ - { "Port": 54007, "MaxPlayers": 100, "Enabled": true } - ] - }, { - "Name": "MezFes", "Description": "", "IP": "", "Type": 6, "Recommended": 6, "AllowedClientFlags": 0, - "Channels": [ - { "Port": 54008, "MaxPlayers": 100, "Enabled": true } - ] - } - ] - } + "ClientMode": "ZZ", + "AutoCreateAccount": true } diff --git a/config.reference.json b/config.reference.json new file mode 100644 index 000000000..0e1270e88 --- /dev/null +++ b/config.reference.json @@ -0,0 +1,267 @@ +{ + "Host": "127.0.0.1", + "BinPath": "bin", + "Language": "en", + "DisableSoftCrash": false, + "HideLoginNotice": true, + "LoginNotices": [ + "
Welcome to Erupe SU9.3!
Erupe is experimental software, we are not liable for any
issues caused by installing the software!

■Report bugs on Discord!

■Test everything!

■Don't talk to softlocking NPCs!

■Fork the code on GitHub!

Thank you to all of the contributors,

this wouldn't exist without you." + ], + "PatchServerManifest": "", + "PatchServerFile": "", + "Screenshots":{ + "Enabled":true, + "Host":"127.0.0.1", + "Port":8080, + "OutputDir":"screenshots", + "UploadQuality":100 + }, + "DeleteOnSaveCorruption": false, + "ClientMode": "ZZ", + "QuestCacheExpiry": 300, + "CommandPrefix": "!", + "AutoCreateAccount": true, + "LoopDelay": 50, + "DefaultCourses": [1, 23, 24], + "EarthStatus": 0, + "EarthID": 0, + "EarthMonsters": [0, 0, 0, 0], + "SaveDumps": { + "Enabled": true, + "RawEnabled": false, + "OutputDir": "save-backups" + }, + "Capture": { + "Enabled": false, + "OutputDir": "captures", + "ExcludeOpcodes": [], + "CaptureSign": true, + "CaptureEntrance": true, + "CaptureChannel": true + }, + "DebugOptions": { + "CleanDB": false, + "MaxLauncherHR": false, + "LogInboundMessages": false, + "LogOutboundMessages": false, + "LogMessageData": false, + "MaxHexdumpLength": 256, + "DivaOverride": 0, + "FestaOverride": -1, + "TournamentOverride": 0, + "DisableTokenCheck": false, + "QuestTools": false, + "AutoQuestBackport": true, + "ProxyPort": 0, + "CapLink": { + "Values": [51728, 20000, 51729, 1, 20000], + "Key": "", + "Host": "", + "Port": 80 + } + }, + "GameplayOptions": { + "MinFeatureWeapons": 0, + "MaxFeatureWeapons": 1, + "MaximumNP": 100000, + "MaximumRP": 50000, + "MaximumFP": 120000, + "TreasureHuntExpiry": 604800, + "DisableLoginBoost": false, + "DisableBoostTime": false, + "BoostTimeDuration": 7200, + "ClanMealDuration": 3600, + "ClanMemberLimits": [[0, 30], [3, 40], [7, 50], [10, 60]], + "BonusQuestAllowance": 3, + "DailyQuestAllowance": 1, + "LowLatencyRaviente": false, + "RegularRavienteMaxPlayers": 8, + "ViolentRavienteMaxPlayers": 8, + "BerserkRavienteMaxPlayers": 32, + "ExtremeRavienteMaxPlayers": 32, + "SmallBerserkRavienteMaxPlayers": 8, + "GUrgentRate": 0.10, + "GCPMultiplier": 1.00, + "HRPMultiplier": 1.00, + "HRPMultiplierNC": 1.00, + "SRPMultiplier": 1.00, + "SRPMultiplierNC": 1.00, + "GRPMultiplier": 1.00, + "GRPMultiplierNC": 1.00, + "GSRPMultiplier": 1.00, + "GSRPMultiplierNC": 1.00, + "ZennyMultiplier": 1.00, + "ZennyMultiplierNC": 1.00, + "GZennyMultiplier": 1.00, + "GZennyMultiplierNC": 1.00, + "MaterialMultiplier": 1.00, + "MaterialMultiplierNC": 1.00, + "GMaterialMultiplier": 1.00, + "GMaterialMultiplierNC": 1.00, + "ExtraCarves": 0, + "ExtraCarvesNC": 0, + "GExtraCarves": 0, + "GExtraCarvesNC": 0, + "DisableHunterNavi": false, + "MezFesSoloTickets": 5, + "MezFesGroupTickets": 1, + "MezFesDuration": 172800, + "MezFesSwitchMinigame": false, + "EnableKaijiEvent": false, + "EnableHiganjimaEvent": false, + "EnableNierEvent": false, + "DisableRoad": false, + "SeasonOverride": false + }, + "Discord": { + "Enabled": false, + "BotToken": "", + "RelayChannel": { + "Enabled": false, + "MaxMessageLength": 183, + "RelayChannelID": "" + } + }, + "Commands": [ + { + "Name": "Help", + "Enabled": true, + "Description": "Show enabled chat commands", + "Prefix": "help" + }, { + "Name": "Rights", + "Enabled": false, + "Description": "Overwrite the Rights value on your account", + "Prefix": "rights" + }, { + "Name": "Raviente", + "Enabled": true, + "Description": "Various Raviente siege commands", + "Prefix": "ravi" + }, { + "Name": "Teleport", + "Enabled": false, + "Description": "Teleport to specified coordinates", + "Prefix": "tele" + }, { + "Name": "Reload", + "Enabled": true, + "Description": "Reload all players in your Land", + "Prefix": "reload" + }, { + "Name": "KeyQuest", + "Enabled": false, + "Description": "Overwrite your HR Key Quest progress", + "Prefix": "kqf" + }, { + "Name": "Course", + "Enabled": true, + "Description": "Toggle Courses on your account", + "Prefix": "course" + }, { + "Name": "PSN", + "Enabled": true, + "Description": "Link a PlayStation Network ID to your account", + "Prefix": "psn" + }, { + "Name": "Discord", + "Enabled": true, + "Description": "Generate a token to link your Discord account", + "Prefix": "discord" + }, { + "Name": "Ban", + "Enabled": false, + "Description": "Ban/Temp Ban a user", + "Prefix": "ban" + }, { + "Name": "Timer", + "Enabled": true, + "Description": "Toggle the Quest timer", + "Prefix": "timer" + }, { + "Name": "Playtime", + "Enabled": true, + "Description": "Show your playtime", + "Prefix": "playtime" + } + ], + "Courses": [ + {"Name": "HunterLife", "Enabled": true}, + {"Name": "Extra", "Enabled": true}, + {"Name": "Premium", "Enabled": true}, + {"Name": "Assist", "Enabled": false}, + {"Name": "N", "Enabled": false}, + {"Name": "Hiden", "Enabled": false}, + {"Name": "HunterSupport", "Enabled": false}, + {"Name": "NBoost", "Enabled": false}, + {"Name": "NetCafe", "Enabled": true}, + {"Name": "HLRenewing", "Enabled": true}, + {"Name": "EXRenewing", "Enabled": true} + ], + "Database": { + "Host": "localhost", + "Port": 5432, + "User": "postgres", + "Password": "", + "Database": "erupe" + }, + "Sign": { + "Enabled": true, + "Port": 53312 + }, + "API": { + "Enabled": true, + "Port": 8080, + "PatchServer": "", + "Banners": [], + "Messages": [], + "Links": [], + "LandingPage": { + "Enabled": true, + "Title": "My Frontier Server", + "Content": "

Welcome! Download the client from our Discord.

" + } + }, + "Channel": { + "Enabled": true + }, + "Entrance": { + "Enabled": true, + "Port": 53310, + "Entries": [ + { + "Name": "Newbie", "Description": "", "IP": "", "Type": 3, "Recommended": 2, "AllowedClientFlags": 0, + "Channels": [ + { "Port": 54001, "MaxPlayers": 100, "Enabled": true }, + { "Port": 54002, "MaxPlayers": 100, "Enabled": true } + ] + }, { + "Name": "Normal", "Description": "", "IP": "", "Type": 1, "Recommended": 0, "AllowedClientFlags": 0, + "Channels": [ + { "Port": 54003, "MaxPlayers": 100, "Enabled": true }, + { "Port": 54004, "MaxPlayers": 100, "Enabled": true } + ] + }, { + "Name": "Cities", "Description": "", "IP": "", "Type": 2, "Recommended": 0, "AllowedClientFlags": 0, + "Channels": [ + { "Port": 54005, "MaxPlayers": 100, "Enabled": true } + ] + }, { + "Name": "Tavern", "Description": "", "IP": "", "Type": 4, "Recommended": 0, "AllowedClientFlags": 0, + "Channels": [ + { "Port": 54006, "MaxPlayers": 100, "Enabled": true } + ] + }, { + "Name": "Return", "Description": "", "IP": "", "Type": 5, "Recommended": 0, "AllowedClientFlags": 0, + "Channels": [ + { "Port": 54007, "MaxPlayers": 100, "Enabled": true } + ] + }, { + "Name": "MezFes", "Description": "", "IP": "", "Type": 6, "Recommended": 6, "AllowedClientFlags": 0, + "Channels": [ + { "Port": 54008, "MaxPlayers": 100, "Enabled": true } + ] + } + ] + } +} diff --git a/config/config.go b/config/config.go index 4cc558cdc..cfff625f7 100644 --- a/config/config.go +++ b/config/config.go @@ -338,22 +338,195 @@ func getOutboundIP4() (net.IP, error) { return localAddr.IP.To4(), nil } -// LoadConfig loads the given config toml file. -func LoadConfig() (*Config, error) { - viper.SetConfigName("config") - viper.AddConfigPath(".") +// registerDefaults sets all sane defaults via Viper so that a minimal +// config.json (just database credentials) produces a fully working server. +func registerDefaults() { + // Top-level settings + viper.SetDefault("BinPath", "bin") + viper.SetDefault("HideLoginNotice", true) + viper.SetDefault("LoginNotices", []string{ + "
Welcome to Erupe!", + }) + viper.SetDefault("ClientMode", "ZZ") + viper.SetDefault("QuestCacheExpiry", 300) + viper.SetDefault("CommandPrefix", "!") + viper.SetDefault("AutoCreateAccount", true) + viper.SetDefault("LoopDelay", 50) + viper.SetDefault("DefaultCourses", []uint16{1, 23, 24}) + viper.SetDefault("EarthMonsters", []int32{0, 0, 0, 0}) - viper.SetDefault("DevModeOptions.SaveDumps", SaveDumpOptions{ + // SaveDumps + viper.SetDefault("SaveDumps", SaveDumpOptions{ Enabled: true, OutputDir: "save-backups", }) + + // Screenshots + viper.SetDefault("Screenshots", ScreenshotsOptions{ + Enabled: true, + Host: "127.0.0.1", + Port: 8080, + OutputDir: "screenshots", + UploadQuality: 100, + }) + + // Capture viper.SetDefault("Capture", CaptureOptions{ OutputDir: "captures", CaptureSign: true, CaptureEntrance: true, CaptureChannel: true, }) - viper.SetDefault("LoopDelay", 50) + + // DebugOptions (dot-notation for per-field merge) + viper.SetDefault("DebugOptions.MaxHexdumpLength", 256) + viper.SetDefault("DebugOptions.FestaOverride", -1) + viper.SetDefault("DebugOptions.AutoQuestBackport", true) + viper.SetDefault("DebugOptions.CapLink", CapLinkOptions{ + Values: []uint16{51728, 20000, 51729, 1, 20000}, + Port: 80, + }) + + // GameplayOptions (dot-notation — critical to avoid zeroing multipliers) + viper.SetDefault("GameplayOptions.MaxFeatureWeapons", 1) + viper.SetDefault("GameplayOptions.MaximumNP", 100000) + viper.SetDefault("GameplayOptions.MaximumRP", uint16(50000)) + viper.SetDefault("GameplayOptions.MaximumFP", uint32(120000)) + viper.SetDefault("GameplayOptions.TreasureHuntExpiry", uint32(604800)) + viper.SetDefault("GameplayOptions.BoostTimeDuration", 7200) + viper.SetDefault("GameplayOptions.ClanMealDuration", 3600) + viper.SetDefault("GameplayOptions.ClanMemberLimits", [][]uint8{{0, 30}, {3, 40}, {7, 50}, {10, 60}}) + viper.SetDefault("GameplayOptions.BonusQuestAllowance", uint32(3)) + viper.SetDefault("GameplayOptions.DailyQuestAllowance", uint32(1)) + viper.SetDefault("GameplayOptions.RegularRavienteMaxPlayers", uint8(8)) + viper.SetDefault("GameplayOptions.ViolentRavienteMaxPlayers", uint8(8)) + viper.SetDefault("GameplayOptions.BerserkRavienteMaxPlayers", uint8(32)) + viper.SetDefault("GameplayOptions.ExtremeRavienteMaxPlayers", uint8(32)) + viper.SetDefault("GameplayOptions.SmallBerserkRavienteMaxPlayers", uint8(8)) + viper.SetDefault("GameplayOptions.GUrgentRate", float64(0.10)) + // All reward multipliers default to 1.0 — without this, Go's zero value + // (0.0) would zero out all quest rewards for minimal configs. + for _, key := range []string{ + "GCPMultiplier", "HRPMultiplier", "HRPMultiplierNC", + "SRPMultiplier", "SRPMultiplierNC", "GRPMultiplier", "GRPMultiplierNC", + "GSRPMultiplier", "GSRPMultiplierNC", "ZennyMultiplier", "ZennyMultiplierNC", + "GZennyMultiplier", "GZennyMultiplierNC", "MaterialMultiplier", "MaterialMultiplierNC", + "GMaterialMultiplier", "GMaterialMultiplierNC", + } { + viper.SetDefault("GameplayOptions."+key, float64(1.0)) + } + viper.SetDefault("GameplayOptions.MezFesSoloTickets", uint32(5)) + viper.SetDefault("GameplayOptions.MezFesGroupTickets", uint32(1)) + viper.SetDefault("GameplayOptions.MezFesDuration", 172800) + + // Discord + viper.SetDefault("Discord.RelayChannel.MaxMessageLength", 183) + + // Commands (whole-struct default — replaced entirely if user provides any) + viper.SetDefault("Commands", []Command{ + {Name: "Help", Enabled: true, Description: "Show enabled chat commands", Prefix: "help"}, + {Name: "Rights", Enabled: false, Description: "Overwrite the Rights value on your account", Prefix: "rights"}, + {Name: "Raviente", Enabled: true, Description: "Various Raviente siege commands", Prefix: "ravi"}, + {Name: "Teleport", Enabled: false, Description: "Teleport to specified coordinates", Prefix: "tele"}, + {Name: "Reload", Enabled: true, Description: "Reload all players in your Land", Prefix: "reload"}, + {Name: "KeyQuest", Enabled: false, Description: "Overwrite your HR Key Quest progress", Prefix: "kqf"}, + {Name: "Course", Enabled: true, Description: "Toggle Courses on your account", Prefix: "course"}, + {Name: "PSN", Enabled: true, Description: "Link a PlayStation Network ID to your account", Prefix: "psn"}, + {Name: "Discord", Enabled: true, Description: "Generate a token to link your Discord account", Prefix: "discord"}, + {Name: "Ban", Enabled: false, Description: "Ban/Temp Ban a user", Prefix: "ban"}, + {Name: "Timer", Enabled: true, Description: "Toggle the Quest timer", Prefix: "timer"}, + {Name: "Playtime", Enabled: true, Description: "Show your playtime", Prefix: "playtime"}, + }) + + // Courses + viper.SetDefault("Courses", []Course{ + {Name: "HunterLife", Enabled: true}, + {Name: "Extra", Enabled: true}, + {Name: "Premium", Enabled: true}, + {Name: "Assist", Enabled: false}, + {Name: "N", Enabled: false}, + {Name: "Hiden", Enabled: false}, + {Name: "HunterSupport", Enabled: false}, + {Name: "NBoost", Enabled: false}, + {Name: "NetCafe", Enabled: true}, + {Name: "HLRenewing", Enabled: true}, + {Name: "EXRenewing", Enabled: true}, + }) + + // Database (Password deliberately has no default) + viper.SetDefault("Database.Host", "localhost") + viper.SetDefault("Database.Port", 5432) + viper.SetDefault("Database.User", "postgres") + viper.SetDefault("Database.Database", "erupe") + + // Sign server + viper.SetDefault("Sign.Enabled", true) + viper.SetDefault("Sign.Port", 53312) + + // API server + viper.SetDefault("API.Enabled", true) + viper.SetDefault("API.Port", 8080) + viper.SetDefault("API.LandingPage", LandingPage{ + Enabled: true, + Title: "My Frontier Server", + Content: "

Welcome! Server is running.

", + }) + + // Channel server + viper.SetDefault("Channel.Enabled", true) + + // Entrance server + viper.SetDefault("Entrance.Enabled", true) + viper.SetDefault("Entrance.Port", uint16(53310)) + boolTrue := true + viper.SetDefault("Entrance.Entries", []EntranceServerInfo{ + { + Name: "Newbie", Type: 3, Recommended: 2, + Channels: []EntranceChannelInfo{ + {Port: 54001, MaxPlayers: 100, Enabled: &boolTrue}, + {Port: 54002, MaxPlayers: 100, Enabled: &boolTrue}, + }, + }, + { + Name: "Normal", Type: 1, + Channels: []EntranceChannelInfo{ + {Port: 54003, MaxPlayers: 100, Enabled: &boolTrue}, + {Port: 54004, MaxPlayers: 100, Enabled: &boolTrue}, + }, + }, + { + Name: "Cities", Type: 2, + Channels: []EntranceChannelInfo{ + {Port: 54005, MaxPlayers: 100, Enabled: &boolTrue}, + }, + }, + { + Name: "Tavern", Type: 4, + Channels: []EntranceChannelInfo{ + {Port: 54006, MaxPlayers: 100, Enabled: &boolTrue}, + }, + }, + { + Name: "Return", Type: 5, + Channels: []EntranceChannelInfo{ + {Port: 54007, MaxPlayers: 100, Enabled: &boolTrue}, + }, + }, + { + Name: "MezFes", Type: 6, Recommended: 6, + Channels: []EntranceChannelInfo{ + {Port: 54008, MaxPlayers: 100, Enabled: &boolTrue}, + }, + }, + }) +} + +// LoadConfig loads the given config toml file. +func LoadConfig() (*Config, error) { + viper.SetConfigName("config") + viper.AddConfigPath(".") + + registerDefaults() err := viper.ReadInConfig() if err != nil { diff --git a/config/config_load_test.go b/config/config_load_test.go index 1b493dbc4..d19359edc 100644 --- a/config/config_load_test.go +++ b/config/config_load_test.go @@ -2,8 +2,11 @@ package config import ( "os" + "path/filepath" "strings" "testing" + + "github.com/spf13/viper" ) // TestLoadConfigNoFile tests LoadConfig when config file doesn't exist @@ -497,3 +500,191 @@ func BenchmarkConfigCreation(b *testing.B) { } } } + +// writeMinimalConfig writes a minimal config.json to dir and returns its path. +func writeMinimalConfig(t *testing.T, dir, content string) { + t.Helper() + if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(content), 0644); err != nil { + t.Fatalf("writing config.json: %v", err) + } +} + +// TestMinimalConfigDefaults verifies that a minimal config.json produces a fully +// populated Config with sane defaults (multipliers not zero, entrance entries present, etc). +func TestMinimalConfigDefaults(t *testing.T) { + viper.Reset() + dir := t.TempDir() + origDir, _ := os.Getwd() + defer func() { _ = os.Chdir(origDir) }() + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + + writeMinimalConfig(t, dir, `{ + "Database": { "Password": "test" } + }`) + + cfg, err := LoadConfig() + if err != nil { + t.Fatalf("LoadConfig() error: %v", err) + } + + // Multipliers must be 1.0 (not Go's zero value 0.0) + multipliers := map[string]float32{ + "HRPMultiplier": cfg.GameplayOptions.HRPMultiplier, + "SRPMultiplier": cfg.GameplayOptions.SRPMultiplier, + "GRPMultiplier": cfg.GameplayOptions.GRPMultiplier, + "ZennyMultiplier": cfg.GameplayOptions.ZennyMultiplier, + "MaterialMultiplier": cfg.GameplayOptions.MaterialMultiplier, + "GCPMultiplier": cfg.GameplayOptions.GCPMultiplier, + "GMaterialMultiplier": cfg.GameplayOptions.GMaterialMultiplier, + } + for name, val := range multipliers { + if val != 1.0 { + t.Errorf("%s = %v, want 1.0", name, val) + } + } + + // Entrance entries should be present + if len(cfg.Entrance.Entries) != 6 { + t.Errorf("Entrance.Entries = %d, want 6", len(cfg.Entrance.Entries)) + } + + // Commands should be present + if len(cfg.Commands) != 12 { + t.Errorf("Commands = %d, want 12", len(cfg.Commands)) + } + + // Courses should be present + if len(cfg.Courses) != 11 { + t.Errorf("Courses = %d, want 11", len(cfg.Courses)) + } + + // Standard ports + if cfg.Sign.Port != 53312 { + t.Errorf("Sign.Port = %d, want 53312", cfg.Sign.Port) + } + if cfg.API.Port != 8080 { + t.Errorf("API.Port = %d, want 8080", cfg.API.Port) + } + if cfg.Entrance.Port != 53310 { + t.Errorf("Entrance.Port = %d, want 53310", cfg.Entrance.Port) + } + + // Servers enabled by default + if !cfg.Sign.Enabled { + t.Error("Sign.Enabled should be true") + } + if !cfg.API.Enabled { + t.Error("API.Enabled should be true") + } + if !cfg.Channel.Enabled { + t.Error("Channel.Enabled should be true") + } + if !cfg.Entrance.Enabled { + t.Error("Entrance.Enabled should be true") + } + + // Database defaults + if cfg.Database.Host != "localhost" { + t.Errorf("Database.Host = %q, want localhost", cfg.Database.Host) + } + if cfg.Database.Port != 5432 { + t.Errorf("Database.Port = %d, want 5432", cfg.Database.Port) + } + + // ClientMode defaults to ZZ + if cfg.RealClientMode != ZZ { + t.Errorf("RealClientMode = %v, want ZZ", cfg.RealClientMode) + } + + // BinPath default + if cfg.BinPath != "bin" { + t.Errorf("BinPath = %q, want bin", cfg.BinPath) + } + + // Gameplay limits + if cfg.GameplayOptions.MaximumNP != 100000 { + t.Errorf("MaximumNP = %d, want 100000", cfg.GameplayOptions.MaximumNP) + } +} + +// TestFullConfigBackwardCompat verifies that existing full configs still load correctly. +func TestFullConfigBackwardCompat(t *testing.T) { + viper.Reset() + dir := t.TempDir() + origDir, _ := os.Getwd() + defer func() { _ = os.Chdir(origDir) }() + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + + // Read the reference config (the full original config.example.json). + // Look in the project root (one level up from config/). + refPath := filepath.Join(origDir, "..", "config.reference.json") + refData, err := os.ReadFile(refPath) + if err != nil { + t.Skipf("config.reference.json not found at %s, skipping backward compat test", refPath) + } + writeMinimalConfig(t, dir, string(refData)) + + cfg, err := LoadConfig() + if err != nil { + t.Fatalf("LoadConfig() with full config error: %v", err) + } + + // Spot-check values from the reference config + if cfg.GameplayOptions.HRPMultiplier != 1.0 { + t.Errorf("HRPMultiplier = %v, want 1.0", cfg.GameplayOptions.HRPMultiplier) + } + if cfg.Sign.Port != 53312 { + t.Errorf("Sign.Port = %d, want 53312", cfg.Sign.Port) + } + if len(cfg.Entrance.Entries) != 6 { + t.Errorf("Entrance.Entries = %d, want 6", len(cfg.Entrance.Entries)) + } + if len(cfg.Commands) != 12 { + t.Errorf("Commands = %d, want 12", len(cfg.Commands)) + } + if cfg.GameplayOptions.MaximumNP != 100000 { + t.Errorf("MaximumNP = %d, want 100000", cfg.GameplayOptions.MaximumNP) + } +} + +// TestSingleFieldOverride verifies that overriding one field in a dot-notation +// section doesn't clobber other fields' defaults. +func TestSingleFieldOverride(t *testing.T) { + viper.Reset() + dir := t.TempDir() + origDir, _ := os.Getwd() + defer func() { _ = os.Chdir(origDir) }() + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + + writeMinimalConfig(t, dir, `{ + "Database": { "Password": "test" }, + "GameplayOptions": { "HRPMultiplier": 2.0 } + }`) + + cfg, err := LoadConfig() + if err != nil { + t.Fatalf("LoadConfig() error: %v", err) + } + + // Overridden field + if cfg.GameplayOptions.HRPMultiplier != 2.0 { + t.Errorf("HRPMultiplier = %v, want 2.0", cfg.GameplayOptions.HRPMultiplier) + } + + // Other multipliers should retain defaults + if cfg.GameplayOptions.SRPMultiplier != 1.0 { + t.Errorf("SRPMultiplier = %v, want 1.0 (should retain default)", cfg.GameplayOptions.SRPMultiplier) + } + if cfg.GameplayOptions.ZennyMultiplier != 1.0 { + t.Errorf("ZennyMultiplier = %v, want 1.0 (should retain default)", cfg.GameplayOptions.ZennyMultiplier) + } + if cfg.GameplayOptions.GCPMultiplier != 1.0 { + t.Errorf("GCPMultiplier = %v, want 1.0 (should retain default)", cfg.GameplayOptions.GCPMultiplier) + } +} diff --git a/docker/README.md b/docker/README.md index bedd3faeb..c7208675f 100644 --- a/docker/README.md +++ b/docker/README.md @@ -8,7 +8,7 @@ cp config.example.json docker/config.json ``` - Edit `docker/config.json` — set `Database.Host` to `"db"` and match the password to `docker-compose.yml` (default: `password`). + Edit `docker/config.json` — set `Database.Host` to `"db"` and `Database.Password` to match `docker-compose.yml` (default: `password`). The example config is minimal; see `config.reference.json` for all available options. 2. Place your [quest/scenario files](https://files.catbox.moe/xf0l7w.7z) in `docker/bin/`. diff --git a/server/channelserver/testhelpers_db.go b/server/channelserver/testhelpers_db.go index 78d421c3b..4c4310deb 100644 --- a/server/channelserver/testhelpers_db.go +++ b/server/channelserver/testhelpers_db.go @@ -185,7 +185,7 @@ func CreateTestCharacter(t *testing.T, db *sqlx.DB, userID uint32, name string) // Create minimal valid savedata (needs to be large enough for the game to parse) // The name is at offset 88, and various game mode pointers extend up to ~147KB for ZZ mode // We need at least 150KB to accommodate all possible pointer offsets - saveData := make([]byte, 150000) // Large enough for all game modes + saveData := make([]byte, 150000) // Large enough for all game modes copy(saveData[88:], append([]byte(name), 0x00)) // Name at offset 88 with null terminator // Import the nullcomp package for compression diff --git a/server/setup/wizard.go b/server/setup/wizard.go index 96c9466b7..21f748af9 100644 --- a/server/setup/wizard.go +++ b/server/setup/wizard.go @@ -30,158 +30,13 @@ type FinishRequest struct { AutoCreateAccount bool `json:"autoCreateAccount"` } -// buildDefaultConfig produces a config map matching config.example.json structure -// with the user's values merged in. +// buildDefaultConfig produces a minimal config map with only user-provided values. +// All other settings are filled by Viper's registered defaults at load time. func buildDefaultConfig(req FinishRequest) map[string]interface{} { - config := map[string]interface{}{ - "Host": req.Host, - "BinPath": "bin", - "Language": "en", - "DisableSoftCrash": false, - "HideLoginNotice": true, - "LoginNotices": []string{"
Welcome to Erupe!"}, - "PatchServerManifest": "", - "PatchServerFile": "", - "DeleteOnSaveCorruption": false, - "ClientMode": req.ClientMode, - "QuestCacheExpiry": 300, - "CommandPrefix": "!", - "AutoCreateAccount": req.AutoCreateAccount, - "LoopDelay": 50, - "DefaultCourses": []int{1, 23, 24}, - "EarthStatus": 0, - "EarthID": 0, - "EarthMonsters": []int{0, 0, 0, 0}, - "Screenshots": map[string]interface{}{ - "Enabled": true, - "Host": "127.0.0.1", - "Port": 8080, - "OutputDir": "screenshots", - "UploadQuality": 100, - }, - "SaveDumps": map[string]interface{}{ - "Enabled": true, - "RawEnabled": false, - "OutputDir": "save-backups", - }, - "Capture": map[string]interface{}{ - "Enabled": false, - "OutputDir": "captures", - "ExcludeOpcodes": []int{}, - "CaptureSign": true, - "CaptureEntrance": true, - "CaptureChannel": true, - }, - "DebugOptions": map[string]interface{}{ - "CleanDB": false, - "MaxLauncherHR": false, - "LogInboundMessages": false, - "LogOutboundMessages": false, - "LogMessageData": false, - "MaxHexdumpLength": 256, - "DivaOverride": 0, - "FestaOverride": -1, - "TournamentOverride": 0, - "DisableTokenCheck": false, - "QuestTools": false, - "AutoQuestBackport": true, - "ProxyPort": 0, - "CapLink": map[string]interface{}{ - "Values": []int{51728, 20000, 51729, 1, 20000}, - "Key": "", - "Host": "", - "Port": 80, - }, - }, - "GameplayOptions": map[string]interface{}{ - "MinFeatureWeapons": 0, - "MaxFeatureWeapons": 1, - "MaximumNP": 100000, - "MaximumRP": 50000, - "MaximumFP": 120000, - "TreasureHuntExpiry": 604800, - "DisableLoginBoost": false, - "DisableBoostTime": false, - "BoostTimeDuration": 7200, - "ClanMealDuration": 3600, - "ClanMemberLimits": [][]int{{0, 30}, {3, 40}, {7, 50}, {10, 60}}, - "BonusQuestAllowance": 3, - "DailyQuestAllowance": 1, - "LowLatencyRaviente": false, - "RegularRavienteMaxPlayers": 8, - "ViolentRavienteMaxPlayers": 8, - "BerserkRavienteMaxPlayers": 32, - "ExtremeRavienteMaxPlayers": 32, - "SmallBerserkRavienteMaxPlayers": 8, - "GUrgentRate": 0.10, - "GCPMultiplier": 1.00, - "HRPMultiplier": 1.00, - "HRPMultiplierNC": 1.00, - "SRPMultiplier": 1.00, - "SRPMultiplierNC": 1.00, - "GRPMultiplier": 1.00, - "GRPMultiplierNC": 1.00, - "GSRPMultiplier": 1.00, - "GSRPMultiplierNC": 1.00, - "ZennyMultiplier": 1.00, - "ZennyMultiplierNC": 1.00, - "GZennyMultiplier": 1.00, - "GZennyMultiplierNC": 1.00, - "MaterialMultiplier": 1.00, - "MaterialMultiplierNC": 1.00, - "GMaterialMultiplier": 1.00, - "GMaterialMultiplierNC": 1.00, - "ExtraCarves": 0, - "ExtraCarvesNC": 0, - "GExtraCarves": 0, - "GExtraCarvesNC": 0, - "DisableHunterNavi": false, - "MezFesSoloTickets": 5, - "MezFesGroupTickets": 1, - "MezFesDuration": 172800, - "MezFesSwitchMinigame": false, - "EnableKaijiEvent": false, - "EnableHiganjimaEvent": false, - "EnableNierEvent": false, - "DisableRoad": false, - "SeasonOverride": false, - }, - "Discord": map[string]interface{}{ - "Enabled": false, - "BotToken": "", - "RelayChannel": map[string]interface{}{ - "Enabled": false, - "MaxMessageLength": 183, - "RelayChannelID": "", - }, - }, - "Commands": []map[string]interface{}{ - {"Name": "Help", "Enabled": true, "Description": "Show enabled chat commands", "Prefix": "help"}, - {"Name": "Rights", "Enabled": false, "Description": "Overwrite the Rights value on your account", "Prefix": "rights"}, - {"Name": "Raviente", "Enabled": true, "Description": "Various Raviente siege commands", "Prefix": "ravi"}, - {"Name": "Teleport", "Enabled": false, "Description": "Teleport to specified coordinates", "Prefix": "tele"}, - {"Name": "Reload", "Enabled": true, "Description": "Reload all players in your Land", "Prefix": "reload"}, - {"Name": "KeyQuest", "Enabled": false, "Description": "Overwrite your HR Key Quest progress", "Prefix": "kqf"}, - {"Name": "Course", "Enabled": true, "Description": "Toggle Courses on your account", "Prefix": "course"}, - {"Name": "PSN", "Enabled": true, "Description": "Link a PlayStation Network ID to your account", "Prefix": "psn"}, - {"Name": "Discord", "Enabled": true, "Description": "Generate a token to link your Discord account", "Prefix": "discord"}, - {"Name": "Ban", "Enabled": false, "Description": "Ban/Temp Ban a user", "Prefix": "ban"}, - {"Name": "Timer", "Enabled": true, "Description": "Toggle the Quest timer", "Prefix": "timer"}, - {"Name": "Playtime", "Enabled": true, "Description": "Show your playtime", "Prefix": "playtime"}, - }, - "Courses": []map[string]interface{}{ - {"Name": "HunterLife", "Enabled": true}, - {"Name": "Extra", "Enabled": true}, - {"Name": "Premium", "Enabled": true}, - {"Name": "Assist", "Enabled": false}, - {"Name": "N", "Enabled": false}, - {"Name": "Hiden", "Enabled": false}, - {"Name": "HunterSupport", "Enabled": false}, - {"Name": "NBoost", "Enabled": false}, - {"Name": "NetCafe", "Enabled": true}, - {"Name": "HLRenewing", "Enabled": true}, - {"Name": "EXRenewing", "Enabled": true}, - }, + return map[string]interface{}{ + "Host": req.Host, + "ClientMode": req.ClientMode, + "AutoCreateAccount": req.AutoCreateAccount, "Database": map[string]interface{}{ "Host": req.DBHost, "Port": req.DBPort, @@ -189,73 +44,7 @@ func buildDefaultConfig(req FinishRequest) map[string]interface{} { "Password": req.DBPassword, "Database": req.DBName, }, - "Sign": map[string]interface{}{ - "Enabled": true, - "Port": 53312, - }, - "API": map[string]interface{}{ - "Enabled": true, - "Port": 8080, - "PatchServer": "", - "Banners": []interface{}{}, - "Messages": []interface{}{}, - "Links": []interface{}{}, - "LandingPage": map[string]interface{}{ - "Enabled": true, - "Title": "My Frontier Server", - "Content": "

Welcome! Server is running.

", - }, - }, - "Channel": map[string]interface{}{ - "Enabled": true, - }, - "Entrance": map[string]interface{}{ - "Enabled": true, - "Port": 53310, - "Entries": []map[string]interface{}{ - { - "Name": "Newbie", "Description": "", "IP": "", "Type": 3, "Recommended": 2, "AllowedClientFlags": 0, - "Channels": []map[string]interface{}{ - {"Port": 54001, "MaxPlayers": 100, "Enabled": true}, - {"Port": 54002, "MaxPlayers": 100, "Enabled": true}, - }, - }, - { - "Name": "Normal", "Description": "", "IP": "", "Type": 1, "Recommended": 0, "AllowedClientFlags": 0, - "Channels": []map[string]interface{}{ - {"Port": 54003, "MaxPlayers": 100, "Enabled": true}, - {"Port": 54004, "MaxPlayers": 100, "Enabled": true}, - }, - }, - { - "Name": "Cities", "Description": "", "IP": "", "Type": 2, "Recommended": 0, "AllowedClientFlags": 0, - "Channels": []map[string]interface{}{ - {"Port": 54005, "MaxPlayers": 100, "Enabled": true}, - }, - }, - { - "Name": "Tavern", "Description": "", "IP": "", "Type": 4, "Recommended": 0, "AllowedClientFlags": 0, - "Channels": []map[string]interface{}{ - {"Port": 54006, "MaxPlayers": 100, "Enabled": true}, - }, - }, - { - "Name": "Return", "Description": "", "IP": "", "Type": 5, "Recommended": 0, "AllowedClientFlags": 0, - "Channels": []map[string]interface{}{ - {"Port": 54007, "MaxPlayers": 100, "Enabled": true}, - }, - }, - { - "Name": "MezFes", "Description": "", "IP": "", "Type": 6, "Recommended": 6, "AllowedClientFlags": 0, - "Channels": []map[string]interface{}{ - {"Port": 54008, "MaxPlayers": 100, "Enabled": true}, - }, - }, - }, - }, } - - return config } // writeConfig writes the config map to config.json with pretty formatting. @@ -368,4 +157,3 @@ func createDatabase(host string, port int, user, password, dbName string) error } return nil } - diff --git a/server/setup/wizard_test.go b/server/setup/wizard_test.go index 9776cab99..d86b25f55 100644 --- a/server/setup/wizard_test.go +++ b/server/setup/wizard_test.go @@ -56,13 +56,9 @@ func TestBuildDefaultConfig(t *testing.T) { t.Errorf("Database.Database = %v, want mydb", db["Database"]) } - // Check that critical sections exist - requiredKeys := []string{ - "Host", "BinPath", "Language", "ClientMode", "Database", - "Sign", "API", "Channel", "Entrance", "DebugOptions", - "GameplayOptions", "Discord", "Commands", "Courses", - "SaveDumps", "Capture", "Screenshots", - } + // Wizard config is now minimal — only user-provided values. + // Viper defaults fill the rest at load time. + requiredKeys := []string{"Host", "ClientMode", "AutoCreateAccount", "Database"} for _, key := range requiredKeys { if _, ok := cfg[key]; !ok { t.Errorf("missing required key %q", key) @@ -74,7 +70,7 @@ func TestBuildDefaultConfig(t *testing.T) { if err != nil { t.Fatalf("failed to marshal config: %v", err) } - if len(data) < 100 { + if len(data) < 50 { t.Errorf("config JSON unexpectedly short: %d bytes", len(data)) } }