diff --git a/.gitignore b/.gitignore index 96964aac..e622e1c3 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ rocketstation_DedicatedServer_Data/ UnityCrashHandler64.exe UnityPlayer.dll setting.xml -rocketstation_DedicatedServer.exe +rocketstation_DedicatedServer* /saves/* .github/workflows/nightly-sync.yml repos.md @@ -22,3 +22,16 @@ Blacklist.txt UIMod/detectionmanager/customdetections.json UIMod/tls/cert.pem UIMod/tls/key.pem +steamapps/** +steamcmd/** +rocketstation_BurstDebugInformation_DoNotShip/** +StationeersServerControlv* +UnityPlayer.so +BepInEx/** +*doorstep* +*doorstop* +run_bepinex.sh +debug.log +modconfig.xml +UIMod/config/config.json +C:/custom/file.txt diff --git a/UIMod/ui/config.html b/UIMod/ui/config.html index 8f350593..4209df40 100644 --- a/UIMod/ui/config.html +++ b/UIMod/ui/config.html @@ -50,6 +50,7 @@

Server Configuration

+
@@ -76,7 +77,7 @@

Basic Server Settings

provide the World type to generate. (MyMoonMap Moon) - +
Advanced Configuration value="{{AutoRestartServerTimer}}">
Timeframe in minutes to schedule an automatic gameserver restart. 0 = disabled, 1440 = 24 hours, etc.. If SSCM is enabled, you will see "Attention, server is restarting in 30/20/10/5 seconds!" messages ingame before restarting.
+ +
+ + +
Branch of the game to use. When changed, requires to restart SSUI!
+
+ + + + +
+

BETA ONLY: NEW TERRAIN AND SAVE SYSTEM SETTINGS

+
+

These settings are only useful if you are running the Stationeers Dedicated Server Beta. Switching is possible from Advanced Settings -> Game Branch. Old savegames are NOT compatible with the new system, but can be migrated with JacksonTheMasters Migration tool.. Related "how to" Information on the FAQ there also applies to the Dedicated Server..

+
+
+ + +
Set to TRUE to enable handling of .save files in the Backup manager and argument parsing. If set to false, only Stationeers versions before the terrain rework (≈ mid 2025) will work. Defaults to false until new Stationeers Terrain and Save System is released.
+
+ +
+ + +
Difficulty to create the world with. Defaults to Normal if empty.
+
+ +
+ + +
Start condition to create the world with. Defaults to the default start condition for the world type if empty.
+
+ +
+ + +
Start location to create the world with. Defaults to "DefaultStartLocation" if empty.
+
diff --git a/go.sum b/go.sum index a5090fb4..328b368e 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,19 @@ github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/server.go b/server.go index b1ab7dab..b8129206 100644 --- a/server.go +++ b/server.go @@ -26,6 +26,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/loader" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/terminal" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/web" ) @@ -44,5 +45,7 @@ func main() { loader.ReloadAll() loader.InitDetector() + terminal.StartConsole(&wg) + web.StartWebServer(&wg) } diff --git a/src/backupmgr/cleanup.go b/src/backupmgr/cleanup.go index e95653b6..340e4e3f 100644 --- a/src/backupmgr/cleanup.go +++ b/src/backupmgr/cleanup.go @@ -124,26 +124,37 @@ func (m *BackupManager) cleanSafeBackupDir() error { // getBackupGroups collects and groups backup files func (m *BackupManager) getBackupGroups() ([]BackupGroup, error) { - files, err := os.ReadDir(m.config.SafeBackupDir) + var files []os.DirEntry + err := filepath.WalkDir(m.config.SafeBackupDir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() { + files = append(files, d) + } + return nil + }) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to walk safe backup dir: %w", err) } groups := make(map[int]BackupGroup) for _, file := range files { - if file.IsDir() { + filename := file.Name() + if !isValidBackupFile(filename) { continue } - index := parseBackupIndex(file.Name()) - if index == -1 { + fullPath := filepath.Join(m.config.SafeBackupDir, filename) + info, err := file.Info() + if err != nil { continue } - fullPath := filepath.Join(m.config.SafeBackupDir, file.Name()) - info, err := os.Stat(fullPath) - if err != nil { + // Parse index or assign synthetic index for .save files + index := parseBackupIndex(filename, info.ModTime(), files) + if index == -1 { continue } @@ -151,13 +162,17 @@ func (m *BackupManager) getBackupGroups() ([]BackupGroup, error) { group.Index = index group.ModTime = info.ModTime() - switch { - case strings.HasSuffix(file.Name(), ".bin"): + if strings.HasSuffix(filename, ".save") { group.BinFile = fullPath - case strings.Contains(file.Name(), "world(") && strings.HasSuffix(file.Name(), ".xml"): - group.XMLFile = fullPath - case strings.Contains(file.Name(), "world_meta(") && strings.HasSuffix(file.Name(), ".xml"): - group.MetaFile = fullPath + } else { + switch { + case strings.HasSuffix(filename, ".bin"): + group.BinFile = fullPath + case strings.Contains(filename, "world(") && strings.HasSuffix(filename, ".xml"): + group.XMLFile = fullPath + case strings.Contains(filename, "world_meta(") && strings.HasSuffix(filename, ".xml"): + group.MetaFile = fullPath + } } groups[index] = group @@ -165,7 +180,8 @@ func (m *BackupManager) getBackupGroups() ([]BackupGroup, error) { var result []BackupGroup for _, group := range groups { - if group.BinFile != "" && group.XMLFile != "" && group.MetaFile != "" { + // Include both old-style groups (all three files) and .save-based groups (just BinFile) + if (group.BinFile != "" && group.XMLFile != "" && group.MetaFile != "") || (group.BinFile != "" && strings.HasSuffix(group.BinFile, ".save")) { result = append(result, group) } } diff --git a/src/backupmgr/manager.go b/src/backupmgr/manager.go index 1927ee3e..0bda65cf 100644 --- a/src/backupmgr/manager.go +++ b/src/backupmgr/manager.go @@ -40,7 +40,7 @@ func (m *BackupManager) Start() error { // Start file watcher watcher, err := newFsWatcher(m.config.BackupDir) if err != nil { - return fmt.Errorf("failed to create file watcher: %w", err) + return fmt.Errorf("failed to create autosave watcher: %w", err) } m.watcher = watcher @@ -98,7 +98,17 @@ func (m *BackupManager) handleNewBackup(filePath string) { defer m.mu.Unlock() fileName := filepath.Base(filePath) - dstPath := filepath.Join(m.config.SafeBackupDir, fileName) + relativePath, err := filepath.Rel(m.config.BackupDir, filePath) + if err != nil { + logger.Backup.Error("Error getting relative path for " + filePath + ": " + err.Error()) + return + } + dstPath := filepath.Join(m.config.SafeBackupDir, relativePath) + + if err := os.MkdirAll(filepath.Dir(dstPath), os.ModePerm); err != nil { + logger.Backup.Error("Error creating destination dir for " + dstPath + ": " + err.Error()) + return + } if err := copyFile(filePath, dstPath); err != nil { logger.Backup.Error("Error copying backup " + fileName + ": " + err.Error()) diff --git a/src/backupmgr/restore.go b/src/backupmgr/restore.go index 089bea2f..ed0fed23 100644 --- a/src/backupmgr/restore.go +++ b/src/backupmgr/restore.go @@ -4,8 +4,9 @@ import ( "fmt" "os" "path/filepath" + "strings" + "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" ) @@ -15,32 +16,87 @@ func (m *BackupManager) RestoreBackup(index int) error { defer m.mu.Unlock() logger.Backup.Info("Restoring backup with index " + fmt.Sprintf("%d", index)) - files := []struct { - backupName string - backupNameAlt string - destName string - }{ - {fmt.Sprintf("world_meta(%d).xml", index), fmt.Sprintf("world_meta(%d)_AutoSave.xml", index), "world_meta.xml"}, - {fmt.Sprintf("world(%d).xml", index), fmt.Sprintf("world(%d)_AutoSave.xml", index), "world.xml"}, - {fmt.Sprintf("world(%d).bin", index), fmt.Sprintf("world(%d)_AutoSave.bin", index), "world.bin"}, + groups, err := m.getBackupGroups() + if err != nil { + return fmt.Errorf("failed to get backup groups: %w", err) + } + + var targetGroup BackupGroup + for _, group := range groups { + if group.Index == index { + targetGroup = group + break + } + } + + if targetGroup.Index == 0 { + return fmt.Errorf("no backup found with index %d", index) } restoredFiles := make(map[string]string) - for _, file := range files { - backupFile := filepath.Join(m.config.SafeBackupDir, file.backupName) - destFile := filepath.Join("./saves/"+config.WorldName, file.destName) + // Handle .save file or old-style trio + if targetGroup.BinFile != "" && strings.HasSuffix(targetGroup.BinFile, ".save") { + // .save file case + backupFile := targetGroup.BinFile + destFile := filepath.Join("./saves/"+m.config.WorldName, m.config.WorldName+".save") - if err := copyFile(backupFile, destFile); err != nil { - // Try alternative name - backupFileAlt := filepath.Join(m.config.SafeBackupDir, file.backupNameAlt) - if err := copyFile(backupFileAlt, destFile); err != nil { - m.revertRestore(restoredFiles) - return fmt.Errorf("failed to restore %s: %w", file.backupName, err) + // Before restore, check if we have existing .save files in the root saves/WorldName dir + saveDir := filepath.Join("./saves/", m.config.WorldName) + files, err := os.ReadDir(saveDir) + if err != nil { + return fmt.Errorf("failed to read save directory %s: %w", saveDir, err) + } + + for _, file := range files { + if file.IsDir() { + continue + } + if strings.HasSuffix(file.Name(), ".save") { + existingFile := filepath.Join(saveDir, file.Name()) + // Move existing .save file to SafeBackupDir with timestamp to avoid overwrites + timestamp := time.Now().Format("2006-01-02_15-04-05") + savedPreviousHeadSaveFilePath := filepath.Join(m.config.SafeBackupDir, fmt.Sprintf("%s_%s_%s", "oldHeadSaveBackup", timestamp, file.Name())) + if err := os.Rename(existingFile, savedPreviousHeadSaveFilePath); err != nil { + return fmt.Errorf("failed to move existing HEAD .save file %s to %s: %w", existingFile, savedPreviousHeadSaveFilePath, err) + } + logger.Backup.Info("Moved previous HEAD .save file to: " + savedPreviousHeadSaveFilePath) } - backupFile = backupFileAlt + } + + // Now copy the new .save file + if err := copyFile(backupFile, destFile); err != nil { + m.revertRestore(restoredFiles) + return fmt.Errorf("failed to restore .save file %s: %w", backupFile, err) } restoredFiles[destFile] = backupFile + } else { + // Old-style trio (world_meta.xml, world.xml, world.bin) + files := []struct { + backupName string + backupNameAlt string + destName string + }{ + {fmt.Sprintf("world_meta(%d).xml", index), fmt.Sprintf("world_meta(%d)_AutoSave.xml", index), "world_meta.xml"}, + {fmt.Sprintf("world(%d).xml", index), fmt.Sprintf("world(%d)_AutoSave.xml", index), "world.xml"}, + {fmt.Sprintf("world(%d).bin", index), fmt.Sprintf("world(%d)_AutoSave.bin", index), "world.bin"}, + } + + for _, file := range files { + backupFile := filepath.Join(m.config.SafeBackupDir, file.backupName) + destFile := filepath.Join("./saves/"+m.config.WorldName, file.destName) + + if err := copyFile(backupFile, destFile); err != nil { + // Try alternative name + backupFileAlt := filepath.Join(m.config.SafeBackupDir, file.backupNameAlt) + if err := copyFile(backupFileAlt, destFile); err != nil { + m.revertRestore(restoredFiles) + return fmt.Errorf("failed to restore %s: %w", file.backupName, err) + } + backupFile = backupFileAlt + } + restoredFiles[destFile] = backupFile + } } logger.Backup.Debug(fmt.Sprintf("%v", restoredFiles)) diff --git a/src/backupmgr/utils.go b/src/backupmgr/utils.go index 489b3c34..15c00eee 100644 --- a/src/backupmgr/utils.go +++ b/src/backupmgr/utils.go @@ -4,8 +4,10 @@ import ( "io" "os" "regexp" + "sort" "strconv" "strings" + "time" ) // copyFile copies a file from src to dst @@ -29,24 +31,60 @@ func copyFile(src, dst string) error { return destination.Sync() } -// parseBackupIndex extracts the backup index from a filename -func parseBackupIndex(filename string) int { +// parseBackupIndex extracts the backup index from a filename or assigns a synthetic index +func parseBackupIndex(filename string, modTime time.Time, files []os.DirEntry) int { + // Try to extract index from old format (e.g., world(1).xml) re := regexp.MustCompile(`\((\d+)\)`) matches := re.FindStringSubmatch(filename) - if len(matches) < 2 { - return -1 + if len(matches) >= 2 { + index, err := strconv.Atoi(matches[1]) + if err == nil { + return index + } } - index, err := strconv.Atoi(matches[1]) - if err != nil { - return -1 + // For .save files, assign synthetic index based on mod time (newest eq highest) + if strings.HasSuffix(filename, ".save") { + // Sort files by mod time to assign indexes + var sortedFiles []struct { + name string + modTime time.Time + } + for _, file := range files { + if !strings.HasSuffix(file.Name(), ".save") { + continue + } + info, err := file.Info() + if err != nil { + continue + } + sortedFiles = append(sortedFiles, struct { + name string + modTime time.Time + }{file.Name(), info.ModTime()}) + } + + // Sort newest first + sort.Slice(sortedFiles, func(i, j int) bool { + return sortedFiles[i].modTime.After(sortedFiles[j].modTime) + }) + + // Find the position of the current file + for i, f := range sortedFiles { + if f.name == filename { + // Assign index starting from max possible index downwards + return len(sortedFiles) - i + } + } } - return index + return -1 } +// isValidBackupFile checks if a filename is a valid backup file func isValidBackupFile(filename string) bool { - return strings.Contains(filename, "world") && + return (strings.Contains(filename, "world") && (strings.HasSuffix(filename, ".bin") || - strings.HasSuffix(filename, ".xml")) + strings.HasSuffix(filename, ".xml"))) || + strings.HasSuffix(filename, ".save") } diff --git a/src/backupmgr/watcher.go b/src/backupmgr/watcher.go index e2e48413..4cc00c9f 100644 --- a/src/backupmgr/watcher.go +++ b/src/backupmgr/watcher.go @@ -2,6 +2,7 @@ package backupmgr import ( "fmt" + "os" "path/filepath" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" @@ -29,11 +30,24 @@ func newFsWatcher(path string) (*fsWatcher, error) { } logger.Backup.Debug("Watcher created successfully") - if err := watcher.Add(normalizedPath); err != nil { + // Watch the root save path and all subdirectories + err = filepath.WalkDir(normalizedPath, func(subPath string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + if err := watcher.Add(subPath); err != nil { + logger.Backup.Error("Failed to add subdir to watcher: " + subPath + ": " + err.Error()) + } else { + logger.Backup.Debug("Successfully watching subdir: " + subPath) + } + } + return nil + }) + if err != nil { watcher.Close() - return nil, fmt.Errorf("failed to add path %s to watcher: %w", normalizedPath, err) + return nil, fmt.Errorf("failed to add paths to watcher: %w", err) } - logger.Backup.Debug("Successfully watching path: " + normalizedPath) w := &fsWatcher{ watcher: watcher, diff --git a/src/config/config.go b/src/config/config.go index 4a7bc5d4..4b5cf5ff 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -11,61 +11,66 @@ import ( var ( // All configuration variables can be found in vars.go - Version = "5.4.34" - Branch = "release" + Version = "5.5.0" + Branch = "indev" ) type JsonConfig struct { - DiscordToken string `json:"discordToken"` - ControlChannelID string `json:"controlChannelID"` - StatusChannelID string `json:"statusChannelID"` - ConnectionListChannelID string `json:"connectionListChannelID"` - LogChannelID string `json:"logChannelID"` - SaveChannelID string `json:"saveChannelID"` - ControlPanelChannelID string `json:"controlPanelChannelID"` - DiscordCharBufferSize int `json:"DiscordCharBufferSize"` - BlackListFilePath string `json:"blackListFilePath"` - IsDiscordEnabled *bool `json:"isDiscordEnabled"` - ErrorChannelID string `json:"errorChannelID"` - BackupKeepLastN int `json:"backupKeepLastN"` - IsCleanupEnabled *bool `json:"isCleanupEnabled"` - BackupKeepDailyFor int `json:"backupKeepDailyFor"` - BackupKeepWeeklyFor int `json:"backupKeepWeeklyFor"` - BackupKeepMonthlyFor int `json:"backupKeepMonthlyFor"` - BackupCleanupInterval int `json:"backupCleanupInterval"` - BackupWaitTime int `json:"backupWaitTime"` - GameBranch string `json:"gameBranch"` - ServerName string `json:"ServerName"` - SaveInfo string `json:"SaveInfo"` - ServerMaxPlayers string `json:"ServerMaxPlayers"` - ServerPassword string `json:"ServerPassword"` - ServerAuthSecret string `json:"ServerAuthSecret"` - AdminPassword string `json:"AdminPassword"` - GamePort string `json:"GamePort"` - UpdatePort string `json:"UpdatePort"` - UPNPEnabled *bool `json:"UPNPEnabled"` - AutoSave *bool `json:"AutoSave"` - SaveInterval string `json:"SaveInterval"` - AutoPauseServer *bool `json:"AutoPauseServer"` - LocalIpAddress string `json:"LocalIpAddress"` - StartLocalHost *bool `json:"StartLocalHost"` - ServerVisible *bool `json:"ServerVisible"` - UseSteamP2P *bool `json:"UseSteamP2P"` - ExePath string `json:"ExePath"` - AdditionalParams string `json:"AdditionalParams"` - Users map[string]string `json:"users"` // Map of username to hashed password - AuthEnabled *bool `json:"authEnabled"` // Toggle for enabling/disabling auth - JwtKey string `json:"JwtKey"` - AuthTokenLifetime int `json:"AuthTokenLifetime"` - Debug *bool `json:"Debug"` - CreateSSUILogFile *bool `json:"CreateSSUILogFile"` - LogLevel int `json:"LogLevel"` - SubsystemFilters []string `json:"subsystemFilters"` - IsUpdateEnabled *bool `json:"IsUpdateEnabled"` - IsSSCMEnabled *bool `json:"IsSSCMEnabled"` - AutoRestartServerTimer string `json:"AutoRestartServerTimer"` - AllowPrereleaseUpdates *bool `json:"AllowPrereleaseUpdates"` - AllowMajorUpdates *bool `json:"AllowMajorUpdates"` + DiscordToken string `json:"discordToken"` + ControlChannelID string `json:"controlChannelID"` + StatusChannelID string `json:"statusChannelID"` + ConnectionListChannelID string `json:"connectionListChannelID"` + LogChannelID string `json:"logChannelID"` + SaveChannelID string `json:"saveChannelID"` + ControlPanelChannelID string `json:"controlPanelChannelID"` + DiscordCharBufferSize int `json:"DiscordCharBufferSize"` + BlackListFilePath string `json:"blackListFilePath"` + IsDiscordEnabled *bool `json:"isDiscordEnabled"` + ErrorChannelID string `json:"errorChannelID"` + BackupKeepLastN int `json:"backupKeepLastN"` + IsCleanupEnabled *bool `json:"isCleanupEnabled"` + BackupKeepDailyFor int `json:"backupKeepDailyFor"` + BackupKeepWeeklyFor int `json:"backupKeepWeeklyFor"` + BackupKeepMonthlyFor int `json:"backupKeepMonthlyFor"` + BackupCleanupInterval int `json:"backupCleanupInterval"` + BackupWaitTime int `json:"backupWaitTime"` + IsNewTerrainAndSaveSystem *bool `json:"IsNewTerrainAndSaveSystem"` + GameBranch string `json:"gameBranch"` + Difficulty string `json:"Difficulty"` + StartCondition string `json:"StartCondition"` + StartLocation string `json:"StartLocation"` + ServerName string `json:"ServerName"` + SaveInfo string `json:"SaveInfo"` + ServerMaxPlayers string `json:"ServerMaxPlayers"` + ServerPassword string `json:"ServerPassword"` + ServerAuthSecret string `json:"ServerAuthSecret"` + AdminPassword string `json:"AdminPassword"` + GamePort string `json:"GamePort"` + UpdatePort string `json:"UpdatePort"` + UPNPEnabled *bool `json:"UPNPEnabled"` + AutoSave *bool `json:"AutoSave"` + SaveInterval string `json:"SaveInterval"` + AutoPauseServer *bool `json:"AutoPauseServer"` + LocalIpAddress string `json:"LocalIpAddress"` + StartLocalHost *bool `json:"StartLocalHost"` + ServerVisible *bool `json:"ServerVisible"` + UseSteamP2P *bool `json:"UseSteamP2P"` + ExePath string `json:"ExePath"` + AdditionalParams string `json:"AdditionalParams"` + Users map[string]string `json:"users"` // Map of username to hashed password + AuthEnabled *bool `json:"authEnabled"` // Toggle for enabling/disabling auth + JwtKey string `json:"JwtKey"` + AuthTokenLifetime int `json:"AuthTokenLifetime"` + Debug *bool `json:"Debug"` + CreateSSUILogFile *bool `json:"CreateSSUILogFile"` + LogLevel int `json:"LogLevel"` + SubsystemFilters []string `json:"subsystemFilters"` + IsUpdateEnabled *bool `json:"IsUpdateEnabled"` + IsSSCMEnabled *bool `json:"IsSSCMEnabled"` + AutoRestartServerTimer string `json:"AutoRestartServerTimer"` + AllowPrereleaseUpdates *bool `json:"AllowPrereleaseUpdates"` + AllowMajorUpdates *bool `json:"AllowMajorUpdates"` + IsConsoleEnabled *bool `json:"IsConsoleEnabled"` } type CustomDetection struct { @@ -131,7 +136,15 @@ func applyConfig(cfg *JsonConfig) { BackupKeepMonthlyFor = time.Duration(getInt(cfg.BackupKeepMonthlyFor, "BACKUP_KEEP_MONTHLY_FOR", 730)) * time.Hour BackupCleanupInterval = time.Duration(getInt(cfg.BackupCleanupInterval, "BACKUP_CLEANUP_INTERVAL", 730)) * time.Hour BackupWaitTime = time.Duration(getInt(cfg.BackupWaitTime, "BACKUP_WAIT_TIME", 30)) * time.Second + + dotSaveVal := getBool(cfg.IsNewTerrainAndSaveSystem, "ENABLE_DOT_SAVES", false) + IsNewTerrainAndSaveSystem = dotSaveVal + cfg.IsNewTerrainAndSaveSystem = &dotSaveVal + GameBranch = getString(cfg.GameBranch, "GAME_BRANCH", "public") + Difficulty = getString(cfg.Difficulty, "DIFFICULTY", "") + StartCondition = getString(cfg.StartCondition, "START_CONDITION", "") + StartLocation = getString(cfg.StartLocation, "START_LOCATION", "") ServerName = getString(cfg.ServerName, "SERVER_NAME", "Stationeers Server UI") SaveInfo = getString(cfg.SaveInfo, "SAVE_INFO", "Moon Moon") ServerMaxPlayers = getString(cfg.ServerMaxPlayers, "SERVER_MAX_PLAYERS", "6") @@ -208,6 +221,10 @@ func applyConfig(cfg *JsonConfig) { IsSSCMEnabled = isSSCMEnabledVal cfg.IsSSCMEnabled = &isSSCMEnabledVal + isConsoleEnabledVal := getBool(cfg.IsConsoleEnabled, "IS_CONSOLE_ENABLED", false) + IsConsoleEnabled = isConsoleEnabledVal + cfg.IsConsoleEnabled = &isConsoleEnabledVal + // Process SaveInfo parts := strings.Split(SaveInfo, " ") if len(parts) > 0 { @@ -217,7 +234,14 @@ func applyConfig(cfg *JsonConfig) { BackupWorldName = parts[1] } - // Set backup paths - ConfiguredBackupDir = filepath.Join("./saves/", WorldName, "Backup") + // Set backup paths for old or new style saves + if IsNewTerrainAndSaveSystem { + // use new new style autosave folder + ConfiguredBackupDir = filepath.Join("./saves/", WorldName, "autosave") + } else { + // use old style Backups folder + ConfiguredBackupDir = filepath.Join("./saves/", WorldName, "Backup") + } + // use Safebackups folder either way. ConfiguredSafeBackupDir = filepath.Join("./saves/", WorldName, "Safebackups") } diff --git a/src/config/vars.go b/src/config/vars.go index 2a6a2f97..fe60d792 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -43,6 +43,9 @@ var ( SaveInterval string AutoPauseServer bool AutoSave bool + Difficulty string + StartCondition string + StartLocation string ) // Logging, debugging and misc @@ -61,6 +64,7 @@ var ( SubsystemFilters []string GameServerUUID uuid.UUID // Assined at startup to the current instance of the server we are managing. Currently unused. AutoRestartServerTimer string + IsConsoleEnabled bool ) // Discord integration @@ -83,15 +87,16 @@ var ( // Backup and cleanup settings var ( - IsCleanupEnabled bool - BackupKeepLastN int - BackupKeepDailyFor time.Duration - BackupKeepWeeklyFor time.Duration - BackupKeepMonthlyFor time.Duration - BackupCleanupInterval time.Duration - ConfiguredBackupDir string - ConfiguredSafeBackupDir string - BackupWaitTime time.Duration + IsCleanupEnabled bool + BackupKeepLastN int + BackupKeepDailyFor time.Duration + BackupKeepWeeklyFor time.Duration + BackupKeepMonthlyFor time.Duration + BackupCleanupInterval time.Duration + ConfiguredBackupDir string + ConfiguredSafeBackupDir string + BackupWaitTime time.Duration + IsNewTerrainAndSaveSystem bool ) // Authentication and security diff --git a/src/gamemgr/args.go b/src/gamemgr/args.go index 157ee987..08eaf448 100644 --- a/src/gamemgr/args.go +++ b/src/gamemgr/args.go @@ -17,26 +17,65 @@ type Arg struct { } func buildCommandArgs() []string { - var argOrder = []Arg{ - {Flag: "-nographics", RequiresValue: false}, - {Flag: "-batchmode", RequiresValue: false}, - {Flag: "-LOAD", Value: config.SaveInfo, RequiresValue: true, NoQuote: true}, // LOAD has special handling because the gameserver expects 2 parameters - {Flag: "-logFile", Value: "./debug.log", Condition: func() bool { return runtime.GOOS == "linux" }, RequiresValue: true}, - {Flag: "-settings", RequiresValue: false}, - {Flag: "StartLocalHost", Value: strconv.FormatBool(config.StartLocalHost), RequiresValue: true}, - {Flag: "ServerVisible", Value: strconv.FormatBool(config.ServerVisible), RequiresValue: true}, - {Flag: "GamePort", Value: config.GamePort, RequiresValue: true}, - {Flag: "UPNPEnabled", Value: strconv.FormatBool(config.UPNPEnabled), RequiresValue: true}, - {Flag: "ServerName", Value: config.ServerName, RequiresValue: true}, - {Flag: "ServerPassword", Value: config.ServerPassword, Condition: func() bool { return config.ServerPassword != "" }, RequiresValue: true}, - {Flag: "ServerMaxPlayers", Value: config.ServerMaxPlayers, RequiresValue: true}, - {Flag: "AutoSave", Value: strconv.FormatBool(config.AutoSave), RequiresValue: true}, - {Flag: "SaveInterval", Value: config.SaveInterval, RequiresValue: true}, - {Flag: "ServerAuthSecret", Value: config.ServerAuthSecret, Condition: func() bool { return config.ServerAuthSecret != "" }, RequiresValue: true}, - {Flag: "UpdatePort", Value: config.UpdatePort, RequiresValue: true}, - {Flag: "AutoPauseServer", Value: strconv.FormatBool(config.AutoPauseServer), RequiresValue: true}, - {Flag: "UseSteamP2P", Value: strconv.FormatBool(config.UseSteamP2P), RequiresValue: true}, - {Flag: "AdminPassword", Value: config.AdminPassword, Condition: func() bool { return config.AdminPassword != "" }, RequiresValue: true}, + var argOrder []Arg + + if config.IsNewTerrainAndSaveSystem { + argOrder = []Arg{ + {Flag: "-nographics", RequiresValue: false}, + {Flag: "-batchmode", RequiresValue: false}, + /* file start: (expects up to four optional args for: + -worldid (Optional to LOAD, required to CREATE save, if start value is not found, tries to create map with worldid) -> config.BackupWorldName for legacy reasons + -difficulty (Optional, defaults to "Normal" if not provided) + -startcondition (Optional, defaults to the default start condition for the world setting if not provided.) + -startlocation (Optional, defaults to "DefaultStartLocation" if not provided.) + */ + {Flag: "-file", RequiresValue: false}, + {Flag: "start", Value: config.WorldName, RequiresValue: true}, + {Flag: config.BackupWorldName, RequiresValue: false}, + {Flag: config.Difficulty, RequiresValue: false, Condition: func() bool { return config.Difficulty != "" }}, + {Flag: config.StartCondition, RequiresValue: false, Condition: func() bool { return config.StartCondition != "" }}, + {Flag: config.StartLocation, RequiresValue: false, Condition: func() bool { return config.StartLocation != "" }}, + // file start end + {Flag: "-logFile", Value: "./debug.log", Condition: func() bool { return runtime.GOOS == "linux" }, RequiresValue: true}, + {Flag: "-settings", RequiresValue: false}, + {Flag: "StartLocalHost", Value: strconv.FormatBool(config.StartLocalHost), RequiresValue: true}, + {Flag: "ServerVisible", Value: strconv.FormatBool(config.ServerVisible), RequiresValue: true}, + {Flag: "GamePort", Value: config.GamePort, RequiresValue: true}, + {Flag: "UPNPEnabled", Value: strconv.FormatBool(config.UPNPEnabled), RequiresValue: true}, + {Flag: "ServerName", Value: config.ServerName, RequiresValue: true}, + {Flag: "ServerPassword", Value: config.ServerPassword, Condition: func() bool { return config.ServerPassword != "" }, RequiresValue: true}, + {Flag: "ServerMaxPlayers", Value: config.ServerMaxPlayers, RequiresValue: true}, + {Flag: "AutoSave", Value: strconv.FormatBool(config.AutoSave), RequiresValue: true}, + {Flag: "SaveInterval", Value: config.SaveInterval, RequiresValue: true}, + {Flag: "ServerAuthSecret", Value: config.ServerAuthSecret, Condition: func() bool { return config.ServerAuthSecret != "" }, RequiresValue: true}, + {Flag: "UpdatePort", Value: config.UpdatePort, RequiresValue: true}, + {Flag: "AutoPauseServer", Value: strconv.FormatBool(config.AutoPauseServer), RequiresValue: true}, + {Flag: "UseSteamP2P", Value: strconv.FormatBool(config.UseSteamP2P), RequiresValue: true}, + {Flag: "AdminPassword", Value: config.AdminPassword, Condition: func() bool { return config.AdminPassword != "" }, RequiresValue: true}, + } + } + if !config.IsNewTerrainAndSaveSystem { + argOrder = []Arg{ + {Flag: "-nographics", RequiresValue: false}, + {Flag: "-batchmode", RequiresValue: false}, + {Flag: "-LOAD", Value: config.SaveInfo, RequiresValue: true, NoQuote: true}, // LOAD has special handling because the gameserver expects 2 parameters + {Flag: "-logFile", Value: "./debug.log", Condition: func() bool { return runtime.GOOS == "linux" }, RequiresValue: true}, + {Flag: "-settings", RequiresValue: false}, + {Flag: "StartLocalHost", Value: strconv.FormatBool(config.StartLocalHost), RequiresValue: true}, + {Flag: "ServerVisible", Value: strconv.FormatBool(config.ServerVisible), RequiresValue: true}, + {Flag: "GamePort", Value: config.GamePort, RequiresValue: true}, + {Flag: "UPNPEnabled", Value: strconv.FormatBool(config.UPNPEnabled), RequiresValue: true}, + {Flag: "ServerName", Value: config.ServerName, RequiresValue: true}, + {Flag: "ServerPassword", Value: config.ServerPassword, Condition: func() bool { return config.ServerPassword != "" }, RequiresValue: true}, + {Flag: "ServerMaxPlayers", Value: config.ServerMaxPlayers, RequiresValue: true}, + {Flag: "AutoSave", Value: strconv.FormatBool(config.AutoSave), RequiresValue: true}, + {Flag: "SaveInterval", Value: config.SaveInterval, RequiresValue: true}, + {Flag: "ServerAuthSecret", Value: config.ServerAuthSecret, Condition: func() bool { return config.ServerAuthSecret != "" }, RequiresValue: true}, + {Flag: "UpdatePort", Value: config.UpdatePort, RequiresValue: true}, + {Flag: "AutoPauseServer", Value: strconv.FormatBool(config.AutoPauseServer), RequiresValue: true}, + {Flag: "UseSteamP2P", Value: strconv.FormatBool(config.UseSteamP2P), RequiresValue: true}, + {Flag: "AdminPassword", Value: config.AdminPassword, Condition: func() bool { return config.AdminPassword != "" }, RequiresValue: true}, + } } var args []string diff --git a/src/loader/loader.go b/src/loader/loader.go index 96765ed4..349782f5 100644 --- a/src/loader/loader.go +++ b/src/loader/loader.go @@ -100,6 +100,7 @@ func PrintConfigDetails() { logger.Config.Debug(fmt.Sprintf("Branch: %s", config.Branch)) logger.Config.Debug(fmt.Sprintf("GameServerAppID: %s", config.GameServerAppID)) logger.Config.Debug(fmt.Sprintf("Version: %s", config.Version)) + logger.Config.Debug(fmt.Sprintf("IsNewTerrainAndSaveSystem: %v", config.IsNewTerrainAndSaveSystem)) logger.Config.Debug("---- UPDATER CONFIG VARS ----") logger.Config.Debug(fmt.Sprintf("AllowPrereleaseUpdates: %v", config.AllowPrereleaseUpdates)) @@ -110,3 +111,7 @@ func PrintConfigDetails() { logger.Config.Debug(fmt.Sprintf("SSCMFilePath: %s", config.SSCMFilePath)) logger.Config.Debug(fmt.Sprintf("IsSSCMEnabled: %v", config.IsSSCMEnabled)) } + +func RestartBackend() { + setup.RestartMySelf() +} diff --git a/src/setup/updater.go b/src/setup/updater.go index 0c5e9f61..0704b189 100644 --- a/src/setup/updater.go +++ b/src/setup/updater.go @@ -135,6 +135,27 @@ func UpdateExecutable() error { return nil } +func RestartMySelf() { + currentExe, err := os.Executable() + if err != nil { + logger.Install.Warn(fmt.Sprintf("⚠️ Restart failed: couldn’t get current executable path: %v. Keeping version %s.", err, config.Version)) + return + } + + if runtime.GOOS == "windows" { + if err := runAndExit(currentExe); err != nil { + logger.Install.Warn(fmt.Sprintf("⚠️ Restart failed: couldn’t launch %s: %v. Keeping version %s.", currentExe, err, config.Version)) + return + } + } + if runtime.GOOS == "linux" { + if err := runAndExitLinux(currentExe); err != nil { + logger.Install.Warn(fmt.Sprintf("⚠️ Restart failed: couldn’t launch %s: %v. Keeping version %s.", currentExe, err, config.Version)) + return + } + } +} + // parseVersion parses a version string (e.g., "4.6.10") into a Version struct and tries to handle a few culprits too func parseVersion(v string) (Version, error) { v = strings.TrimPrefix(v, "v") diff --git a/src/terminal/runtimecommands.go b/src/terminal/runtimecommands.go new file mode 100644 index 00000000..816dd01c --- /dev/null +++ b/src/terminal/runtimecommands.go @@ -0,0 +1,177 @@ +// Package misc provides a non-blocking command-line interface for entering commands +// while allowing the application to continue its operations normally. +package terminal + +import ( + "bufio" + "errors" + "fmt" + "os" + "sort" + "strings" + "sync" + "time" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/gamemgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/loader" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" +) + +// ANSI escape codes for green text and reset +const ( + cliPrompt = "\033[32m" + "SSUICLI" + " » " + "\033[0m" +) + +// CommandFunc defines the signature for command handler functions. +type CommandFunc func(args []string) error + +// commandRegistry holds the map of command names to their handler functions. +var commandRegistry = make(map[string]CommandFunc) +var mu sync.Mutex + +var commandAliases = make(map[string][]string) + +// RegisterCommand adds a new command and its handler to the registry. +func RegisterCommand(name string, handler CommandFunc, aliases ...string) { + mu.Lock() + defer mu.Unlock() + commandRegistry[name] = handler + if len(aliases) > 0 { + commandAliases[name] = append(commandAliases[name], aliases...) + for _, alias := range aliases { + commandRegistry[alias] = handler + } + } +} + +// StartConsole starts a non-blocking console input loop in a separate goroutine. +func StartConsole(wg *sync.WaitGroup) { + if !config.IsConsoleEnabled { + logger.Core.Warn("Console is disabled, skipping...") + return + } + wg.Add(1) + go func() { + defer wg.Done() + scanner := bufio.NewScanner(os.Stdin) + logger.Core.Info("Console input started. Type 'help' for commands.") + time.Sleep(10 * time.Millisecond) + + for { + fmt.Print(cliPrompt) + os.Stdout.Sync() // Force flush the output buffer + if !scanner.Scan() { + break + } + input := strings.TrimSpace(scanner.Text()) + if input == "" { + continue + } + ProcessCommand(input) + } + + if err := scanner.Err(); err != nil { + logger.Core.Error("Console input error:" + err.Error()) + } + logger.Core.Info("Console input stopped.") + }() +} + +// ProcessCommand parses and executes a command from the input string. +func ProcessCommand(input string) { + args := strings.Fields(input) + if len(args) == 0 { + return + } + + commandName := strings.ToLower(args[0]) + args = args[1:] // Remove command name from args + + mu.Lock() + handler, exists := commandRegistry[commandName] + mu.Unlock() + + if !exists { + logger.Core.Error("Unknown command:" + commandName + ". Type 'help' for available commands.") + return + } + + if err := handler(args); err != nil { + logger.Core.Error("Command " + commandName + " failed:" + err.Error()) + } +} + +// WrapNoReturn wraps a function with no return value to match CommandFunc. +func WrapNoReturn(fn func()) CommandFunc { + return func(args []string) error { + if len(args) > 0 { + return errors.New("command does not accept arguments") + } + fn() + logger.Core.Info("Runtime CLI Command executed successfully") + return nil + } +} + +// helpCommand displays available commands along with their aliases. +func helpCommand(args []string) error { + mu.Lock() + defer mu.Unlock() + logger.Core.Info("Available commands:") + // Collect primary commands (those in commandAliases keys) + primaryCommands := make([]string, 0, len(commandAliases)) + for cmd := range commandAliases { + primaryCommands = append(primaryCommands, cmd) + } + sort.Strings(primaryCommands) + for _, cmd := range primaryCommands { + aliases := commandAliases[cmd] + if len(aliases) > 0 { + logger.Core.Info("- " + cmd + " (aliases: " + strings.Join(aliases, ", ") + ")") + } else { + logger.Core.Info("- %s" + cmd) + } + } + return nil +} + +// init registers default cli commands and their aliases. +func init() { + RegisterCommand("help", helpCommand, "h") + RegisterCommand("reloadbackend", WrapNoReturn(loader.ReloadAll), "rlb", "rb", "r") + RegisterCommand("reloadconfig", WrapNoReturn(loader.ReloadConfig), "rlc", "rc") + RegisterCommand("restartbackend", WrapNoReturn(loader.RestartBackend), "rsb") + RegisterCommand("exit", WrapNoReturn(exitfromcli), "e") + RegisterCommand("deleteconfig", WrapNoReturn(deleteConfig), "delc", "dc") + RegisterCommand("startserver", WrapNoReturn(startServer), "start") + RegisterCommand("stopserver", WrapNoReturn(stopServer), "stop") +} + +func startServer() { + err := gamemgr.InternalStartServer() + if err != nil { + logger.Core.Error("Error starting server:" + err.Error()) + } +} +func stopServer() { + err := gamemgr.InternalStopServer() + if err != nil { + logger.Core.Error("Error stopping server:" + err.Error()) + } +} + +func exitfromcli() { + // send signal to the main process to exit + logger.Core.Info("I have to go...") + os.Exit(0) +} + +func deleteConfig() { + //remove file at config.ConfigPath + if err := os.Remove(config.ConfigPath); err != nil { + logger.Core.Error("Error deleting config file: " + err.Error()) + return + } + logger.Core.Info("Config file deleted successfully") +} diff --git a/src/terminal/terminalmsg.go b/src/terminal/terminalmsg.go new file mode 100644 index 00000000..25b0b750 --- /dev/null +++ b/src/terminal/terminalmsg.go @@ -0,0 +1,58 @@ +package terminal + +import ( + "fmt" + "runtime" + "time" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" +) + +// PrintStartupMessage prints a stylish startup message to the terminal +func PrintStartupMessage() { + // Clear some space + fmt.Println() + fmt.Println() + + // Main ASCII art logo + fmt.Println(" ███████╗████████╗ █████╗ ████████╗██╗ ██████╗ ███╗ ██╗███████╗███████╗██████╗ ███████╗ ███████╗██╗ ██╗██╗") + fmt.Println(" ██╔════╝╚══██╔══╝██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║██╔════╝██╔════╝██╔══██╗██╔════╝ ██╔════╝██║ ██║██║") + fmt.Println(" ███████╗ ██║ ███████║ ██║ ██║██║ ██║██╔██╗ ██║█████╗ █████╗ ██████╔╝███████╗█████╗███████╗██║ ██║██║") + fmt.Println(" ╚════██║ ██║ ██╔══██║ ██║ ██║██║ ██║██║╚██╗██║██╔══╝ ██╔══╝ ██╔══██╗╚════██║╚════╝╚════██║██║ ██║██║") + fmt.Println(" ███████║ ██║ ██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║███████╗███████╗██║ ██║███████║ ███████║╚██████╔╝██║") + fmt.Println(" ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚══════╝╚═╝ ╚═╝╚══════╝ ╚══════╝ ╚═════╝ ╚═╝") + + // Decorative line + fmt.Println(" ╔═══════════════════════════════════════════════════════════════════════════════════════════════════╗") + // Tagline + fmt.Println(" ║ 🎮 YOUR ONE-STOP SHOP FOR RUNNING A STATIONEERS SERVER 🎮 ║") + // System info + fmt.Printf(" ║ 🚀 Version: %s 📅 %s 💻 Runtime: %s/%s ║\n", + config.Version, + time.Now().Format("2006-01-02 15:04:05"), + runtime.GOOS, + runtime.GOARCH) + // Decorative line + fmt.Println(" ╚═══════════════════════════════════════════════════════════════════════════════════════════════════╝") + + // Web UI info + fmt.Println("\n 🌐 Web UI available at: https://localhost:8443 (default) or https://:8443") + fmt.Println("\n 🌐 Support available at: https://discord.gg/8n3vN92MyJ") + + // Quote + fmt.Println("\n JacksonTheMaster: \"Managing game servers shouldn't be rocket science... unless it's a rocket game!\"") +} + +func PrintFirstTimeSetupMessage() { + // Setup guide + fmt.Println(" 📋 GETTING STARTED:") + fmt.Println(" ┌─────────────────────────────────────────────────────────────────────────────────────────────┐") + fmt.Println(" │ • Ready, set, go! Welcome to StationeersServerUI, new User! │") + fmt.Println(" │ • The good news: you made it here, which means you are likely ready to run your server! │") + fmt.Println(" │ • If this is your first time here, no worries: SSUI is made to be easy to use. │") + fmt.Println(" │ • Configure your server by visiting the WebUI! │") + fmt.Println(" │ • Support is provided at https://discord.gg/8n3vN92MyJ │") + fmt.Println(" │ • For more details, check the GitHub Wiki: │") + fmt.Println(" │ • https://github.com/JacksonTheMaster/StationeersServerUI/v5/wiki │") + fmt.Println(" └─────────────────────────────────────────────────────────────────────────────────────────────┘") +} diff --git a/src/web/TwoBoxForm.go b/src/web/TwoBoxForm.go index 3153e57d..dca08ece 100644 --- a/src/web/TwoBoxForm.go +++ b/src/web/TwoBoxForm.go @@ -140,7 +140,7 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { SecondaryLabelType: "hidden", SubmitButtonText: "Save & Continue", SkipButtonText: "Use Release Version", - ConfigField: "GameBranch", + ConfigField: "gameBranch", NextStep: "network_config_choice", }, diff --git a/src/web/http.go b/src/web/http.go index a075fcb6..4adc70a3 100644 --- a/src/web/http.go +++ b/src/web/http.go @@ -119,6 +119,15 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { serverVisibleFalseSelected = "selected" } + isNewTerrainAndSaveSystemTrueSelected := "" + isNewTerrainAndSaveSystemFalseSelected := "" + + if config.IsNewTerrainAndSaveSystem { + isNewTerrainAndSaveSystemTrueSelected = "selected" + } else { + isNewTerrainAndSaveSystemFalseSelected = "selected" + } + steamP2PTrueSelected := "" steamP2PFalseSelected := "" if config.UseSteamP2P { @@ -129,50 +138,56 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { // Replace placeholders in the HTML with actual config values replacements := map[string]string{ - "{{discordToken}}": config.DiscordToken, - "{{controlChannelID}}": config.ControlChannelID, - "{{statusChannelID}}": config.StatusChannelID, - "{{connectionListChannelID}}": config.ConnectionListChannelID, - "{{logChannelID}}": config.LogChannelID, - "{{saveChannelID}}": config.SaveChannelID, - "{{controlPanelChannelID}}": config.ControlPanelChannelID, - "{{blackListFilePath}}": config.BlackListFilePath, - "{{errorChannelID}}": config.ErrorChannelID, - "{{isDiscordEnabled}}": fmt.Sprintf("%v", config.IsDiscordEnabled), - "{{IsDiscordEnabledTrueSelected}}": discordTrueSelected, - "{{IsDiscordEnabledFalseSelected}}": discordFalseSelected, - "{{gameBranch}}": config.GameBranch, - "{{ServerName}}": config.ServerName, - "{{SaveInfo}}": config.SaveInfo, - "{{ServerMaxPlayers}}": config.ServerMaxPlayers, - "{{ServerPassword}}": config.ServerPassword, - "{{ServerAuthSecret}}": config.ServerAuthSecret, - "{{AdminPassword}}": config.AdminPassword, - "{{GamePort}}": config.GamePort, - "{{UpdatePort}}": config.UpdatePort, - "{{UPNPEnabled}}": fmt.Sprintf("%v", config.UPNPEnabled), - "{{UPNPEnabledTrueSelected}}": upnpTrueSelected, - "{{UPNPEnabledFalseSelected}}": upnpFalseSelected, - "{{AutoSave}}": fmt.Sprintf("%v", config.AutoSave), - "{{AutoSaveTrueSelected}}": autoSaveTrueSelected, - "{{AutoSaveFalseSelected}}": autoSaveFalseSelected, - "{{SaveInterval}}": config.SaveInterval, - "{{AutoPauseServer}}": fmt.Sprintf("%v", config.AutoPauseServer), - "{{AutoPauseServerTrueSelected}}": autoPauseTrueSelected, - "{{AutoPauseServerFalseSelected}}": autoPauseFalseSelected, - "{{LocalIpAddress}}": config.LocalIpAddress, - "{{StartLocalHost}}": fmt.Sprintf("%v", config.StartLocalHost), - "{{StartLocalHostTrueSelected}}": startLocalTrueSelected, - "{{StartLocalHostFalseSelected}}": startLocalFalseSelected, - "{{ServerVisible}}": fmt.Sprintf("%v", config.ServerVisible), - "{{ServerVisibleTrueSelected}}": serverVisibleTrueSelected, - "{{ServerVisibleFalseSelected}}": serverVisibleFalseSelected, - "{{UseSteamP2P}}": fmt.Sprintf("%v", config.UseSteamP2P), - "{{UseSteamP2PTrueSelected}}": steamP2PTrueSelected, - "{{UseSteamP2PFalseSelected}}": steamP2PFalseSelected, - "{{ExePath}}": config.ExePath, - "{{AdditionalParams}}": config.AdditionalParams, - "{{AutoRestartServerTimer}}": config.AutoRestartServerTimer, + "{{discordToken}}": config.DiscordToken, + "{{controlChannelID}}": config.ControlChannelID, + "{{statusChannelID}}": config.StatusChannelID, + "{{connectionListChannelID}}": config.ConnectionListChannelID, + "{{logChannelID}}": config.LogChannelID, + "{{saveChannelID}}": config.SaveChannelID, + "{{controlPanelChannelID}}": config.ControlPanelChannelID, + "{{blackListFilePath}}": config.BlackListFilePath, + "{{errorChannelID}}": config.ErrorChannelID, + "{{isDiscordEnabled}}": fmt.Sprintf("%v", config.IsDiscordEnabled), + "{{IsDiscordEnabledTrueSelected}}": discordTrueSelected, + "{{IsDiscordEnabledFalseSelected}}": discordFalseSelected, + "{{gameBranch}}": config.GameBranch, + "{{Difficulty}}": config.Difficulty, + "{{StartCondition}}": config.StartCondition, + "{{StartLocation}}": config.StartLocation, + "{{ServerName}}": config.ServerName, + "{{SaveInfo}}": config.SaveInfo, + "{{ServerMaxPlayers}}": config.ServerMaxPlayers, + "{{ServerPassword}}": config.ServerPassword, + "{{ServerAuthSecret}}": config.ServerAuthSecret, + "{{AdminPassword}}": config.AdminPassword, + "{{GamePort}}": config.GamePort, + "{{UpdatePort}}": config.UpdatePort, + "{{UPNPEnabled}}": fmt.Sprintf("%v", config.UPNPEnabled), + "{{UPNPEnabledTrueSelected}}": upnpTrueSelected, + "{{UPNPEnabledFalseSelected}}": upnpFalseSelected, + "{{AutoSave}}": fmt.Sprintf("%v", config.AutoSave), + "{{AutoSaveTrueSelected}}": autoSaveTrueSelected, + "{{AutoSaveFalseSelected}}": autoSaveFalseSelected, + "{{SaveInterval}}": config.SaveInterval, + "{{AutoPauseServer}}": fmt.Sprintf("%v", config.AutoPauseServer), + "{{AutoPauseServerTrueSelected}}": autoPauseTrueSelected, + "{{AutoPauseServerFalseSelected}}": autoPauseFalseSelected, + "{{LocalIpAddress}}": config.LocalIpAddress, + "{{StartLocalHost}}": fmt.Sprintf("%v", config.StartLocalHost), + "{{StartLocalHostTrueSelected}}": startLocalTrueSelected, + "{{StartLocalHostFalseSelected}}": startLocalFalseSelected, + "{{ServerVisible}}": fmt.Sprintf("%v", config.ServerVisible), + "{{ServerVisibleTrueSelected}}": serverVisibleTrueSelected, + "{{ServerVisibleFalseSelected}}": serverVisibleFalseSelected, + "{{UseSteamP2P}}": fmt.Sprintf("%v", config.UseSteamP2P), + "{{UseSteamP2PTrueSelected}}": steamP2PTrueSelected, + "{{UseSteamP2PFalseSelected}}": steamP2PFalseSelected, + "{{ExePath}}": config.ExePath, + "{{AdditionalParams}}": config.AdditionalParams, + "{{AutoRestartServerTimer}}": config.AutoRestartServerTimer, + "{{IsNewTerrainAndSaveSystem}}": fmt.Sprintf("%v", config.IsNewTerrainAndSaveSystem), + "{{IsNewTerrainAndSaveSystemTrueSelected}}": isNewTerrainAndSaveSystemTrueSelected, + "{{IsNewTerrainAndSaveSystemFalseSelected}}": isNewTerrainAndSaveSystemFalseSelected, } for placeholder, value := range replacements { diff --git a/src/web/start.go b/src/web/start.go index 36aed086..90659d7a 100644 --- a/src/web/start.go +++ b/src/web/start.go @@ -12,6 +12,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/detectionmgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/security" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/terminal" ) func StartWebServer(wg *sync.WaitGroup) { @@ -77,12 +78,9 @@ func StartWebServer(wg *sync.WaitGroup) { wg.Add(1) go func() { defer wg.Done() - logger.Web.Info("Starting the HTTP server on port 8443...") - logger.Web.Info("UI available at: https://0.0.0.0:8443 or https://localhost:8443") + terminal.PrintStartupMessage() if config.IsFirstTimeSetup { - logger.Web.Error("For first-time setup, visit the UI to configure a user or skip authentication.") - logger.Web.Warn("Fill the Username and Password fields, then click Register User and when done Finalize Setup.") - logger.Web.Warn("For more details, check the GitHub Wiki: https://github.com/JacksonTheMaster/StationeersServerUI/v5/wiki") + terminal.PrintFirstTimeSetupMessage() } // Ensure TLS certs are ready if err := security.EnsureTLSCerts(); err != nil {