From 73c6a051e58afb908ea0120040a3550ac9f44e0b Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sat, 2 Aug 2025 17:23:26 +0200 Subject: [PATCH 01/14] (fix) update ConfigField to lowercase "gameBranch" in ServeTwoBoxFormTemplate --- src/web/TwoBoxForm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", }, From 3d218a92f8ab4c38c0184c86992ec704090e0972 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sat, 2 Aug 2025 17:24:07 +0200 Subject: [PATCH 02/14] update gitignore --- .gitignore | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 96964aac..2a583a6a 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,15 @@ 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 From 42dd9b6c484a8c0b98343c5f1215355aaab84041 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 3 Aug 2025 04:54:44 +0200 Subject: [PATCH 03/14] added support for .save files to backup manager. Watches the autosave dir of the worldFolder atm technically supports old system too --- src/backupmgr/cleanup.go | 46 ++++++++++++++++++--------- src/backupmgr/manager.go | 12 ++++++- src/backupmgr/restore.go | 68 +++++++++++++++++++++++++++++----------- src/backupmgr/utils.go | 58 ++++++++++++++++++++++++++++------ src/backupmgr/watcher.go | 20 ++++++++++-- src/config/config.go | 6 ++-- 6 files changed, 160 insertions(+), 50 deletions(-) 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..6ac164df 100644 --- a/src/backupmgr/manager.go +++ b/src/backupmgr/manager.go @@ -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..b393745b 100644 --- a/src/backupmgr/restore.go +++ b/src/backupmgr/restore.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" @@ -15,32 +16,63 @@ 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/"+config.WorldName, filepath.Base(backupFile)) 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 + 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/"+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..6f8e7e9a 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -11,8 +11,8 @@ 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 { @@ -218,6 +218,6 @@ func applyConfig(cfg *JsonConfig) { } // Set backup paths - ConfiguredBackupDir = filepath.Join("./saves/", WorldName, "Backup") + ConfiguredBackupDir = filepath.Join("./saves/", WorldName, "autosave") ConfiguredSafeBackupDir = filepath.Join("./saves/", WorldName, "Safebackups") } From d2dd118228c8f88f17ee2fea52061e2039db09ed Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 3 Aug 2025 05:31:48 +0200 Subject: [PATCH 04/14] updated error message to reflect new term "autosave watcher" in BackupManager --- src/backupmgr/manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backupmgr/manager.go b/src/backupmgr/manager.go index 6ac164df..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 From ad2632920aa8287d990e2dc2969749742e326795 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 3 Aug 2025 16:39:13 +0200 Subject: [PATCH 05/14] refactor restore process: remove unnecesary config import (have m.config), enhance .save file handling --- src/backupmgr/restore.go | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/backupmgr/restore.go b/src/backupmgr/restore.go index b393745b..8d33ac40 100644 --- a/src/backupmgr/restore.go +++ b/src/backupmgr/restore.go @@ -6,7 +6,6 @@ import ( "path/filepath" "strings" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" ) @@ -39,8 +38,31 @@ func (m *BackupManager) RestoreBackup(index int) error { if targetGroup.BinFile != "" && strings.HasSuffix(targetGroup.BinFile, ".save") { // .save file case backupFile := targetGroup.BinFile - destFile := filepath.Join("./saves/"+config.WorldName, filepath.Base(backupFile)) + destFile := filepath.Join("./saves/"+m.config.WorldName, filepath.Base(backupFile)) + // 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 + savedPreviousHeadSaveFilePath := filepath.Join(m.config.SafeBackupDir, fmt.Sprintf("%s_%s", "_oldHeadSaveBackup", 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) + } + } + + // 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) @@ -60,7 +82,7 @@ func (m *BackupManager) RestoreBackup(index int) error { for _, file := range files { backupFile := filepath.Join(m.config.SafeBackupDir, file.backupName) - destFile := filepath.Join("./saves/"+config.WorldName, file.destName) + destFile := filepath.Join("./saves/"+m.config.WorldName, file.destName) if err := copyFile(backupFile, destFile); err != nil { // Try alternative name From c1ca553602560da301b7098cc47d80735ba9a2cf Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 3 Aug 2025 16:39:43 +0200 Subject: [PATCH 06/14] add EnableDotSaves config option to steer .save file handling, update UI and paths accordingly --- UIMod/ui/config.html | 11 +++++++++++ src/config/config.go | 17 +++++++++++++++-- src/config/vars.go | 1 + src/loader/loader.go | 1 + src/web/http.go | 13 +++++++++++++ 5 files changed, 41 insertions(+), 2 deletions(-) diff --git a/UIMod/ui/config.html b/UIMod/ui/config.html index 8f350593..400da519 100644 --- a/UIMod/ui/config.html +++ b/UIMod/ui/config.html @@ -220,6 +220,17 @@

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.
+ +
+ + +
Set to TRUE to enable handling of .save files in the Backup manager. Defaults to false, which will use the old trio of world_meta.xml, world.xml, and world.bin files.
+
+ + diff --git a/src/config/config.go b/src/config/config.go index 6f8e7e9a..8a4c062e 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -34,6 +34,7 @@ type JsonConfig struct { BackupKeepMonthlyFor int `json:"backupKeepMonthlyFor"` BackupCleanupInterval int `json:"backupCleanupInterval"` BackupWaitTime int `json:"backupWaitTime"` + EnableDotSaves *bool `json:"EnableDotSaves"` GameBranch string `json:"gameBranch"` ServerName string `json:"ServerName"` SaveInfo string `json:"SaveInfo"` @@ -131,6 +132,11 @@ 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.EnableDotSaves, "ENABLE_DOT_SAVES", false) + EnableDotSaves = dotSaveVal + cfg.EnableDotSaves = &dotSaveVal + GameBranch = getString(cfg.GameBranch, "GAME_BRANCH", "public") ServerName = getString(cfg.ServerName, "SERVER_NAME", "Stationeers Server UI") SaveInfo = getString(cfg.SaveInfo, "SAVE_INFO", "Moon Moon") @@ -217,7 +223,14 @@ func applyConfig(cfg *JsonConfig) { BackupWorldName = parts[1] } - // Set backup paths - ConfiguredBackupDir = filepath.Join("./saves/", WorldName, "autosave") + // Set backup paths for old or new style saves + if EnableDotSaves { + // use new new style autosave folder + ConfiguredBackupDir = filepath.Join("./saves/", WorldName, "autosave") + } else { + // use old style Backups folder + ConfiguredBackupDir = filepath.Join("./saves/", WorldName, "Backups") + } + // 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..fbed2bf1 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -92,6 +92,7 @@ var ( ConfiguredBackupDir string ConfiguredSafeBackupDir string BackupWaitTime time.Duration + EnableDotSaves bool ) // Authentication and security diff --git a/src/loader/loader.go b/src/loader/loader.go index 96765ed4..b8c0447b 100644 --- a/src/loader/loader.go +++ b/src/loader/loader.go @@ -91,6 +91,7 @@ func PrintConfigDetails() { logger.Config.Debug(fmt.Sprintf("ConfiguredBackupDir: %s", config.ConfiguredBackupDir)) logger.Config.Debug(fmt.Sprintf("ConfiguredSafeBackupDir: %s", config.ConfiguredSafeBackupDir)) logger.Config.Debug(fmt.Sprintf("BackupWaitTime: %s", config.BackupWaitTime)) + logger.Config.Debug(fmt.Sprintf("EnableDotSaves: %v", config.EnableDotSaves)) logger.Config.Debug("---- AUTHENTICATION CONFIG VARS ----") logger.Config.Debug(fmt.Sprintf("AuthTokenLifetime: %d", config.AuthTokenLifetime)) diff --git a/src/web/http.go b/src/web/http.go index a075fcb6..a80e6aaa 100644 --- a/src/web/http.go +++ b/src/web/http.go @@ -119,6 +119,16 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { serverVisibleFalseSelected = "selected" } + enableDotSavesTrueSelected := "" + enableDotSavesFalseSelected := "" + if config.EnableDotSaves { + enableDotSavesTrueSelected = "selected" + logger.Config.Debug("enableDotSavesTrueSelected") + } else { + enableDotSavesFalseSelected = "selected" + logger.Config.Debug("enableDotSavesFalseSelected") + } + steamP2PTrueSelected := "" steamP2PFalseSelected := "" if config.UseSteamP2P { @@ -173,6 +183,9 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { "{{ExePath}}": config.ExePath, "{{AdditionalParams}}": config.AdditionalParams, "{{AutoRestartServerTimer}}": config.AutoRestartServerTimer, + "{{EnableDotSaves}}": fmt.Sprintf("%v", config.EnableDotSaves), + "{{EnableDotSavesTrueSelected}}": enableDotSavesTrueSelected, + "{{EnableDotSavesFalseSelected}}": enableDotSavesFalseSelected, } for placeholder, value := range replacements { From 8cbbc00cc68ecf976dec74ea1ba9c2cdff072a87 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 3 Aug 2025 16:48:45 +0200 Subject: [PATCH 07/14] enhance restore logic: timestamp existing .save backups to avoid overwrites and improve file naming conventions --- src/backupmgr/restore.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/backupmgr/restore.go b/src/backupmgr/restore.go index 8d33ac40..ed0fed23 100644 --- a/src/backupmgr/restore.go +++ b/src/backupmgr/restore.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" ) @@ -38,7 +39,7 @@ func (m *BackupManager) RestoreBackup(index int) error { if targetGroup.BinFile != "" && strings.HasSuffix(targetGroup.BinFile, ".save") { // .save file case backupFile := targetGroup.BinFile - destFile := filepath.Join("./saves/"+m.config.WorldName, filepath.Base(backupFile)) + destFile := filepath.Join("./saves/"+m.config.WorldName, m.config.WorldName+".save") // Before restore, check if we have existing .save files in the root saves/WorldName dir saveDir := filepath.Join("./saves/", m.config.WorldName) @@ -54,7 +55,8 @@ func (m *BackupManager) RestoreBackup(index int) error { if strings.HasSuffix(file.Name(), ".save") { existingFile := filepath.Join(saveDir, file.Name()) // Move existing .save file to SafeBackupDir with timestamp to avoid overwrites - savedPreviousHeadSaveFilePath := filepath.Join(m.config.SafeBackupDir, fmt.Sprintf("%s_%s", "_oldHeadSaveBackup", file.Name())) + 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) } From e3f69ead123aa661ac025c1e13a3b880c10450dd Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 3 Aug 2025 20:18:38 +0200 Subject: [PATCH 08/14] update go.sum: update fsnotify, gorilla websocket, crypto, sys --- go.sum | 7 ------- 1 file changed, 7 deletions(-) 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= From e0b1eb6299d4c94270ab2ad41c5c418271997a0c Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 3 Aug 2025 20:28:13 +0200 Subject: [PATCH 09/14] (newterrain) add difficulty, start condition, and start location options to config; update UI and command argument handling accordingly to handle both old and new system --- UIMod/ui/config.html | 21 ++++++++++++ src/config/config.go | 6 ++++ src/config/vars.go | 3 ++ src/gamemgr/args.go | 79 +++++++++++++++++++++++++++++++++----------- src/web/http.go | 3 ++ 5 files changed, 92 insertions(+), 20 deletions(-) diff --git a/UIMod/ui/config.html b/UIMod/ui/config.html index 400da519..4e1138fd 100644 --- a/UIMod/ui/config.html +++ b/UIMod/ui/config.html @@ -77,6 +77,27 @@

Basic Server Settings

World type to generate. (MyMoonMap Moon) +
+ + +
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.
+
+
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.EnableDotSaves { + 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/web/http.go b/src/web/http.go index a80e6aaa..dc661a90 100644 --- a/src/web/http.go +++ b/src/web/http.go @@ -152,6 +152,9 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { "{{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, From 6a70520ef76edf1de7750648b8adef8aab923ed8 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 3 Aug 2025 20:29:54 +0200 Subject: [PATCH 10/14] add supermaven log file to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2a583a6a..e622e1c3 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ run_bepinex.sh debug.log modconfig.xml UIMod/config/config.json +C:/custom/file.txt From 5543edbd884bd6d788970f78b54661146d6095b3 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 3 Aug 2025 21:04:30 +0200 Subject: [PATCH 11/14] (newterrain) rename EnableDotSaves with IsNewTerrainAndSaveSystem as it has a broader use now --- UIMod/ui/config.html | 16 +++--- src/config/config.go | 116 +++++++++++++++++++++---------------------- src/config/vars.go | 20 ++++---- src/gamemgr/args.go | 4 +- src/loader/loader.go | 2 +- src/web/http.go | 113 +++++++++++++++++++++-------------------- 6 files changed, 135 insertions(+), 136 deletions(-) diff --git a/UIMod/ui/config.html b/UIMod/ui/config.html index 4e1138fd..a7d5cce8 100644 --- a/UIMod/ui/config.html +++ b/UIMod/ui/config.html @@ -81,21 +81,21 @@

Basic Server Settings

-
Difficulty to create the world with. Defaults to Normal if empty.
+
CURRENTLY STATIONEERS BETA ONLY: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.
+
CURRENTLY STATIONEERS BETA ONLY: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.
+
CURRENTLY STATIONEERS BETA ONLY: Start location to create the world with. Defaults to "DefaultStartLocation" if empty.
@@ -243,12 +243,12 @@

Advanced Configuration

- - + + -
Set to TRUE to enable handling of .save files in the Backup manager. Defaults to false, which will use the old trio of world_meta.xml, world.xml, and world.bin files.
+
CURRENTLY STATIONEERS BETA ONLY: 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.
diff --git a/src/config/config.go b/src/config/config.go index 11c0c9a7..fe0f4067 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -16,60 +16,60 @@ var ( ) 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"` - EnableDotSaves *bool `json:"EnableDotSaves"` - 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"` + 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"` } type CustomDetection struct { @@ -136,9 +136,9 @@ func applyConfig(cfg *JsonConfig) { 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.EnableDotSaves, "ENABLE_DOT_SAVES", false) - EnableDotSaves = dotSaveVal - cfg.EnableDotSaves = &dotSaveVal + 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", "") @@ -230,7 +230,7 @@ func applyConfig(cfg *JsonConfig) { } // Set backup paths for old or new style saves - if EnableDotSaves { + if IsNewTerrainAndSaveSystem { // use new new style autosave folder ConfiguredBackupDir = filepath.Join("./saves/", WorldName, "autosave") } else { diff --git a/src/config/vars.go b/src/config/vars.go index 7ced40f7..afd5e918 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -86,16 +86,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 - EnableDotSaves bool + 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 2f884794..08eaf448 100644 --- a/src/gamemgr/args.go +++ b/src/gamemgr/args.go @@ -19,7 +19,7 @@ type Arg struct { func buildCommandArgs() []string { var argOrder []Arg - if config.EnableDotSaves { + if config.IsNewTerrainAndSaveSystem { argOrder = []Arg{ {Flag: "-nographics", RequiresValue: false}, {Flag: "-batchmode", RequiresValue: false}, @@ -54,7 +54,7 @@ func buildCommandArgs() []string { {Flag: "AdminPassword", Value: config.AdminPassword, Condition: func() bool { return config.AdminPassword != "" }, RequiresValue: true}, } } - if !config.EnableDotSaves { + if !config.IsNewTerrainAndSaveSystem { argOrder = []Arg{ {Flag: "-nographics", RequiresValue: false}, {Flag: "-batchmode", RequiresValue: false}, diff --git a/src/loader/loader.go b/src/loader/loader.go index b8c0447b..70576b9d 100644 --- a/src/loader/loader.go +++ b/src/loader/loader.go @@ -91,7 +91,6 @@ func PrintConfigDetails() { logger.Config.Debug(fmt.Sprintf("ConfiguredBackupDir: %s", config.ConfiguredBackupDir)) logger.Config.Debug(fmt.Sprintf("ConfiguredSafeBackupDir: %s", config.ConfiguredSafeBackupDir)) logger.Config.Debug(fmt.Sprintf("BackupWaitTime: %s", config.BackupWaitTime)) - logger.Config.Debug(fmt.Sprintf("EnableDotSaves: %v", config.EnableDotSaves)) logger.Config.Debug("---- AUTHENTICATION CONFIG VARS ----") logger.Config.Debug(fmt.Sprintf("AuthTokenLifetime: %d", config.AuthTokenLifetime)) @@ -101,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)) diff --git a/src/web/http.go b/src/web/http.go index dc661a90..4adc70a3 100644 --- a/src/web/http.go +++ b/src/web/http.go @@ -119,14 +119,13 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { serverVisibleFalseSelected = "selected" } - enableDotSavesTrueSelected := "" - enableDotSavesFalseSelected := "" - if config.EnableDotSaves { - enableDotSavesTrueSelected = "selected" - logger.Config.Debug("enableDotSavesTrueSelected") + isNewTerrainAndSaveSystemTrueSelected := "" + isNewTerrainAndSaveSystemFalseSelected := "" + + if config.IsNewTerrainAndSaveSystem { + isNewTerrainAndSaveSystemTrueSelected = "selected" } else { - enableDotSavesFalseSelected = "selected" - logger.Config.Debug("enableDotSavesFalseSelected") + isNewTerrainAndSaveSystemFalseSelected = "selected" } steamP2PTrueSelected := "" @@ -139,56 +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, - "{{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, - "{{EnableDotSaves}}": fmt.Sprintf("%v", config.EnableDotSaves), - "{{EnableDotSavesTrueSelected}}": enableDotSavesTrueSelected, - "{{EnableDotSavesFalseSelected}}": enableDotSavesFalseSelected, + "{{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 { From 9bb8bb3ac7bd5ec8d4fd0d704db2b3c190e6c45f Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 3 Aug 2025 21:05:27 +0200 Subject: [PATCH 12/14] add stationeers beta settings section to config; added gameBranch to config page with restart notice --- UIMod/ui/config.html | 60 +++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/UIMod/ui/config.html b/UIMod/ui/config.html index a7d5cce8..4209df40 100644 --- a/UIMod/ui/config.html +++ b/UIMod/ui/config.html @@ -50,6 +50,7 @@

Server Configuration

+
@@ -76,27 +77,6 @@

Basic Server Settings

provide the World type to generate. (MyMoonMap Moon) - -
- - -
CURRENTLY STATIONEERS BETA ONLY:Difficulty to create the world with. Defaults to Normal if empty.
-
- -
- - -
CURRENTLY STATIONEERS BETA ONLY:Start condition to create the world with. Defaults to the default start condition for the world type if empty.
-
- -
- - -
CURRENTLY STATIONEERS BETA ONLY: Start location to create the world with. Defaults to "DefaultStartLocation" if empty.
-
@@ -242,16 +222,50 @@

Advanced Configuration

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..

+
-
CURRENTLY STATIONEERS BETA ONLY: 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.
+
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.
+
From 183b6e24211780c7911b7065f8d92fe25ca6158c Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 3 Aug 2025 21:33:38 +0200 Subject: [PATCH 13/14] (fix) correct legacy backup dir --- src/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.go b/src/config/config.go index fe0f4067..148ebbbe 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -235,7 +235,7 @@ func applyConfig(cfg *JsonConfig) { ConfiguredBackupDir = filepath.Join("./saves/", WorldName, "autosave") } else { // use old style Backups folder - ConfiguredBackupDir = filepath.Join("./saves/", WorldName, "Backups") + ConfiguredBackupDir = filepath.Join("./saves/", WorldName, "Backup") } // use Safebackups folder either way. ConfiguredSafeBackupDir = filepath.Join("./saves/", WorldName, "Safebackups") From 4c0564f190ad52c223d4cc47e98be3987a0b0e6c Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 3 Aug 2025 22:08:22 +0200 Subject: [PATCH 14/14] merged nice features over from SteamServerUI: -added SSUICLI (and some runtime commands), default is disabled, config -> IsConsoleEnabled -added fancy startup message -added setup.RestartMySelf(), can be triggered via SSUICLI -updated isFirstTimeSetup message --- server.go | 3 + src/config/config.go | 5 + src/config/vars.go | 1 + src/loader/loader.go | 4 + src/setup/updater.go | 21 ++++ src/terminal/runtimecommands.go | 177 ++++++++++++++++++++++++++++++++ src/terminal/terminalmsg.go | 58 +++++++++++ src/web/start.go | 8 +- 8 files changed, 272 insertions(+), 5 deletions(-) create mode 100644 src/terminal/runtimecommands.go create mode 100644 src/terminal/terminalmsg.go 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/config/config.go b/src/config/config.go index 148ebbbe..4b5cf5ff 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -70,6 +70,7 @@ type JsonConfig struct { AutoRestartServerTimer string `json:"AutoRestartServerTimer"` AllowPrereleaseUpdates *bool `json:"AllowPrereleaseUpdates"` AllowMajorUpdates *bool `json:"AllowMajorUpdates"` + IsConsoleEnabled *bool `json:"IsConsoleEnabled"` } type CustomDetection struct { @@ -220,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 { diff --git a/src/config/vars.go b/src/config/vars.go index afd5e918..fe60d792 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -64,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 diff --git a/src/loader/loader.go b/src/loader/loader.go index 70576b9d..349782f5 100644 --- a/src/loader/loader.go +++ b/src/loader/loader.go @@ -111,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/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 {