Files
Erupe/config/config.go
Houmgaor ecfe58ffb4 feat: add SQLite support, setup wizard enhancements, and live dashboard
Add zero-dependency SQLite mode so users can run Erupe without
PostgreSQL. A transparent db.DB wrapper auto-translates PostgreSQL
SQL ($N placeholders, now(), ::casts, ILIKE, public. prefix,
TRUNCATE) for SQLite at runtime — all 28 repo files use the wrapper
with no per-query changes needed.

Setup wizard gains two new steps: quest file detection with download
link, and gameplay presets (solo/small/community/rebalanced). The API
server gets a /dashboard endpoint with auto-refreshing stats.

CI release workflow now builds and pushes Docker images to GHCR
alongside binary artifacts on tag push.

Key changes:
- common/db: DB/Tx wrapper with 6 SQL translation rules
- server/migrations/sqlite: full SQLite schema (0001-0005)
- config: Database.Driver field ("postgres" or "sqlite")
- main.go: SQLite connection with WAL mode, single writer
- server/setup: quest check + preset selection steps
- server/api: /dashboard with live stats
- .github/workflows: Docker in release, deduplicate docker.yml
2026-03-05 18:00:30 +01:00

576 lines
21 KiB
Go

package config
import (
"fmt"
"net"
"strings"
"github.com/spf13/viper"
)
type Mode int
const (
S1 Mode = iota + 1
S15
S2
S25
S3
S35
S4
S5
S55
S6
S7
S8
S85
S9
S10
F1
F2
F3
F4
F5
G1
G2
G3
G31
G32
GG
G5
G51
G52
G6
G61
G7
G8
G81
G9
G91
G10
G101
Z1
Z2
ZZ
)
var versionStrings = []string{"S1.0", "S1.5", "S2.0", "S2.5", "S3.0", "S3.5", "S4.0", "S5.0", "S5.5", "S6.0", "S7.0",
"S8.0", "S8.5", "S9.0", "S10", "FW.1", "FW.2", "FW.3", "FW.4", "FW.5", "G1", "G2", "G3", "G3.1", "G3.2", "GG", "G5",
"G5.1", "G5.2", "G6", "G6.1", "G7", "G8", "G8.1", "G9", "G9.1", "G10", "G10.1", "Z1", "Z2", "ZZ"}
func (m Mode) String() string {
return versionStrings[m]
}
// Config holds the global server-wide config.
type Config struct {
Host string `mapstructure:"Host"`
BinPath string `mapstructure:"BinPath"`
Language string
DisableSoftCrash bool // Disables the 'Press Return to exit' dialog allowing scripts to reboot the server automatically
HideLoginNotice bool // Hide the Erupe notice on login
LoginNotices []string // MHFML string of the login notices displayed
PatchServerManifest string // Manifest patch server override
PatchServerFile string // File patch server override
DeleteOnSaveCorruption bool // Attempts to save corrupted data will flag the save for deletion
ClientMode string
RealClientMode Mode
QuestCacheExpiry int // Number of seconds to keep quest data cached
CommandPrefix string // The prefix for commands
AutoCreateAccount bool // Automatically create accounts if they don't exist
LoopDelay int // Delay in milliseconds between each loop iteration
DefaultCourses []uint16
EarthStatus int32
EarthID int32
EarthMonsters []int32
SaveDumps SaveDumpOptions
Screenshots ScreenshotsOptions
Capture CaptureOptions
DebugOptions DebugOptions
GameplayOptions GameplayOptions
Discord Discord
Commands []Command
Courses []Course
Database Database
Sign Sign
API API
Channel Channel
Entrance Entrance
}
type SaveDumpOptions struct {
Enabled bool
RawEnabled bool
OutputDir string
}
type ScreenshotsOptions struct {
Enabled bool
Host string // Destination for screenshots uploaded to BBS
Port uint32 // Port for screenshots API
OutputDir string
UploadQuality int //Determines the upload quality to the server
}
// CaptureOptions controls protocol packet capture recording.
type CaptureOptions struct {
Enabled bool // Enable packet capture
OutputDir string // Directory for .mhfr capture files
ExcludeOpcodes []uint16 // Opcodes to exclude from capture (e.g., ping, nop, position)
CaptureSign bool // Capture sign server sessions
CaptureEntrance bool // Capture entrance server sessions
CaptureChannel bool // Capture channel server sessions
}
// DebugOptions holds various debug/temporary options for use while developing Erupe.
type DebugOptions struct {
CleanDB bool // Automatically wipes the DB on server reset.
MaxLauncherHR bool // Sets the HR returned in the launcher to HR7 so that you can join non-beginner worlds.
LogInboundMessages bool // Log all messages sent to the server
LogOutboundMessages bool // Log all messages sent to the clients
LogMessageData bool // Log all bytes transferred as a hexdump
MaxHexdumpLength int // Maximum number of bytes printed when logs are enabled
DivaOverride int // Diva Defense event status
FestaOverride int // Hunter's Festa event status
TournamentOverride int // VS Tournament event status
DisableTokenCheck bool // Disables checking login token exists in the DB (security risk!)
QuestTools bool // Enable various quest debug logs
AutoQuestBackport bool // Automatically backport quest files
ProxyPort uint16 // Forces the game to connect to a channel server proxy
CapLink CapLinkOptions
}
type CapLinkOptions struct {
Values []uint16
Key string
Host string
Port int
}
// GameplayOptions has various gameplay modifiers
type GameplayOptions struct {
MinFeatureWeapons int // Minimum number of Active Feature weapons to generate daily
MaxFeatureWeapons int // Maximum number of Active Feature weapons to generate daily
MaximumNP int // Maximum number of NP held by a player
MaximumRP uint16 // Maximum number of RP held by a player
MaximumFP uint32 // Maximum number of FP held by a player
TreasureHuntExpiry uint32 // Seconds until a Clan Treasure Hunt will expire
TreasureHuntPartnyaCooldown uint32 // Seconds until a Partnya can be assigned to another Clan Treasure Hunt
DisableLoginBoost bool // Disables the Login Boost system
DisableBoostTime bool // Disables the daily NetCafe Boost Time
BoostTimeDuration int // Second that the NetCafe Boost Time lasts
ClanMealDuration int // Second that a Clan Meal can be activated for after cooking
ClanMemberLimits [][]uint8 // Array of maximum Clan Members -> [Rank, Members]
BonusQuestAllowance uint32 // Number of Bonus Point Quests to allow daily
DailyQuestAllowance uint32 // Number of Daily Quests to allow daily
LowLatencyRaviente bool // Toggles low latency mode for Raviente, can be network intensive
RegularRavienteMaxPlayers uint8
ViolentRavienteMaxPlayers uint8
BerserkRavienteMaxPlayers uint8
ExtremeRavienteMaxPlayers uint8
SmallBerserkRavienteMaxPlayers uint8
GUrgentRate float32 // Adjusts the rate of G Urgent quests spawning
GCPMultiplier float32 // Adjusts the multiplier of GCP rewarded for quest completion
HRPMultiplier float32 // Adjusts the multiplier of Hunter Rank Points rewarded for quest completion
HRPMultiplierNC float32 // Adjusts the multiplier of Hunter Rank Points rewarded for quest completion in a NetCafe
SRPMultiplier float32 // Adjusts the multiplier of Skill Rank Points rewarded for quest completion
SRPMultiplierNC float32 // Adjusts the multiplier of Skill Rank Points rewarded for quest completion in a NetCafe
GRPMultiplier float32 // Adjusts the multiplier of G Rank Points rewarded for quest completion
GRPMultiplierNC float32 // Adjusts the multiplier of G Rank Points rewarded for quest completion in a NetCafe
GSRPMultiplier float32 // Adjusts the multiplier of G Skill Rank Points rewarded for quest completion
GSRPMultiplierNC float32 // Adjusts the multiplier of G Skill Rank Points rewarded for quest completion in a NetCafe
ZennyMultiplier float32 // Adjusts the multiplier of Zenny rewarded for quest completion
ZennyMultiplierNC float32 // Adjusts the multiplier of Zenny rewarded for quest completion in a NetCafe
GZennyMultiplier float32 // Adjusts the multiplier of G Zenny rewarded for quest completion
GZennyMultiplierNC float32 // Adjusts the multiplier of G Zenny rewarded for quest completion in a NetCafe
MaterialMultiplier float32 // Adjusts the multiplier of Monster Materials rewarded for quest completion
MaterialMultiplierNC float32 // Adjusts the multiplier of Monster Materials rewarded for quest completion in a NetCafe
GMaterialMultiplier float32 // Adjusts the multiplier of G Rank Monster Materials rewarded for quest completion
GMaterialMultiplierNC float32 // Adjusts the multiplier of G Rank Monster Materials rewarded for quest completion in a NetCafe
ExtraCarves uint16 // Grant n extra chances to carve ALL carcasses
ExtraCarvesNC uint16 // Grant n extra chances to carve ALL carcasses in a NetCafe
GExtraCarves uint16 // Grant n extra chances to carve ALL G Rank carcasses
GExtraCarvesNC uint16 // Grant n extra chances to carve ALL G Rank carcasses in a NetCafe
DisableHunterNavi bool // Disables the Hunter Navi
MezFesSoloTickets uint32 // Number of solo tickets given weekly
MezFesGroupTickets uint32 // Number of group tickets given weekly
MezFesDuration int // Seconds that MezFes will last for weekly (from 12AM Mon backwards)
MezFesSwitchMinigame bool // Swaps out Volpakkun Together for Tokotoko Partnya
EnableKaijiEvent bool // Enables the Kaiji event in the Rasta Bar
EnableHiganjimaEvent bool // Enables the Higanjima event in the Rasta Bar
EnableNierEvent bool // Enables the Nier event in the Rasta Bar
DisableRoad bool // Disables the Hunting Road
SeasonOverride bool // Overrides the Quest Season with the current Mezeporta Season
}
// Discord holds the discord integration config.
type Discord struct {
Enabled bool
BotToken string
RelayChannel DiscordRelay
}
type DiscordRelay struct {
Enabled bool
MaxMessageLength int
RelayChannelID string
}
// Command is a channelserver chat command
type Command struct {
Name string
Enabled bool
Description string
Prefix string
}
// Course represents a course within MHF
type Course struct {
Name string
Enabled bool
}
// Database holds the database config.
// Driver can be "postgres" (default) or "sqlite".
// When Driver is "sqlite", only Database (file path) is used;
// Host, Port, User, and Password are ignored.
type Database struct {
Driver string // "postgres" or "sqlite"
Host string
Port int
User string
Password string
Database string
}
// Sign holds the sign server config.
type Sign struct {
Enabled bool
Port int
}
// API holds server config
type API struct {
Enabled bool
Port int
PatchServer string
Banners []APISignBanner
Messages []APISignMessage
Links []APISignLink
LandingPage LandingPage
}
// LandingPage holds config for the browser-facing landing page at /.
type LandingPage struct {
Enabled bool // Toggle the landing page on/off
Title string // Page title (e.g. "My Frontier Server")
Content string // Body content — supports raw HTML
}
type APISignBanner struct {
Src string `json:"src"` // Displayed image URL
Link string `json:"link"` // Link accessed on click
}
type APISignMessage struct {
Message string `json:"message"` // Displayed message
Date int64 `json:"date"` // Displayed date
Kind int `json:"kind"` // 0 for 'Default', 1 for 'New'
Link string `json:"link"` // Link accessed on click
}
type APISignLink struct {
Name string `json:"name"` // Displayed name
Icon string `json:"icon"` // Displayed icon. It will be cast as a monochrome color as long as it is transparent.
Link string `json:"link"` // Link accessed on click
}
type Channel struct {
Enabled bool
}
// Entrance holds the entrance server config.
type Entrance struct {
Enabled bool
Port uint16
Entries []EntranceServerInfo
}
// EntranceServerInfo represents an entry in the serverlist.
type EntranceServerInfo struct {
IP string
Type uint8 // Server type. 0=?, 1=open, 2=cities, 3=newbie, 4=bar
Season uint8 // Server activity. 0 = green, 1 = orange, 2 = blue
Recommended uint8 // Something to do with server recommendation on 0, 3, and 5.
Name string // Server name, 66 byte null terminated Shift-JIS(JP) or Big5(TW).
Description string // Server description
// 4096(PC, PS3/PS4)?, 8258(PC, PS3/PS4)?, 8192 == nothing?
// THIS ONLY EXISTS IF Binary8Header.type == "SV2", NOT "SVR"!
AllowedClientFlags uint32
Channels []EntranceChannelInfo
}
// EntranceChannelInfo represents an entry in a server's channel list.
type EntranceChannelInfo struct {
Port uint16
MaxPlayers uint16
CurrentPlayers uint16
Enabled *bool // nil defaults to true for backward compatibility
}
// IsEnabled returns whether this channel is enabled. Defaults to true if Enabled is nil.
func (c *EntranceChannelInfo) IsEnabled() bool {
if c.Enabled == nil {
return true
}
return *c.Enabled
}
// getOutboundIP4 gets the preferred outbound ip4 of this machine
// From https://stackoverflow.com/a/37382208
func getOutboundIP4() (net.IP, error) {
conn, err := net.Dial("udp4", "8.8.8.8:80")
if err != nil {
return nil, fmt.Errorf("detecting outbound IP: %w", err)
}
defer func() { _ = conn.Close() }()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP.To4(), nil
}
// 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("Language", "jp")
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})
// 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,
})
// 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 for postgres)
viper.SetDefault("Database.Driver", "postgres")
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()
if err != nil {
return nil, err
}
c := &Config{}
err = viper.Unmarshal(c)
if err != nil {
return nil, err
}
if c.Host == "" {
ip, err := getOutboundIP4()
if err != nil {
return nil, fmt.Errorf("failed to detect host IP: %w", err)
}
c.Host = ip.To4().String()
}
for i := range versionStrings {
if strings.ToUpper(c.ClientMode) == versionStrings[i] {
c.RealClientMode = Mode(i + 1)
c.ClientMode = strings.ToUpper(c.ClientMode)
if c.RealClientMode <= G101 {
c.ClientMode += " (Debug only)"
}
}
}
if c.RealClientMode == 0 {
c.ClientMode = versionStrings[len(versionStrings)-1]
c.RealClientMode = ZZ
}
if c.GameplayOptions.MinFeatureWeapons > c.GameplayOptions.MaxFeatureWeapons {
c.GameplayOptions.MinFeatureWeapons = c.GameplayOptions.MaxFeatureWeapons
}
return c, nil
}