mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
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
This commit is contained in:
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@@ -43,6 +43,7 @@ jobs:
|
|||||||
mkdir -p staging
|
mkdir -p staging
|
||||||
cp ${{ matrix.binary }} staging/
|
cp ${{ matrix.binary }} staging/
|
||||||
cp config.example.json staging/
|
cp config.example.json staging/
|
||||||
|
cp config.reference.json staging/
|
||||||
cp -r www/ staging/www/
|
cp -r www/ staging/www/
|
||||||
cp -r savedata/ staging/savedata/
|
cp -r savedata/ staging/savedata/
|
||||||
# Schema is now embedded in the binary via server/migrations/
|
# Schema is now embedded in the binary via server/migrations/
|
||||||
|
|||||||
@@ -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)
|
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:
|
5. Install dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ Edit `config.json` before starting the server. The essential settings are:
|
|||||||
| `BinPath` | Path to quest/scenario files |
|
| `BinPath` | Path to quest/scenario files |
|
||||||
| `Language` | `"en"` or `"jp"` |
|
| `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
|
## Features
|
||||||
|
|
||||||
|
|||||||
@@ -1,203 +1,5 @@
|
|||||||
{
|
{
|
||||||
"Host": "127.0.0.1",
|
"Host": "",
|
||||||
"BinPath": "bin",
|
|
||||||
"Language": "en",
|
|
||||||
"DisableSoftCrash": false,
|
|
||||||
"HideLoginNotice": true,
|
|
||||||
"LoginNotices": [
|
|
||||||
"<BODY><CENTER><SIZE_3><C_4>Welcome to Erupe SU9.3!<BR><BODY><LEFT><SIZE_2><C_5>Erupe is experimental software<C_7>, we are not liable for any<BR><BODY>issues caused by installing the software!<BR><BODY><BR><BODY><C_4>■Report bugs on Discord!<C_7><BR><BODY><BR><BODY><C_4>■Test everything!<C_7><BR><BODY><BR><BODY><C_4>■Don't talk to softlocking NPCs!<C_7><BR><BODY><BR><BODY><C_4>■Fork the code on GitHub!<C_7><BR><BODY><BR><BODY>Thank you to all of the contributors,<BR><BODY><BR><BODY>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": {
|
"Database": {
|
||||||
"Host": "localhost",
|
"Host": "localhost",
|
||||||
"Port": 5432,
|
"Port": 5432,
|
||||||
@@ -205,63 +7,6 @@
|
|||||||
"Password": "",
|
"Password": "",
|
||||||
"Database": "erupe"
|
"Database": "erupe"
|
||||||
},
|
},
|
||||||
"Sign": {
|
"ClientMode": "ZZ",
|
||||||
"Enabled": true,
|
"AutoCreateAccount": true
|
||||||
"Port": 53312
|
|
||||||
},
|
|
||||||
"API": {
|
|
||||||
"Enabled": true,
|
|
||||||
"Port": 8080,
|
|
||||||
"PatchServer": "",
|
|
||||||
"Banners": [],
|
|
||||||
"Messages": [],
|
|
||||||
"Links": [],
|
|
||||||
"LandingPage": {
|
|
||||||
"Enabled": true,
|
|
||||||
"Title": "My Frontier Server",
|
|
||||||
"Content": "<p>Welcome! Download the client from our <a href=\"https://discord.gg/example\">Discord</a>.</p>"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
267
config.reference.json
Normal file
267
config.reference.json
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
{
|
||||||
|
"Host": "127.0.0.1",
|
||||||
|
"BinPath": "bin",
|
||||||
|
"Language": "en",
|
||||||
|
"DisableSoftCrash": false,
|
||||||
|
"HideLoginNotice": true,
|
||||||
|
"LoginNotices": [
|
||||||
|
"<BODY><CENTER><SIZE_3><C_4>Welcome to Erupe SU9.3!<BR><BODY><LEFT><SIZE_2><C_5>Erupe is experimental software<C_7>, we are not liable for any<BR><BODY>issues caused by installing the software!<BR><BODY><BR><BODY><C_4>■Report bugs on Discord!<C_7><BR><BODY><BR><BODY><C_4>■Test everything!<C_7><BR><BODY><BR><BODY><C_4>■Don't talk to softlocking NPCs!<C_7><BR><BODY><BR><BODY><C_4>■Fork the code on GitHub!<C_7><BR><BODY><BR><BODY>Thank you to all of the contributors,<BR><BODY><BR><BODY>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": "<p>Welcome! Download the client from our <a href=\"https://discord.gg/example\">Discord</a>.</p>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
185
config/config.go
185
config/config.go
@@ -338,22 +338,195 @@ func getOutboundIP4() (net.IP, error) {
|
|||||||
return localAddr.IP.To4(), nil
|
return localAddr.IP.To4(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfig loads the given config toml file.
|
// registerDefaults sets all sane defaults via Viper so that a minimal
|
||||||
func LoadConfig() (*Config, error) {
|
// config.json (just database credentials) produces a fully working server.
|
||||||
viper.SetConfigName("config")
|
func registerDefaults() {
|
||||||
viper.AddConfigPath(".")
|
// Top-level settings
|
||||||
|
viper.SetDefault("BinPath", "bin")
|
||||||
|
viper.SetDefault("HideLoginNotice", true)
|
||||||
|
viper.SetDefault("LoginNotices", []string{
|
||||||
|
"<BODY><CENTER><SIZE_3><C_4>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,
|
Enabled: true,
|
||||||
OutputDir: "save-backups",
|
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{
|
viper.SetDefault("Capture", CaptureOptions{
|
||||||
OutputDir: "captures",
|
OutputDir: "captures",
|
||||||
CaptureSign: true,
|
CaptureSign: true,
|
||||||
CaptureEntrance: true,
|
CaptureEntrance: true,
|
||||||
CaptureChannel: 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: "<p>Welcome! Server is running.</p>",
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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()
|
err := viper.ReadInConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestLoadConfigNoFile tests LoadConfig when config file doesn't exist
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
cp config.example.json docker/config.json
|
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/`.
|
2. Place your [quest/scenario files](https://files.catbox.moe/xf0l7w.7z) in `docker/bin/`.
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
// 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
|
// 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
|
// 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
|
copy(saveData[88:], append([]byte(name), 0x00)) // Name at offset 88 with null terminator
|
||||||
|
|
||||||
// Import the nullcomp package for compression
|
// Import the nullcomp package for compression
|
||||||
|
|||||||
@@ -30,158 +30,13 @@ type FinishRequest struct {
|
|||||||
AutoCreateAccount bool `json:"autoCreateAccount"`
|
AutoCreateAccount bool `json:"autoCreateAccount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildDefaultConfig produces a config map matching config.example.json structure
|
// buildDefaultConfig produces a minimal config map with only user-provided values.
|
||||||
// with the user's values merged in.
|
// All other settings are filled by Viper's registered defaults at load time.
|
||||||
func buildDefaultConfig(req FinishRequest) map[string]interface{} {
|
func buildDefaultConfig(req FinishRequest) map[string]interface{} {
|
||||||
config := map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"Host": req.Host,
|
"Host": req.Host,
|
||||||
"BinPath": "bin",
|
"ClientMode": req.ClientMode,
|
||||||
"Language": "en",
|
"AutoCreateAccount": req.AutoCreateAccount,
|
||||||
"DisableSoftCrash": false,
|
|
||||||
"HideLoginNotice": true,
|
|
||||||
"LoginNotices": []string{"<BODY><CENTER><SIZE_3><C_4>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},
|
|
||||||
},
|
|
||||||
"Database": map[string]interface{}{
|
"Database": map[string]interface{}{
|
||||||
"Host": req.DBHost,
|
"Host": req.DBHost,
|
||||||
"Port": req.DBPort,
|
"Port": req.DBPort,
|
||||||
@@ -189,73 +44,7 @@ func buildDefaultConfig(req FinishRequest) map[string]interface{} {
|
|||||||
"Password": req.DBPassword,
|
"Password": req.DBPassword,
|
||||||
"Database": req.DBName,
|
"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": "<p>Welcome! Server is running.</p>",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"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.
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,13 +56,9 @@ func TestBuildDefaultConfig(t *testing.T) {
|
|||||||
t.Errorf("Database.Database = %v, want mydb", db["Database"])
|
t.Errorf("Database.Database = %v, want mydb", db["Database"])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that critical sections exist
|
// Wizard config is now minimal — only user-provided values.
|
||||||
requiredKeys := []string{
|
// Viper defaults fill the rest at load time.
|
||||||
"Host", "BinPath", "Language", "ClientMode", "Database",
|
requiredKeys := []string{"Host", "ClientMode", "AutoCreateAccount", "Database"}
|
||||||
"Sign", "API", "Channel", "Entrance", "DebugOptions",
|
|
||||||
"GameplayOptions", "Discord", "Commands", "Courses",
|
|
||||||
"SaveDumps", "Capture", "Screenshots",
|
|
||||||
}
|
|
||||||
for _, key := range requiredKeys {
|
for _, key := range requiredKeys {
|
||||||
if _, ok := cfg[key]; !ok {
|
if _, ok := cfg[key]; !ok {
|
||||||
t.Errorf("missing required key %q", key)
|
t.Errorf("missing required key %q", key)
|
||||||
@@ -74,7 +70,7 @@ func TestBuildDefaultConfig(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to marshal config: %v", err)
|
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))
|
t.Errorf("config JSON unexpectedly short: %d bytes", len(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user