From 42dd9b6c484a8c0b98343c5f1215355aaab84041 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 3 Aug 2025 04:54:44 +0200 Subject: [PATCH 01/33] 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 02/33] 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 03/33] 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 04/33] 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 05/33] 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 06/33] 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 07/33] (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 08/33] 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 09/33] (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 10/33] 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 11/33] (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 12/33] 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 { From d4653ac2122ebd39b7430014f14876ba85ac6353 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 00:38:50 +0200 Subject: [PATCH 13/33] Implement onboard bundled assets for web UI, embedding resources directly. reduces github api requests significantly --- .../{ => onboard_bundled}/assets/apiinfo.html | 0 .../assets/css/apiinfo.css | 0 .../assets/css/background.css | 0 .../{ => onboard_bundled}/assets/css/base.css | 0 .../assets/css/components.css | 0 .../assets/css/config.css | 0 .../assets/css/detectionmanager.css | 0 .../{ => onboard_bundled}/assets/css/home.css | 0 .../assets/css/mobile.css | 0 .../{ => onboard_bundled}/assets/css/sscm.css | 0 .../assets/css/style.css | 0 .../{ => onboard_bundled}/assets/css/tabs.css | 0 .../assets/css/variables.css | 0 .../{ => onboard_bundled}/assets/favicon.ico | Bin .../assets/js/console-manager.js | 0 .../assets/js/detectionmanager.js | 0 UIMod/{ => onboard_bundled}/assets/js/main.js | 0 .../assets/js/server-api.js | 0 .../assets/js/ui-utils.js | 0 UIMod/{ => onboard_bundled}/assets/script.js | 0 .../assets/stationeers.png | Bin server.go | 5 ++ src/config/config.go | 2 +- src/config/helpers.go | 9 ++++ src/config/vars.go | 5 ++ src/loader/loader.go | 6 +++ src/setup/install.go | 50 +++++++++--------- src/web/start.go | 7 ++- 28 files changed, 56 insertions(+), 28 deletions(-) rename UIMod/{ => onboard_bundled}/assets/apiinfo.html (100%) rename UIMod/{ => onboard_bundled}/assets/css/apiinfo.css (100%) rename UIMod/{ => onboard_bundled}/assets/css/background.css (100%) rename UIMod/{ => onboard_bundled}/assets/css/base.css (100%) rename UIMod/{ => onboard_bundled}/assets/css/components.css (100%) rename UIMod/{ => onboard_bundled}/assets/css/config.css (100%) rename UIMod/{ => onboard_bundled}/assets/css/detectionmanager.css (100%) rename UIMod/{ => onboard_bundled}/assets/css/home.css (100%) rename UIMod/{ => onboard_bundled}/assets/css/mobile.css (100%) rename UIMod/{ => onboard_bundled}/assets/css/sscm.css (100%) rename UIMod/{ => onboard_bundled}/assets/css/style.css (100%) rename UIMod/{ => onboard_bundled}/assets/css/tabs.css (100%) rename UIMod/{ => onboard_bundled}/assets/css/variables.css (100%) rename UIMod/{ => onboard_bundled}/assets/favicon.ico (100%) rename UIMod/{ => onboard_bundled}/assets/js/console-manager.js (100%) rename UIMod/{ => onboard_bundled}/assets/js/detectionmanager.js (100%) rename UIMod/{ => onboard_bundled}/assets/js/main.js (100%) rename UIMod/{ => onboard_bundled}/assets/js/server-api.js (100%) rename UIMod/{ => onboard_bundled}/assets/js/ui-utils.js (100%) rename UIMod/{ => onboard_bundled}/assets/script.js (100%) rename UIMod/{ => onboard_bundled}/assets/stationeers.png (100%) diff --git a/UIMod/assets/apiinfo.html b/UIMod/onboard_bundled/assets/apiinfo.html similarity index 100% rename from UIMod/assets/apiinfo.html rename to UIMod/onboard_bundled/assets/apiinfo.html diff --git a/UIMod/assets/css/apiinfo.css b/UIMod/onboard_bundled/assets/css/apiinfo.css similarity index 100% rename from UIMod/assets/css/apiinfo.css rename to UIMod/onboard_bundled/assets/css/apiinfo.css diff --git a/UIMod/assets/css/background.css b/UIMod/onboard_bundled/assets/css/background.css similarity index 100% rename from UIMod/assets/css/background.css rename to UIMod/onboard_bundled/assets/css/background.css diff --git a/UIMod/assets/css/base.css b/UIMod/onboard_bundled/assets/css/base.css similarity index 100% rename from UIMod/assets/css/base.css rename to UIMod/onboard_bundled/assets/css/base.css diff --git a/UIMod/assets/css/components.css b/UIMod/onboard_bundled/assets/css/components.css similarity index 100% rename from UIMod/assets/css/components.css rename to UIMod/onboard_bundled/assets/css/components.css diff --git a/UIMod/assets/css/config.css b/UIMod/onboard_bundled/assets/css/config.css similarity index 100% rename from UIMod/assets/css/config.css rename to UIMod/onboard_bundled/assets/css/config.css diff --git a/UIMod/assets/css/detectionmanager.css b/UIMod/onboard_bundled/assets/css/detectionmanager.css similarity index 100% rename from UIMod/assets/css/detectionmanager.css rename to UIMod/onboard_bundled/assets/css/detectionmanager.css diff --git a/UIMod/assets/css/home.css b/UIMod/onboard_bundled/assets/css/home.css similarity index 100% rename from UIMod/assets/css/home.css rename to UIMod/onboard_bundled/assets/css/home.css diff --git a/UIMod/assets/css/mobile.css b/UIMod/onboard_bundled/assets/css/mobile.css similarity index 100% rename from UIMod/assets/css/mobile.css rename to UIMod/onboard_bundled/assets/css/mobile.css diff --git a/UIMod/assets/css/sscm.css b/UIMod/onboard_bundled/assets/css/sscm.css similarity index 100% rename from UIMod/assets/css/sscm.css rename to UIMod/onboard_bundled/assets/css/sscm.css diff --git a/UIMod/assets/css/style.css b/UIMod/onboard_bundled/assets/css/style.css similarity index 100% rename from UIMod/assets/css/style.css rename to UIMod/onboard_bundled/assets/css/style.css diff --git a/UIMod/assets/css/tabs.css b/UIMod/onboard_bundled/assets/css/tabs.css similarity index 100% rename from UIMod/assets/css/tabs.css rename to UIMod/onboard_bundled/assets/css/tabs.css diff --git a/UIMod/assets/css/variables.css b/UIMod/onboard_bundled/assets/css/variables.css similarity index 100% rename from UIMod/assets/css/variables.css rename to UIMod/onboard_bundled/assets/css/variables.css diff --git a/UIMod/assets/favicon.ico b/UIMod/onboard_bundled/assets/favicon.ico similarity index 100% rename from UIMod/assets/favicon.ico rename to UIMod/onboard_bundled/assets/favicon.ico diff --git a/UIMod/assets/js/console-manager.js b/UIMod/onboard_bundled/assets/js/console-manager.js similarity index 100% rename from UIMod/assets/js/console-manager.js rename to UIMod/onboard_bundled/assets/js/console-manager.js diff --git a/UIMod/assets/js/detectionmanager.js b/UIMod/onboard_bundled/assets/js/detectionmanager.js similarity index 100% rename from UIMod/assets/js/detectionmanager.js rename to UIMod/onboard_bundled/assets/js/detectionmanager.js diff --git a/UIMod/assets/js/main.js b/UIMod/onboard_bundled/assets/js/main.js similarity index 100% rename from UIMod/assets/js/main.js rename to UIMod/onboard_bundled/assets/js/main.js diff --git a/UIMod/assets/js/server-api.js b/UIMod/onboard_bundled/assets/js/server-api.js similarity index 100% rename from UIMod/assets/js/server-api.js rename to UIMod/onboard_bundled/assets/js/server-api.js diff --git a/UIMod/assets/js/ui-utils.js b/UIMod/onboard_bundled/assets/js/ui-utils.js similarity index 100% rename from UIMod/assets/js/ui-utils.js rename to UIMod/onboard_bundled/assets/js/ui-utils.js diff --git a/UIMod/assets/script.js b/UIMod/onboard_bundled/assets/script.js similarity index 100% rename from UIMod/assets/script.js rename to UIMod/onboard_bundled/assets/script.js diff --git a/UIMod/assets/stationeers.png b/UIMod/onboard_bundled/assets/stationeers.png similarity index 100% rename from UIMod/assets/stationeers.png rename to UIMod/onboard_bundled/assets/stationeers.png diff --git a/server.go b/server.go index b8129206..10c4350a 100644 --- a/server.go +++ b/server.go @@ -21,6 +21,7 @@ package main import ( + "embed" "sync" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/loader" @@ -30,6 +31,9 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/web" ) +//go:embed UIMod/onboard_bundled +var v1uiFS embed.FS + func main() { var wg sync.WaitGroup logger.Main.Install("Starting setup...") @@ -42,6 +46,7 @@ func main() { wg.Wait() // Load config,discordbot, backupmgr and detectionmgr using the loader package + loader.InitVirtFS(v1uiFS) loader.ReloadAll() loader.InitDetector() diff --git a/src/config/config.go b/src/config/config.go index 4b5cf5ff..85530db3 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -11,7 +11,7 @@ import ( var ( // All configuration variables can be found in vars.go - Version = "5.5.0" + Version = "5.5.1" Branch = "indev" ) diff --git a/src/config/helpers.go b/src/config/helpers.go index e8e0da91..5aaa4d68 100644 --- a/src/config/helpers.go +++ b/src/config/helpers.go @@ -4,6 +4,7 @@ package config import ( "crypto/rand" + "embed" "encoding/base64" "fmt" "os" @@ -109,3 +110,11 @@ func generateJwtKey() string { } return base64.RawURLEncoding.EncodeToString(key) } + +func SetV1UIFS(v1uiFS embed.FS) { + V1UIFS = v1uiFS +} + +func GetV1UIFS() embed.FS { + return V1UIFS +} diff --git a/src/config/vars.go b/src/config/vars.go index fe60d792..188f7a36 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -1,6 +1,7 @@ package config import ( + "embed" "sync" "time" @@ -137,3 +138,7 @@ var ( SSCMFilePath = "./BepInEx/plugins/SSCM/SSCM.socket" SSCMPluginDir = "./BepInEx/plugins/SSCM/" ) + +// Bundled Assets + +var V1UIFS embed.FS diff --git a/src/loader/loader.go b/src/loader/loader.go index 349782f5..49dee9e6 100644 --- a/src/loader/loader.go +++ b/src/loader/loader.go @@ -2,6 +2,7 @@ package loader import ( + "embed" "fmt" "strconv" @@ -115,3 +116,8 @@ func PrintConfigDetails() { func RestartBackend() { setup.RestartMySelf() } + +// InitBundler initialized the onboard bundled assets for the web UI +func InitVirtFS(v1uiFS embed.FS) { + config.SetV1UIFS(v1uiFS) +} diff --git a/src/setup/install.go b/src/setup/install.go index 59bcec06..0a610563 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -68,31 +68,31 @@ func CheckAndDownloadUIMod() { // Define file mappings files := map[string]string{ - uiDir + "config.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/ui/config.html", downloadBranch), - uiDir + "index.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/ui/index.html", downloadBranch), - uiDir + "detectionmanager.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/ui/detectionmanager.html", downloadBranch), - assetDir + "stationeers.png": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/stationeers.png", downloadBranch), - assetDir + "favicon.ico": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/favicon.ico", downloadBranch), - assetDir + "apiinfo.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/apiinfo.html", downloadBranch), - twoBoxFormDir + "twoboxform.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.css", downloadBranch), - twoBoxFormDir + "twoboxform.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.js", downloadBranch), - twoBoxFormDir + "twoboxform.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.html", downloadBranch), - cssAssetDIr + "apiinfo.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/apiinfo.css", downloadBranch), - cssAssetDIr + "background.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/background.css", downloadBranch), - cssAssetDIr + "base.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/base.css", downloadBranch), - cssAssetDIr + "components.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/components.css", downloadBranch), - cssAssetDIr + "config.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/config.css", downloadBranch), - cssAssetDIr + "detectionmanager.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/detectionmanager.css", downloadBranch), - cssAssetDIr + "home.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/home.css", downloadBranch), - cssAssetDIr + "mobile.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/mobile.css", downloadBranch), - cssAssetDIr + "style.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/style.css", downloadBranch), - cssAssetDIr + "tabs.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/tabs.css", downloadBranch), - cssAssetDIr + "variables.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/variables.css", downloadBranch), - jsAssetDir + "main.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/main.js", downloadBranch), - jsAssetDir + "detectionmanager.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/detectionmanager.js", downloadBranch), - jsAssetDir + "console-manager.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/console-manager.js", downloadBranch), - jsAssetDir + "server-api.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/server-api.js", downloadBranch), - jsAssetDir + "ui-utils.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/ui-utils.js", downloadBranch), + uiDir + "config.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/ui/config.html", downloadBranch), + uiDir + "index.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/ui/index.html", downloadBranch), + uiDir + "detectionmanager.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/ui/detectionmanager.html", downloadBranch), + //assetDir + "stationeers.png": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/stationeers.png", downloadBranch), + //assetDir + "favicon.ico": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/favicon.ico", downloadBranch), + //assetDir + "apiinfo.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/apiinfo.html", downloadBranch), + twoBoxFormDir + "twoboxform.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.css", downloadBranch), + twoBoxFormDir + "twoboxform.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.js", downloadBranch), + twoBoxFormDir + "twoboxform.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.html", downloadBranch), + //cssAssetDIr + "apiinfo.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/apiinfo.css", downloadBranch), + //cssAssetDIr + "background.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/background.css", downloadBranch), + //cssAssetDIr + "base.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/base.css", downloadBranch), + //cssAssetDIr + "components.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/components.css", downloadBranch), + //cssAssetDIr + "config.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/config.css", downloadBranch), + //cssAssetDIr + "detectionmanager.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/detectionmanager.css", downloadBranch), + //cssAssetDIr + "home.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/home.css", downloadBranch), + //cssAssetDIr + "mobile.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/mobile.css", downloadBranch), + //cssAssetDIr + "style.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/style.css", downloadBranch), + //cssAssetDIr + "tabs.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/tabs.css", downloadBranch), + //cssAssetDIr + "variables.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/variables.css", downloadBranch), + //jsAssetDir + "main.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/main.js", downloadBranch), + //jsAssetDir + "detectionmanager.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/detectionmanager.js", downloadBranch), + //jsAssetDir + "console-manager.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/console-manager.js", downloadBranch), + //jsAssetDir + "server-api.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/server-api.js", downloadBranch), + //jsAssetDir + "ui-utils.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/ui-utils.js", downloadBranch), } createRequiredDirs(requiredDirs) diff --git a/src/web/start.go b/src/web/start.go index 90659d7a..b178e5b8 100644 --- a/src/web/start.go +++ b/src/web/start.go @@ -2,6 +2,7 @@ package web import ( + "io/fs" "net/http" "net/http/pprof" "sync" @@ -31,8 +32,10 @@ func StartWebServer(wg *sync.WaitGroup) { // Protected routes (wrapped with middleware) protectedMux := http.NewServeMux() - fs := http.FileServer(http.Dir(config.UIModFolder + "/assets")) - protectedMux.Handle("/static/", http.StripPrefix("/static/", fs)) + + legacyAssetsFS, _ := fs.Sub(config.GetV1UIFS(), "UIMod/onboard_bundled/assets") + protectedMux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(legacyAssetsFS)))) + protectedMux.HandleFunc("/config", ServeConfigPage) protectedMux.HandleFunc("/detectionmanager", ServeDetectionManager) protectedMux.HandleFunc("/", ServeIndex) From 18045e67da32917cb96b13f9b1fcc9ea91502ddc Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 00:51:39 +0200 Subject: [PATCH 14/33] added twoboxform to bundled files, further reducing github api calls --- .../twoboxform/twoboxform.css | 0 .../twoboxform/twoboxform.html | 0 .../twoboxform/twoboxform.js | 0 src/setup/install.go | 16 ++++++++-------- src/web/TwoBoxForm.go | 12 ++++++++++-- src/web/start.go | 4 ++-- 6 files changed, 20 insertions(+), 12 deletions(-) rename UIMod/{ => onboard_bundled}/twoboxform/twoboxform.css (100%) rename UIMod/{ => onboard_bundled}/twoboxform/twoboxform.html (100%) rename UIMod/{ => onboard_bundled}/twoboxform/twoboxform.js (100%) diff --git a/UIMod/twoboxform/twoboxform.css b/UIMod/onboard_bundled/twoboxform/twoboxform.css similarity index 100% rename from UIMod/twoboxform/twoboxform.css rename to UIMod/onboard_bundled/twoboxform/twoboxform.css diff --git a/UIMod/twoboxform/twoboxform.html b/UIMod/onboard_bundled/twoboxform/twoboxform.html similarity index 100% rename from UIMod/twoboxform/twoboxform.html rename to UIMod/onboard_bundled/twoboxform/twoboxform.html diff --git a/UIMod/twoboxform/twoboxform.js b/UIMod/onboard_bundled/twoboxform/twoboxform.js similarity index 100% rename from UIMod/twoboxform/twoboxform.js rename to UIMod/onboard_bundled/twoboxform/twoboxform.js diff --git a/src/setup/install.go b/src/setup/install.go index 0a610563..7df7e9db 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -47,16 +47,16 @@ func Install(wg *sync.WaitGroup) { func CheckAndDownloadUIMod() { uiModDir := config.UIModFolder - twoBoxFormDir := config.UIModFolder + "twoboxform/" + //twoBoxFormDir := config.UIModFolder + "twoboxform/" detectionmanagerDir := config.UIModFolder + "detectionmanager/" - assetDir := config.UIModFolder + "assets/" - cssAssetDIr := config.UIModFolder + "assets/css/" + //assetDir := config.UIModFolder + "assets/" + //cssAssetDIr := config.UIModFolder + "assets/css/" uiDir := config.UIModFolder + "ui/" configDir := config.UIModFolder + "config/" tlsDir := config.UIModFolder + "tls/" - jsAssetDir := config.UIModFolder + "assets/js/" + //jsAssetDir := config.UIModFolder + "assets/js/" - requiredDirs := []string{uiModDir, uiDir, assetDir, cssAssetDIr, twoBoxFormDir, detectionmanagerDir, configDir, jsAssetDir} + requiredDirs := []string{uiModDir, uiDir, detectionmanagerDir, configDir} // Set branch if config.Branch == "release" || config.Branch == "Release" { @@ -71,12 +71,12 @@ func CheckAndDownloadUIMod() { uiDir + "config.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/ui/config.html", downloadBranch), uiDir + "index.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/ui/index.html", downloadBranch), uiDir + "detectionmanager.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/ui/detectionmanager.html", downloadBranch), + //twoBoxFormDir + "twoboxform.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.css", downloadBranch), + //twoBoxFormDir + "twoboxform.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.js", downloadBranch), + //twoBoxFormDir + "twoboxform.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.html", downloadBranch), //assetDir + "stationeers.png": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/stationeers.png", downloadBranch), //assetDir + "favicon.ico": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/favicon.ico", downloadBranch), //assetDir + "apiinfo.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/apiinfo.html", downloadBranch), - twoBoxFormDir + "twoboxform.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.css", downloadBranch), - twoBoxFormDir + "twoboxform.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.js", downloadBranch), - twoBoxFormDir + "twoboxform.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.html", downloadBranch), //cssAssetDIr + "apiinfo.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/apiinfo.css", downloadBranch), //cssAssetDIr + "background.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/background.css", downloadBranch), //cssAssetDIr + "base.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/base.css", downloadBranch), diff --git a/src/web/TwoBoxForm.go b/src/web/TwoBoxForm.go index dca08ece..667343a8 100644 --- a/src/web/TwoBoxForm.go +++ b/src/web/TwoBoxForm.go @@ -1,6 +1,7 @@ package web import ( + "io/fs" "net/http" "text/template" @@ -46,9 +47,16 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { SecondaryPlaceholderText string } - tmpl, err := template.ParseFiles(config.TwoBoxFormHtmlPath) + twoboxformAssetsFS, err := fs.Sub(config.GetV1UIFS(), "UIMod/onboard_bundled/twoboxform") if err != nil { - logger.Web.Error("Failed to parse 2BoxForm template: %v" + err.Error()) + logger.Web.Error("Failed to get bundled FS") + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + tmpl, err := template.ParseFS(twoboxformAssetsFS, "twoboxform.html") + if err != nil { + logger.Web.Error("Failed to parse 2BoxForm template") http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } diff --git a/src/web/start.go b/src/web/start.go index b178e5b8..1af3e109 100644 --- a/src/web/start.go +++ b/src/web/start.go @@ -23,8 +23,8 @@ func StartWebServer(wg *sync.WaitGroup) { mux := http.NewServeMux() // Use a mux to apply middleware globally // Unprotected auth routes - mux.HandleFunc("/twoboxform/twoboxform.js", ServeTwoBoxJs) - mux.HandleFunc("/twoboxform/twoboxform.css", ServeTwoBoxCss) + twoboxformAssetsFS, _ := fs.Sub(config.GetV1UIFS(), "UIMod/onboard_bundled/twoboxform") + mux.Handle("/twoboxform/", http.StripPrefix("/twoboxform/", http.FileServer(http.FS(twoboxformAssetsFS)))) mux.HandleFunc("/sscm/sscm.js", ServeSSCMJs) mux.HandleFunc("/auth/login", LoginHandler) // Token issuer mux.HandleFunc("/auth/logout", LogoutHandler) From ef9c2de577b4d1f431dd7dc9e350ed02a2627e57 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 01:14:13 +0200 Subject: [PATCH 15/33] moved detectionmanager json to config folder --- src/config/vars.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/vars.go b/src/config/vars.go index 188f7a36..3e20ebb2 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -126,7 +126,7 @@ var ( TLSCertPath = "./UIMod/tls/cert.pem" TLSKeyPath = "./UIMod/tls/key.pem" ConfigPath = "./UIMod/config/config.json" - CustomDetectionsFilePath = "./UIMod/detectionmanager/customdetections.json" + CustomDetectionsFilePath = "./UIMod/config/customdetections.json" LogFolder = "./UIMod/logs/" UIModFolder = "./UIMod/" TwoBoxFormFolder = "./UIMod/twoboxform/" From c87aa1ad8d961457b6861d69fe6c9174dc794a3e Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 01:15:02 +0200 Subject: [PATCH 16/33] removed all download request calls to github, now all assets are bundled into the executable. Left functionality intact for later use, if needed --- UIMod/config/customdetections.json | 1 + UIMod/detectionmanager/detectionmanager.js | 211 -------------- .../detectionmanager/detectionmanager.html | 0 UIMod/{ => onboard_bundled}/ui/config.html | 0 UIMod/{ => onboard_bundled}/ui/index.html | 0 UIMod/sscm/sscm.js | 261 ------------------ UIMod/ui/detectionmanager.html | 119 -------- src/setup/install.go | 44 +-- src/web/http.go | 44 ++- 9 files changed, 50 insertions(+), 630 deletions(-) create mode 100644 UIMod/config/customdetections.json delete mode 100644 UIMod/detectionmanager/detectionmanager.js rename UIMod/{ => onboard_bundled}/detectionmanager/detectionmanager.html (100%) rename UIMod/{ => onboard_bundled}/ui/config.html (100%) rename UIMod/{ => onboard_bundled}/ui/index.html (100%) delete mode 100644 UIMod/sscm/sscm.js delete mode 100644 UIMod/ui/detectionmanager.html diff --git a/UIMod/config/customdetections.json b/UIMod/config/customdetections.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/UIMod/config/customdetections.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/UIMod/detectionmanager/detectionmanager.js b/UIMod/detectionmanager/detectionmanager.js deleted file mode 100644 index 5f361742..00000000 --- a/UIMod/detectionmanager/detectionmanager.js +++ /dev/null @@ -1,211 +0,0 @@ -// Show active tab -function showTab(tabId) { - document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active')); - document.querySelectorAll('.tab-button').forEach(button => button.classList.remove('active')); - - document.getElementById(tabId).classList.add('active'); - document.querySelector(`.tab-button[data-tab="${tabId}"]`).classList.add('active'); - - if (tabId === 'detection-list-tab') { - loadDetections(); - } -} - -// Toggle detection type -function setupDetectionTypeToggle() { - const toggle = document.getElementById('detection-type-toggle'); - const typeLabel = document.getElementById('detection-type-label'); - const typeInput = document.getElementById('type'); - const patternInfo = document.getElementById('pattern-info'); - const messageInfo = document.getElementById('message-info'); - - toggle.addEventListener('change', function() { - if (this.checked) { - typeLabel.textContent = 'Regex'; - typeInput.value = 'regex'; - patternInfo.textContent = 'Regular expression pattern (e.g., "Player (.+) has reached level (\\d+)")'; - messageInfo.textContent = 'Message to display when pattern is detected. Use {1}, {2}, etc. for captured groups'; - } else { - typeLabel.textContent = 'Keyword'; - typeInput.value = 'keyword'; - patternInfo.textContent = 'Text to match exactly (case-sensitive)'; - messageInfo.textContent = 'Message to display when pattern is detected'; - } - }); -} - -// Load detections -function loadDetections() { - const loader = document.getElementById('list-loader'); - const detectionItems = document.getElementById('detection-items'); - - loader.style.display = 'block'; - - fetch('/api/v2/custom-detections') - .then(response => { - if (!response.ok) throw new Error('Failed to load detections'); - return response.json(); - }) - .then(detections => { - loader.style.display = 'none'; - - if (detections.length === 0) { - detectionItems.innerHTML = '
No custom detections found. Add one to get started.
'; - return; - } - - detectionItems.innerHTML = ''; - detections.forEach(detection => { - const item = document.createElement('div'); - item.className = 'detection-item'; - item.innerHTML = ` -
${detection.type}
-
${escapeHtml(detection.pattern)}
-
${escapeHtml(detection.message)}
-
- -
- `; - detectionItems.appendChild(item); - }); - }) - .catch(error => { - loader.style.display = 'none'; - showNotification('Error: ' + error.message, 'error'); - console.error('Error loading detections:', error); - }); -} - -// Submit detection -function submitDetection() { - const form = document.getElementById('detection-form'); - const type = document.getElementById('type').value; - const pattern = document.getElementById('pattern').value.trim(); - const message = document.getElementById('message').value.trim(); - - if (!pattern || !message) { - showNotification('Please fill in all fields', 'error'); - return; - } - - const data = { - type: type, - pattern: pattern, - eventType: 'CUSTOM_DETECTION', - message: message - }; - - fetch('/api/v2/custom-detections', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }) - .then(response => { - if (!response.ok) { - return response.text().then(text => { throw new Error(text || 'Failed to add detection'); }); - } - return response.json(); - }) - .then(() => { - form.reset(); - document.getElementById('detection-type-toggle').checked = false; - document.getElementById('detection-type-label').textContent = 'Keyword'; - document.getElementById('type').value = 'keyword'; - showNotification('Detection added successfully', 'success'); - showTab('detection-list-tab'); - }) - .catch(error => { - showNotification('Error: ' + error.message, 'error'); - console.error('Error adding detection:', error); - }); -} - -// Delete detection -function deleteDetection(id) { - - fetch(`/api/v2/custom-detections/delete/?id=${id}`, { method: 'DELETE' }) - .then(response => { - if (!response.ok) { - return response.text().then(text => { throw new Error(text || 'Failed to delete detection'); }); - } - showNotification('Detection deleted successfully', 'success'); - loadDetections(); - }) - .catch(error => { - showNotification('Error: ' + error.message, 'error'); - console.error('Error deleting detection:', error); - }); -} - -// Show notification -function showNotification(message, type) { - const notification = document.getElementById('notification'); - notification.textContent = message; - notification.className = `notification notification-${type}`; - notification.style.display = 'block'; - - setTimeout(() => { - notification.style.display = 'none'; - }, 5000); -} - -// Helper function to escape HTML -function escapeHtml(unsafe) { - return unsafe - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} - -// Event listeners -document.addEventListener('DOMContentLoaded', () => { - loadDetections(); - setupDetectionTypeToggle(); - document.querySelectorAll('.tab-button').forEach(button => { - button.addEventListener('click', () => showTab(button.getAttribute('data-tab'))); - }); - document.querySelector('.add-button').addEventListener('click', submitDetection); -}); - - -function resourceSaver(pause) { - // Get space background once outside the loop - const spaceBackground = document.getElementById('space-background'); - - // Handle animation states for all elements - document.querySelectorAll('*').forEach(element => { - element.style.animationPlayState = pause ? 'paused' : 'running'; - }); - - // Fade the space background in/out instead of abrupt display change - if (pause) { - // Fade out - spaceBackground.style.transition = 'opacity 0.5s ease'; - spaceBackground.style.opacity = '0'; - // Only hide it after the fade completes - setTimeout(() => { - if (document.hasFocus() === false) { // Double-check we're still unfocused - spaceBackground.style.display = 'none'; - } - }, 500); - } else { - // Make it visible first, then fade in - spaceBackground.style.display = 'block'; - // Use setTimeout to ensure the display change is processed before starting the fade - setTimeout(() => { - spaceBackground.style.transition = 'opacity 0.5s ease'; - spaceBackground.style.opacity = '1'; - }, 10); - } -} - -// Event listeners for window focus and blur -window.addEventListener('focus', () => { - resourceSaver(false); // Resume animations when page is in focus -}); - -window.addEventListener('blur', () => { - resourceSaver(true); // Pause animations when page loses focus -}); \ No newline at end of file diff --git a/UIMod/detectionmanager/detectionmanager.html b/UIMod/onboard_bundled/detectionmanager/detectionmanager.html similarity index 100% rename from UIMod/detectionmanager/detectionmanager.html rename to UIMod/onboard_bundled/detectionmanager/detectionmanager.html diff --git a/UIMod/ui/config.html b/UIMod/onboard_bundled/ui/config.html similarity index 100% rename from UIMod/ui/config.html rename to UIMod/onboard_bundled/ui/config.html diff --git a/UIMod/ui/index.html b/UIMod/onboard_bundled/ui/index.html similarity index 100% rename from UIMod/ui/index.html rename to UIMod/onboard_bundled/ui/index.html diff --git a/UIMod/sscm/sscm.js b/UIMod/sscm/sscm.js deleted file mode 100644 index 8850f8ba..00000000 --- a/UIMod/sscm/sscm.js +++ /dev/null @@ -1,261 +0,0 @@ -// sscm.js - -// Full command list with descriptions and parameters -const availableCommands = [ - { name: "addgas", params: "[Oxygen,Nitrogen,CarbonDioxide,Volatiles,Pollutant,Water,NitrousOxide]", desc: "Adds GasType to target thing" }, - { name: "atmos", params: "[pipe,world,direction,room,global,thing,cleanup,count,liquid]", desc: "Enables atmosphere debugging" }, - { name: "ban", params: "[,refresh]", desc: "Bans a client from the server" }, - { name: "camera", params: "[shake]", desc: "Various camera debug functions" }, - { name: "celestial", params: "[eccentricity,semimajoraxisau,semimajoraxiskm,inclination,periapsis,period,ascendingnode,rotation]", desc: "Allows editing of celestial bodies" }, - { name: "cleanupplayers", params: "[dead,disconnected,all]", desc: "Cleans up player bodies" }, - { name: "clear", params: "", desc: "Clears all console text" }, - { name: "debugthreads", params: "[GameTick,Terrain]", desc: "Show worker thread run times" }, - { name: "deletelooseitems", params: "", desc: "Removes all loose items in world" }, - { name: "deleteoutofbounds", params: "", desc: "Removes out-of-bounds objects" }, - { name: "difficulty", params: "[]", desc: "Prints or sets difficulty" }, - { name: "discord", params: "", desc: "Interaction with Discord SDK" }, - { name: "dlc", params: "[shared]", desc: "Various DLC debug functions" }, - { name: "emote", params: "[emoteName]", desc: "Triggers player emote" }, - { name: "entity", params: "[state ]", desc: "Entity debug functions" }, - { name: "exportworld", params: "", desc: "Exports world to WorldSettings file" }, - { name: "help", params: "[commands,list,,tofile]", desc: "Displays command help" }, - { name: "helperhints", params: "[Dismiss,Complete,Trigger]", desc: "Tests world objectives" }, - { name: "keybindings", params: "[reset]", desc: "Displays or resets keybindings" }, - { name: "kick", params: "[]", desc: "Kicks a client from server" }, - { name: "legacycpu", params: "[enable,disable]", desc: "Enables Legacy CPU mode" }, - { name: "liquid", params: "[show,renderer,solver,WorldVolume]", desc: "Debugs liquid solver" }, - { name: "listnetworkdevices", params: "[id]", desc: "Lists network devices" }, - { name: "localization", params: "[None,WordCount,Generate,Refresh,CheckKeys,CheckFonts]", desc: "Displays localization info" }, - { name: "log", params: "[,clear]", desc: "Dumps logs to file" }, - { name: "logtoclipboard", params: "", desc: "Copies console to clipboard" }, - { name: "masterserver", params: "[refresh]", desc: "Interacts with Master Server" }, - { name: "minables", params: "[range,generate]", desc: "Toggles minable debug" }, - { name: "netconfig", params: "[list,print, ]", desc: "Changes NetConfig.xml" }, - { name: "network", params: "", desc: "Shows network status" }, - { name: "networkdebug", params: "", desc: "Displays network debug window" }, - { name: "orbit", params: "[debug,view,celestials,simulate,set,timescale,makeoffset]", desc: "Controls orbital simulation" }, - { name: "pause", params: "[true,false]", desc: "Pauses/unpauses game" }, - { name: "plant", params: "[grow ]", desc: "Plant debug functions" }, - { name: "prefabs", params: "[Thumbnails]", desc: "Validates source prefabs" }, - { name: "printgasinfo", params: "", desc: "Prints gas coefficients" }, - { name: "profiler", params: "[enable,disable]", desc: "Toggles profiler" }, - { name: "regeneraterooms", params: "", desc: "Regenerates world rooms" }, - { name: "rocket", params: "[refresh,print,abandon,debug,chart]", desc: "Rocket debug functions" }, - { name: "save", params: "[,delete ,list]", desc: "Saves game" }, - { name: "say", params: "", desc: "Sends message to players" }, - { name: "setbatteries", params: "[Empty,Critical,Very Low,Low,Medium,High,Full]", desc: "Sets battery levels" }, - { name: "settings", params: "[list,print, ]", desc: "Changes settings.xml" }, - { name: "settingspath", params: "[]", desc: "Sets settings path" }, - { name: "spacemap", params: "[regenerate,fill,chart,testpaths]", desc: "Space map debug functions" }, - { name: "spacemapnode", params: "[]", desc: "Space map node debug" }, - { name: "status", params: "", desc: "Shows server state" }, - { name: "steam", params: "[Refresh,Store,Achieve,Clear,ClearAll,Invalid]", desc: "Tests Steamworks" }, - { name: "storm", params: "[start,stop,debug]", desc: "Controls weather events" }, - { name: "structure", params: "[completeall]", desc: "Structure debug functions" }, - { name: "structurenetwork", params: "[chute,rocket]", desc: "Debugs structure networks" }, - { name: "systeminfo", params: "", desc: "Prints system info" }, - { name: "test", params: "", desc: "Tests colors" }, - { name: "testbytearray", params: "", desc: "Tests network read/write" }, - { name: "testoctree", params: "[[number of iterations]]", desc: "Benchmarks read density" }, - { name: "thing", params: "[find ,delete ,spawn [amount],info ,...]", desc: "Manages things" }, - { name: "trader", params: "[regenerate,land,depart,contacts,buys,sells,evaluate,checksum]", desc: "Trader debug commands" }, - { name: "unstuck", params: "", desc: "Attempts to unstick player" }, - { name: "upnp", params: "", desc: "Shows UPnP state" }, - { name: "vegetation", params: "[set,debug]", desc: "Sets vegetation quantity" }, - { name: "version", params: "", desc: "Shows game version" }, - { name: "world", params: "", desc: "Prints world settings" }, - { name: "worldsetting", params: "", desc: "Authors WorldSetting info" } -]; - -// Check if SSCM is enabled (unchanged) -async function checkSSCMEnabled() { - try { - const response = await fetch('/api/v2/SSCM/enabled', { - method: 'GET', - headers: { 'Content-Type': 'application/json' } - }); - const input = document.getElementById('sscm-command-input'); - if (response.status === 200) { - input.disabled = false; - input.placeholder = "Enter command..."; - } else { - input.onclick = () => { - window.location.href = "/setup?step=sscm_opt_in"; - }; - input.placeholder = "SSCM is not enabled, commands unavailable. Click here to configure."; - } - } catch (error) { - console.error('Error checking SSCM status:', error); - const input = document.getElementById('sscm-command-input'); - input.disabled = true; - input.placeholder = "Commands unavailable"; - } -} - -// Send command to SSCM run endpoint (unchanged) -async function sendSSCMCommand(command) { - try { - // Check server status before sending command - const statusResponse = await fetch('/api/v2/server/status'); - const statusData = await statusResponse.json(); - - if (!statusData.isRunning) { - appendToConsole('[SSCM] Error: Gameserver is not running, start the server and try again.'); - return; - } - - const response = await fetch('/api/v2/SSCM/run', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ command }) - }); - const result = await response.json(); - appendToConsole(result.status === 'success' - ? `[SSCM] ${result.message}: ${command}` - : `[SSCM] Error: ${result.message || 'Command failed'}`); - } catch (error) { - console.error('Error sending SSCM command:', error); - appendToConsole(`[SSCM] Error: Failed to send command "${command}"`); - } -} - -// Append message to console -function appendToConsole(message) { - const consoleDiv = document.getElementById('console'); - const commandContainer = consoleDiv.querySelector('.sscm-command-container'); - const messageElement = document.createElement('p'); - messageElement.textContent = message; - - if (commandContainer) { - consoleDiv.insertBefore(messageElement, commandContainer); // Insert before input - } else { - console.log("SSCM failed to insert command box"); - return - } - - // Auto-scroll only if at bottom - if (consoleDiv.scrollTop + consoleDiv.clientHeight >= consoleDiv.scrollHeight - 10) { - consoleDiv.scrollTop = consoleDiv.scrollHeight; - } -} - -// Enhanced autocomplete functionality -function setupAutocomplete() { - const input = document.getElementById('sscm-command-input'); - const suggestionsDiv = document.getElementById('sscm-autocomplete-suggestions'); - let selectedIndex = -1; - - function updateSuggestions(query) { - suggestionsDiv.innerHTML = ''; - selectedIndex = -1; - if (query.length === 0) return; - - const matches = availableCommands - .filter(cmd => cmd.name.toLowerCase().startsWith(query.toLowerCase())) - .sort((a, b) => a.name.length - b.name.length) // Prioritize shorter matches - .slice(0, 5); // Limit to 5 suggestions - - if (matches.length === 0) return; - - matches.forEach((cmd, index) => { - const suggestion = document.createElement('div'); - suggestion.classList.add('sscm-suggestion-item'); - suggestion.innerHTML = ` - ${cmd.name} - ${cmd.params || 'No params'} - ${cmd.desc} - `; - suggestion.addEventListener('click', () => { - input.value = cmd.name; - suggestionsDiv.innerHTML = ''; - input.focus(); - }); - suggestion.addEventListener('mouseover', () => { - selectedIndex = index; - updateHighlight(); - }); - suggestionsDiv.appendChild(suggestion); - }); - } - - function updateHighlight() { - const items = suggestionsDiv.querySelectorAll('.sscm-suggestion-item'); - items.forEach((item, index) => { - item.classList.toggle('highlighted', index === selectedIndex); - }); - } - - function selectBestMatch() { - const query = input.value.toLowerCase(); - const matches = availableCommands.filter(cmd => - cmd.name.toLowerCase().startsWith(query) - ).sort((a, b) => a.name.length - b.name.length); // Shortest match first - return matches[0]?.name || input.value; - } - - input.addEventListener('input', () => { - updateSuggestions(input.value); - }); - - input.addEventListener('keydown', async (e) => { - const items = suggestionsDiv.querySelectorAll('.sscm-suggestion-item'); - if (e.key === 'ArrowDown') { - e.preventDefault(); - selectedIndex = Math.min(selectedIndex + 1, items.length - 1); - updateHighlight(); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - selectedIndex = Math.max(selectedIndex - 1, -1); - updateHighlight(); - } else if (e.key === 'Tab' || e.key === 'Enter') { - if (items.length > 0) { - e.preventDefault(); - input.value = selectedIndex >= 0 - ? suggestionsDiv.children[selectedIndex].querySelector('.sscm-suggestion-name').textContent - : selectBestMatch(); - suggestionsDiv.innerHTML = ''; - if (e.key === 'Enter' && !input.disabled && input.value.trim()) { - await sendSSCMCommand(input.value.trim()); - input.value = ''; - } - } else if (e.key === 'Enter' && !input.disabled && input.value.trim()) { - await sendSSCMCommand(input.value.trim()); - input.value = ''; - } - } else if (e.key === 'Escape') { - suggestionsDiv.innerHTML = ''; - selectedIndex = -1; - } - }); - - // Clear suggestions when clicking outside - document.addEventListener('click', (e) => { - if (!input.contains(e.target) && !suggestionsDiv.contains(e.target)) { - suggestionsDiv.innerHTML = ''; - selectedIndex = -1; - } - }); -} - -// Wait for SSCM input to be created before initializing -function waitForSSCMInput(callback) { - const checkInput = () => { - const input = document.getElementById('sscm-command-input'); - const suggestionsDiv = document.getElementById('sscm-autocomplete-suggestions'); - if (input && suggestionsDiv) { - callback(); - } else { - setTimeout(checkInput, 100); // Poll every 100ms - } - }; - checkInput(); -} - -// Initialize SSCM functionality -document.addEventListener('DOMContentLoaded', () => { - waitForSSCMInput(() => { - checkSSCMEnabled(); - setupAutocomplete(); - setInterval(checkSSCMEnabled, 30000); - }); -}); \ No newline at end of file diff --git a/UIMod/ui/detectionmanager.html b/UIMod/ui/detectionmanager.html deleted file mode 100644 index f848d514..00000000 --- a/UIMod/ui/detectionmanager.html +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - Custom Detection Manager - - - - - - - - - - -
-
- -
-
-

Custom Detection Manager

-
-
- - - -
-
- -
-
-
-
-
Type
-
Pattern
-
Message
-
Actions
-
-
-
No custom detections found. Add one to get started.
-
-
-
- -
- -
-
- Change Detection Mode - - Keyword -
- -
- - -
Text to match exactly (case-sensitive)
-
- -
- - -
Message to display when pattern is detected
-
- - - -
-
- -
- -
- -
-

Custom Detection Patterns

-

Custom detections allow you to create custom patterns for detection. These patterns can be used to detect specific events in the server logs.

-

To create a custom detection, you can use the "Add Detection Tab" to define a regex pattern or alternatively a simple string match ("keyword") and a message that will be logged in the Events and if enabled in Discord when the pattern is detected. It is not possible to create patterns that have faulty regex.

-
-

Creating Effective Detections

-
-
-

Keyword Detection:

-

For example, to detect the "Unsupported shader" message that unity logs when a shader is not supported, you would use the following pattern:

- Pattern: "Unsupported shader" - Message: "Unity detected an unsupported shader. This may cause unexpected behavior." -
-
-

Regex Detection:

-

For example, to detect (fictional) the "Player (.+) has reached level (\d+)" message that is logged when a player reaches a certain level in an elevator, you would use the following pattern:

- Pattern: "Player (.+) has reached level (\d+)" - Message: "Player {1} has reached level {2}" -

The AI of your choise will be more than happy to help you create effective detections. You can also use the Regex101 tool to test your patterns.

-
-
-

For more information, visit the GitHub Wiki

-
-
- -
- -
- -
-
- - - - - - - \ No newline at end of file diff --git a/src/setup/install.go b/src/setup/install.go index 7df7e9db..f64113d9 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -41,22 +41,15 @@ func Install(wg *sync.WaitGroup) { // Step 3: Install and run SteamCMD logger.Install.Info("🔄Installing and running SteamCMD...") InstallAndRunSteamCMD() - logger.Install.Warn("🙏Thank you for using StationeersServerUI!") logger.Install.Info("✅Setup complete!") } func CheckAndDownloadUIMod() { uiModDir := config.UIModFolder - //twoBoxFormDir := config.UIModFolder + "twoboxform/" - detectionmanagerDir := config.UIModFolder + "detectionmanager/" - //assetDir := config.UIModFolder + "assets/" - //cssAssetDIr := config.UIModFolder + "assets/css/" - uiDir := config.UIModFolder + "ui/" configDir := config.UIModFolder + "config/" tlsDir := config.UIModFolder + "tls/" - //jsAssetDir := config.UIModFolder + "assets/js/" - requiredDirs := []string{uiModDir, uiDir, detectionmanagerDir, configDir} + requiredDirs := []string{uiModDir, configDir} // Set branch if config.Branch == "release" || config.Branch == "Release" { @@ -68,35 +61,19 @@ func CheckAndDownloadUIMod() { // Define file mappings files := map[string]string{ - uiDir + "config.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/ui/config.html", downloadBranch), - uiDir + "index.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/ui/index.html", downloadBranch), - uiDir + "detectionmanager.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/ui/detectionmanager.html", downloadBranch), - //twoBoxFormDir + "twoboxform.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.css", downloadBranch), - //twoBoxFormDir + "twoboxform.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.js", downloadBranch), - //twoBoxFormDir + "twoboxform.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.html", downloadBranch), - //assetDir + "stationeers.png": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/stationeers.png", downloadBranch), - //assetDir + "favicon.ico": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/favicon.ico", downloadBranch), - //assetDir + "apiinfo.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/apiinfo.html", downloadBranch), - //cssAssetDIr + "apiinfo.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/apiinfo.css", downloadBranch), - //cssAssetDIr + "background.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/background.css", downloadBranch), - //cssAssetDIr + "base.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/base.css", downloadBranch), - //cssAssetDIr + "components.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/components.css", downloadBranch), - //cssAssetDIr + "config.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/config.css", downloadBranch), - //cssAssetDIr + "detectionmanager.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/detectionmanager.css", downloadBranch), - //cssAssetDIr + "home.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/home.css", downloadBranch), - //cssAssetDIr + "mobile.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/mobile.css", downloadBranch), - //cssAssetDIr + "style.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/style.css", downloadBranch), - //cssAssetDIr + "tabs.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/tabs.css", downloadBranch), - //cssAssetDIr + "variables.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/variables.css", downloadBranch), - //jsAssetDir + "main.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/main.js", downloadBranch), - //jsAssetDir + "detectionmanager.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/detectionmanager.js", downloadBranch), - //jsAssetDir + "console-manager.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/console-manager.js", downloadBranch), - //jsAssetDir + "server-api.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/server-api.js", downloadBranch), - //jsAssetDir + "ui-utils.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/ui-utils.js", downloadBranch), + // NOTE: Now empty as files are now embedded in the executable. Kept this structure for future use. + + // UI - commented out since files are embedded, left here for reference in case we need this funcitonality again + // "ui/config.html": "https://raw.githubusercontent.com/SteamServerUI/SteamServerUI/{branch}/UIMod/ui/config.html", } createRequiredDirs(requiredDirs) + if len(files) == 0 { + logger.Install.Debug("📁 File mappings empty - no additional files to download available") + return + } + // Check if the directory exists if _, err := os.Stat(uiModDir); os.IsNotExist(err) { // Initial download @@ -382,6 +359,7 @@ func createRequiredDirs(requiredDirs []string) { // Create directories for _, dir := range requiredDirs { if _, err := os.Stat(dir); os.IsNotExist(err) { + config.IsFirstTimeSetup = true err := os.MkdirAll(dir, os.ModePerm) if err != nil { logger.Install.Error("❌Error creating folder: " + err.Error()) diff --git a/src/web/http.go b/src/web/http.go index 4adc70a3..3ffedd23 100644 --- a/src/web/http.go +++ b/src/web/http.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "io/fs" "net/http" "os" "strings" @@ -23,9 +24,16 @@ type TemplateData struct { } func ServeIndex(w http.ResponseWriter, r *http.Request) { - tmpl, err := template.ParseFiles(config.IndexHtmlPath) + htmlFS, err := fs.Sub(config.V1UIFS, "UIMod/onboard_bundled/ui") + if err != nil { + http.Error(w, "Error accessing Virt FS: "+err.Error(), http.StatusInternalServerError) + return + } + + tmpl, err := template.ParseFS(htmlFS, "index.html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Core.Error("failed to serve v1 Index.html") return } @@ -48,27 +56,51 @@ func ServeIndex(w http.ResponseWriter, r *http.Request) { } func ServeDetectionManager(w http.ResponseWriter, r *http.Request) { + detectionmanagerFS, err := fs.Sub(config.V1UIFS, "UIMod/onboard_bundled/detectionmanager") + if err != nil { + http.Error(w, "Error accessing Virt FS: "+err.Error(), http.StatusInternalServerError) + return + } - htmlFile, err := os.ReadFile(config.DetectionManagerHtmlPath) + htmlFile, err := detectionmanagerFS.Open("detectionmanager.html") if err != nil { http.Error(w, fmt.Sprintf("Error reading detectionmanager.html: %v", err), http.StatusInternalServerError) return } + defer htmlFile.Close() - htmlContent := string(htmlFile) + htmlContent, err := io.ReadAll(htmlFile) + if err != nil { + http.Error(w, fmt.Sprintf("Error reading detectionmanager.html content: %v", err), http.StatusInternalServerError) + return + } - fmt.Fprint(w, htmlContent) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write(htmlContent) } func ServeConfigPage(w http.ResponseWriter, r *http.Request) { - htmlFile, err := os.ReadFile(config.ConfigHtmlPath) + htmlFS, err := fs.Sub(config.V1UIFS, "UIMod/onboard_bundled/ui") + if err != nil { + http.Error(w, "Error accessing Virt FS: "+err.Error(), http.StatusInternalServerError) + return + } + + htmlFile, err := htmlFS.Open("config.html") if err != nil { http.Error(w, fmt.Sprintf("Error reading config.html: %v", err), http.StatusInternalServerError) return } + defer htmlFile.Close() + + htmlContentBytes, err := io.ReadAll(htmlFile) + if err != nil { + http.Error(w, fmt.Sprintf("Error reading config.html content: %v", err), http.StatusInternalServerError) + return + } - htmlContent := string(htmlFile) + htmlContent := string(htmlContentBytes) // Determine selected attributes for boolean fields upnpTrueSelected := "" From e758afc069ac210d3c32b00bfbfd68bcbb74fdd5 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 01:17:37 +0200 Subject: [PATCH 17/33] changed log stream connection message from info to debug level --- src/detectionmgr/logstream.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/detectionmgr/logstream.go b/src/detectionmgr/logstream.go index 59b43467..851be83f 100644 --- a/src/detectionmgr/logstream.go +++ b/src/detectionmgr/logstream.go @@ -21,7 +21,7 @@ func StreamLogs(detector *Detector) { logChan := ssestream.ConsoleStreamManager.AddInternalSubscriber() go func() { - logger.Detection.Info("Connected to internal log stream.") + logger.Detection.Debug("Connected to internal log stream.") for logMessage := range logChan { if config.IsDiscordEnabled { discordbot.PassLogStreamToDiscordLogBuffer(logMessage) From d153bc70696a77de9e0e879b7f92d79b32d98060 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 01:17:44 +0200 Subject: [PATCH 18/33] removed sleep delay between logging and SteamCMD installation for smoother execution flow --- src/setup/install.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/setup/install.go b/src/setup/install.go index f64113d9..71affb0a 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -37,7 +37,6 @@ func Install(wg *sync.WaitGroup) { logger.Install.Info("🔄Checking for Blacklist.txt...") checkAndCreateBlacklist() logger.Install.Info("✅Blacklist.txt verified or created.") - time.Sleep(2 * time.Second) // Small pause to let the user read potential errors // Step 3: Install and run SteamCMD logger.Install.Info("🔄Installing and running SteamCMD...") InstallAndRunSteamCMD() From f6ea4847889035b30f35e20cc3243f70083ac230 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 01:18:39 +0200 Subject: [PATCH 19/33] remove ANSI color constants from types.go since logger handles that for quite a while now --- src/detectionmgr/types.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/detectionmgr/types.go b/src/detectionmgr/types.go index 58e1dccc..c24773f6 100644 --- a/src/detectionmgr/types.go +++ b/src/detectionmgr/types.go @@ -3,17 +3,6 @@ package detectionmgr import "regexp" -const ( - // ANSI color codes for styling terminal output - colorReset = "\033[0m" - colorRed = "\033[31m" - colorGreen = "\033[32m" - colorYellow = "\033[33m" - colorBlue = "\033[34m" - colorMagenta = "\033[35m" - colorCyan = "\033[36m" -) - // EventType defines the type of event detected type EventType string From 6422c3f5cfa3ea3709f93dbe5405ee8ca2718cf3 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 01:42:52 +0200 Subject: [PATCH 20/33] refactor: move SaveConfig function to loader to be able to save config after startup --- server.go | 1 + src/config/config.go | 17 +++++++++++++++++ src/configchanger/configuration.go | 24 ++---------------------- src/loader/loader.go | 22 ++++++++++++++++++++++ src/web/login.go | 5 ++--- 5 files changed, 44 insertions(+), 25 deletions(-) diff --git a/server.go b/server.go index 10c4350a..f518b777 100644 --- a/server.go +++ b/server.go @@ -49,6 +49,7 @@ func main() { loader.InitVirtFS(v1uiFS) loader.ReloadAll() loader.InitDetector() + loader.AfterStartComplete() terminal.StartConsole(&wg) diff --git a/src/config/config.go b/src/config/config.go index 85530db3..8296c66b 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -245,3 +245,20 @@ func applyConfig(cfg *JsonConfig) { // use Safebackups folder either way. ConfiguredSafeBackupDir = filepath.Join("./saves/", WorldName, "Safebackups") } + +// use SaveConfig EXCLUSIVELY though loader.SaveConfig to trigger a reload afterwards! +func SaveConfig(cfg *JsonConfig) error { + file, err := os.Create(ConfigPath) + if err != nil { + return fmt.Errorf("error creating config.json: %v", err) + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(cfg); err != nil { + return fmt.Errorf("error encoding config.json: %v", err) + } + + return nil +} diff --git a/src/configchanger/configuration.go b/src/configchanger/configuration.go index e0037da4..46c1a604 100644 --- a/src/configchanger/configuration.go +++ b/src/configchanger/configuration.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "net/http" - "os" "reflect" "strconv" @@ -13,25 +12,6 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/loader" ) -// SaveConfig writes the given config to file and reloads it -func SaveConfig(cfg *config.JsonConfig) error { - file, err := os.Create(config.ConfigPath) - if err != nil { - return fmt.Errorf("error creating config.json: %v", err) - } - defer file.Close() - - encoder := json.NewEncoder(file) - encoder.SetIndent("", " ") - if err := encoder.Encode(cfg); err != nil { - return fmt.Errorf("error encoding config.json: %v", err) - } - - // Reload using the loader package - loader.ReloadAll() - return nil -} - func SaveConfigForm(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) @@ -84,7 +64,7 @@ func SaveConfigForm(w http.ResponseWriter, r *http.Request) { } // Save the updated config - if err := SaveConfig(existingConfig); err != nil { + if err := loader.SaveConfig(existingConfig); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -176,7 +156,7 @@ func SaveConfigRestful(w http.ResponseWriter, r *http.Request) { } // Save the updated config - if err := SaveConfig(existingConfig); err != nil { + if err := loader.SaveConfig(existingConfig); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/src/loader/loader.go b/src/loader/loader.go index 49dee9e6..fffdaa11 100644 --- a/src/loader/loader.go +++ b/src/loader/loader.go @@ -121,3 +121,25 @@ func RestartBackend() { func InitVirtFS(v1uiFS embed.FS) { config.SetV1UIFS(v1uiFS) } + +// this is a Hack, but it works for now. Ideally, move the getter setter logic from SteamServerUI to StationeersServerUI, but not feasible at the moment. +func SaveConfig(cfg *config.JsonConfig) error { + err := config.SaveConfig(cfg) + if err != nil { + logger.Core.Error("Failed to save config: " + err.Error()) + return err + } + ReloadConfig() + return err +} + +func AfterStartComplete() { + existingConfig, err := config.LoadConfig() + if err != nil { + logger.Core.Error("AfterStartComplete: Failed to Load config: " + err.Error()) + } + err2 := SaveConfig(existingConfig) + if err2 != nil { + logger.Core.Error("AfterStartComplete: Failed to save config: " + err2.Error()) + } +} diff --git a/src/web/login.go b/src/web/login.go index 030eb284..69422cdb 100644 --- a/src/web/login.go +++ b/src/web/login.go @@ -10,7 +10,6 @@ import ( "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/configchanger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/loader" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/security" @@ -192,7 +191,7 @@ func RegisterUserHandler(w http.ResponseWriter, r *http.Request) { existingConfig.Users[creds.Username] = hashedPassword // Persist the updated config - if err := configchanger.SaveConfig(existingConfig); err != nil { + if err := loader.SaveConfig(existingConfig); err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error - Failed to save config"}) @@ -235,7 +234,7 @@ func SetupFinalizeHandler(w http.ResponseWriter, r *http.Request) { newConfig.AuthEnabled = &isTrue // Set the pointer to true // Save the updated config - err = configchanger.SaveConfig(newConfig) + err = loader.SaveConfig(newConfig) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) From 2415fe88d7bb6275ed44b3c2cb6edd184fe36d65 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 01:57:34 +0200 Subject: [PATCH 21/33] not sure why, but re-added main.js to detectionmanager as it had it before --- UIMod/onboard_bundled/detectionmanager/detectionmanager.html | 1 + 1 file changed, 1 insertion(+) diff --git a/UIMod/onboard_bundled/detectionmanager/detectionmanager.html b/UIMod/onboard_bundled/detectionmanager/detectionmanager.html index e6a8e87b..f848d514 100644 --- a/UIMod/onboard_bundled/detectionmanager/detectionmanager.html +++ b/UIMod/onboard_bundled/detectionmanager/detectionmanager.html @@ -113,6 +113,7 @@

Regex Detection:

+ \ No newline at end of file From 1afd77657a8b86fa1080e266211c50896a169745 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 02:02:42 +0200 Subject: [PATCH 22/33] implement cleanup of old UI mod files post-v5.5 and copy customdetections.json to new location if it exists to persist user config --- src/loader/loader.go | 67 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/src/loader/loader.go b/src/loader/loader.go index fffdaa11..4de1a94e 100644 --- a/src/loader/loader.go +++ b/src/loader/loader.go @@ -4,6 +4,8 @@ package loader import ( "embed" "fmt" + "os" + "path/filepath" "strconv" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/backupmgr" @@ -138,8 +140,67 @@ func AfterStartComplete() { if err != nil { logger.Core.Error("AfterStartComplete: Failed to Load config: " + err.Error()) } - err2 := SaveConfig(existingConfig) - if err2 != nil { - logger.Core.Error("AfterStartComplete: Failed to save config: " + err2.Error()) + err = SaveConfig(existingConfig) + if err != nil { + logger.Core.Error("AfterStartComplete: Failed to save config: " + err.Error()) + } + err = cleanUpOldUIModFolderFiles() + if err != nil { + logger.Core.Error("AfterStartComplete: Failed to clean up old pre-v5.5 UI mod folder files: " + err.Error()) + } +} + +func cleanUpOldUIModFolderFiles() error { + uiModFolder := config.UIModFolder + customdetectionsSourceFile := filepath.Join(uiModFolder, "detectionmanager", "customdetections.json") + customdetectionsDestinationFile := config.CustomDetectionsFilePath + oldUiFolder := filepath.Join(uiModFolder, "ui") // used to test if we need clean up from a structure before v5.5 (since we now have embedded assets) + + //if uiModFolder doesn't contain a folder called UI, return early as there is nothing to clean up + if _, err := os.Stat(oldUiFolder); os.IsNotExist(err) { + return nil } + + // Copy customdetections.json to the destination path + if _, err := os.Stat(customdetectionsSourceFile); err == nil { + // Ensure destination directory exists + destDir := filepath.Dir(customdetectionsDestinationFile) + if err := os.MkdirAll(destDir, 0755); err != nil { + return fmt.Errorf("failed to create destination directory: %w", err) + } + + // Read source file + data, err := os.ReadFile(customdetectionsSourceFile) + if err != nil { + return fmt.Errorf("failed to read source file: %w", err) + } + + // Write to destination file + if err := os.WriteFile(customdetectionsDestinationFile, data, 0644); err != nil { + return fmt.Errorf("failed to write destination file: %w", err) + } + } else if !os.IsNotExist(err) { + logger.Core.Error("Error moving customdetections.json file to new location: " + err.Error()) + } + + // List of folders to remove + foldersToRemove := []string{ + filepath.Join(uiModFolder, "detectionmanager"), + filepath.Join(uiModFolder, "ui"), + filepath.Join(uiModFolder, "twoboxform"), + filepath.Join(uiModFolder, "assets"), + } + + // Remove specified folders if they exist + for _, folder := range foldersToRemove { + if _, err := os.Stat(folder); err == nil { + if err := os.RemoveAll(folder); err != nil { + return fmt.Errorf("failed to remove folder %s: %w", folder, err) + } + } else if !os.IsNotExist(err) { + return fmt.Errorf("error checking folder %s: %w", folder, err) + } + } + + return nil } From 1961499afd7dea8a3994dfb3c534a33a54a3aabb Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 02:28:57 +0200 Subject: [PATCH 23/33] fix: update last occurence of dotSaveVal to IsNewTerrainAndSaveSystem --- src/config/config.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/config.go b/src/config/config.go index 8296c66b..d8d79b66 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -137,9 +137,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.IsNewTerrainAndSaveSystem, "ENABLE_DOT_SAVES", false) - IsNewTerrainAndSaveSystem = dotSaveVal - cfg.IsNewTerrainAndSaveSystem = &dotSaveVal + isNewTerrainAndSaveSystemVal := getBool(cfg.IsNewTerrainAndSaveSystem, "ENABLE_DOT_SAVES", false) + IsNewTerrainAndSaveSystem = isNewTerrainAndSaveSystemVal + cfg.IsNewTerrainAndSaveSystem = &isNewTerrainAndSaveSystemVal GameBranch = getString(cfg.GameBranch, "GAME_BRANCH", "public") Difficulty = getString(cfg.Difficulty, "DIFFICULTY", "") From 6bd5caa295d4f405b5fd6180cb4c9e755c4cbc34 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 02:29:16 +0200 Subject: [PATCH 24/33] fix: remove redundand double dots at end of sentence --- UIMod/onboard_bundled/ui/config.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UIMod/onboard_bundled/ui/config.html b/UIMod/onboard_bundled/ui/config.html index 4209df40..f6ef7afd 100644 --- a/UIMod/onboard_bundled/ui/config.html +++ b/UIMod/onboard_bundled/ui/config.html @@ -235,7 +235,7 @@

Advanced Configuration

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

+

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 JacksonTheMaster's Migration tool.. Related "how to" Information on the FAQ there also applies to the Dedicated Server.

From ed1dc85cc8be9e8349bc21a5995fd64fa493ede5 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 02:30:08 +0200 Subject: [PATCH 25/33] update branch to nightly for upcoming merge --- 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 d8d79b66..40ce8a6f 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -12,7 +12,7 @@ import ( var ( // All configuration variables can be found in vars.go Version = "5.5.1" - Branch = "indev" + Branch = "nightly" ) type JsonConfig struct { From 87117372b89f0659097251c5f53b49a4cdf425a3 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 03:13:59 +0200 Subject: [PATCH 26/33] move UI mod folder cleanup logic to setup package for better organization, add CleanUpOldExecutables function that renames old executables to _old --- src/loader/loader.go | 61 ++------------------- src/setup/cleanup.go | 128 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 57 deletions(-) create mode 100644 src/setup/cleanup.go diff --git a/src/loader/loader.go b/src/loader/loader.go index 4de1a94e..ec141ab2 100644 --- a/src/loader/loader.go +++ b/src/loader/loader.go @@ -4,8 +4,6 @@ package loader import ( "embed" "fmt" - "os" - "path/filepath" "strconv" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/backupmgr" @@ -144,63 +142,12 @@ func AfterStartComplete() { if err != nil { logger.Core.Error("AfterStartComplete: Failed to save config: " + err.Error()) } - err = cleanUpOldUIModFolderFiles() + err = setup.CleanUpOldUIModFolderFiles() if err != nil { logger.Core.Error("AfterStartComplete: Failed to clean up old pre-v5.5 UI mod folder files: " + err.Error()) } -} - -func cleanUpOldUIModFolderFiles() error { - uiModFolder := config.UIModFolder - customdetectionsSourceFile := filepath.Join(uiModFolder, "detectionmanager", "customdetections.json") - customdetectionsDestinationFile := config.CustomDetectionsFilePath - oldUiFolder := filepath.Join(uiModFolder, "ui") // used to test if we need clean up from a structure before v5.5 (since we now have embedded assets) - - //if uiModFolder doesn't contain a folder called UI, return early as there is nothing to clean up - if _, err := os.Stat(oldUiFolder); os.IsNotExist(err) { - return nil - } - - // Copy customdetections.json to the destination path - if _, err := os.Stat(customdetectionsSourceFile); err == nil { - // Ensure destination directory exists - destDir := filepath.Dir(customdetectionsDestinationFile) - if err := os.MkdirAll(destDir, 0755); err != nil { - return fmt.Errorf("failed to create destination directory: %w", err) - } - - // Read source file - data, err := os.ReadFile(customdetectionsSourceFile) - if err != nil { - return fmt.Errorf("failed to read source file: %w", err) - } - - // Write to destination file - if err := os.WriteFile(customdetectionsDestinationFile, data, 0644); err != nil { - return fmt.Errorf("failed to write destination file: %w", err) - } - } else if !os.IsNotExist(err) { - logger.Core.Error("Error moving customdetections.json file to new location: " + err.Error()) - } - - // List of folders to remove - foldersToRemove := []string{ - filepath.Join(uiModFolder, "detectionmanager"), - filepath.Join(uiModFolder, "ui"), - filepath.Join(uiModFolder, "twoboxform"), - filepath.Join(uiModFolder, "assets"), - } - - // Remove specified folders if they exist - for _, folder := range foldersToRemove { - if _, err := os.Stat(folder); err == nil { - if err := os.RemoveAll(folder); err != nil { - return fmt.Errorf("failed to remove folder %s: %w", folder, err) - } - } else if !os.IsNotExist(err) { - return fmt.Errorf("error checking folder %s: %w", folder, err) - } + err = setup.CleanUpOldExecutables() + if err != nil { + logger.Core.Error("AfterStartComplete: Failed to clean up old executables: " + err.Error()) } - - return nil } diff --git a/src/setup/cleanup.go b/src/setup/cleanup.go new file mode 100644 index 00000000..65e4cf58 --- /dev/null +++ b/src/setup/cleanup.go @@ -0,0 +1,128 @@ +package setup + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" +) + +func CleanUpOldUIModFolderFiles() error { + uiModFolder := config.UIModFolder + customdetectionsSourceFile := filepath.Join(uiModFolder, "detectionmanager", "customdetections.json") + customdetectionsDestinationFile := config.CustomDetectionsFilePath + oldUiFolder := filepath.Join(uiModFolder, "ui") // used to test if we need clean up from a structure before v5.5 (since we now have embedded assets) + + //if uiModFolder doesn't contain a folder called UI, return early as there is nothing to clean up + if _, err := os.Stat(oldUiFolder); os.IsNotExist(err) { + return nil + } + + // Copy customdetections.json to the destination path + if _, err := os.Stat(customdetectionsSourceFile); err == nil { + // Ensure destination directory exists + destDir := filepath.Dir(customdetectionsDestinationFile) + if err := os.MkdirAll(destDir, 0755); err != nil { + return fmt.Errorf("failed to create destination directory: %w", err) + } + + // Read source file + data, err := os.ReadFile(customdetectionsSourceFile) + if err != nil { + return fmt.Errorf("failed to read source file: %w", err) + } + + // Write to destination file + if err := os.WriteFile(customdetectionsDestinationFile, data, 0644); err != nil { + return fmt.Errorf("failed to write destination file: %w", err) + } + } else if !os.IsNotExist(err) { + logger.Core.Error("Error moving customdetections.json file to new location: " + err.Error()) + } + + // List of folders to remove + foldersToRemove := []string{ + filepath.Join(uiModFolder, "detectionmanager"), + filepath.Join(uiModFolder, "ui"), + filepath.Join(uiModFolder, "twoboxform"), + filepath.Join(uiModFolder, "assets"), + } + + // Remove specified folders if they exist + for _, folder := range foldersToRemove { + if _, err := os.Stat(folder); err == nil { + if err := os.RemoveAll(folder); err != nil { + return fmt.Errorf("failed to remove folder %s: %w", folder, err) + } + } else if !os.IsNotExist(err) { + return fmt.Errorf("error checking folder %s: %w", folder, err) + } + } + + return nil +} + +func CleanUpOldExecutables() error { + // Exit early if update is disabled to allow running old versions if needed + if !config.IsUpdateEnabled { + return nil + } + currentBackendVersion := config.Version + pattern := `StationeersServerControlv(\d+\.\d+\.\d+)(?:\.exe|\.x86_64)$` + re, err := regexp.Compile(pattern) + if err != nil { + return fmt.Errorf("failed to compile regex: %w", err) + } + + // Get current directory + dir, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current directory: %w", err) + } + + // Walk through the directory + err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Skip directories and non-matching files + if info.IsDir() || !re.MatchString(info.Name()) { + return nil + } + + // Extract version from filename + matches := re.FindStringSubmatch(info.Name()) + if len(matches) < 2 { + return nil + } + fileVersion := matches[1] + + // Skip if the version matches the current backend version + if fileVersion == currentBackendVersion { + return nil + } + + // Generate new filename with _old prefix + newName := "_old" + info.Name() + newPath := filepath.Join(filepath.Dir(path), newName) + + // Rename the file + err = os.Rename(path, newPath) + if err != nil { + return fmt.Errorf("failed to rename %s to %s: %w", path, newName, err) + } + logger.Install.Info(fmt.Sprintf("Old Executable cleanup: Renamed %s to %s", path, newName)) + + return nil + }) + + if err != nil { + return fmt.Errorf("error walking directory: %w", err) + } + + return nil +} From e6d2a0105c64eac9418d7dabba953ab3112489cd Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 03:15:14 +0200 Subject: [PATCH 27/33] fix: improve log message clarity for disabled SSUICLI in runtime commands --- src/terminal/runtimecommands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal/runtimecommands.go b/src/terminal/runtimecommands.go index 816dd01c..0c4764e3 100644 --- a/src/terminal/runtimecommands.go +++ b/src/terminal/runtimecommands.go @@ -48,7 +48,7 @@ func RegisterCommand(name string, handler CommandFunc, aliases ...string) { // 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...") + logger.Core.Info("SSUICLI runtime console is disabled in config, skipping...") return } wg.Add(1) From f92e08f720bad92903009e5b00ea66d4e2c78bf1 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 03:17:37 +0200 Subject: [PATCH 28/33] remove customdetections.json from git --- UIMod/config/customdetections.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 UIMod/config/customdetections.json diff --git a/UIMod/config/customdetections.json b/UIMod/config/customdetections.json deleted file mode 100644 index 0637a088..00000000 --- a/UIMod/config/customdetections.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file From 0b2b37326c3e9ceb52a5f3ae8d64a93b13af42ae Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 03:31:30 +0200 Subject: [PATCH 29/33] included customdetections in gitignore again --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e622e1c3..0c1f7417 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ debug.log modconfig.xml UIMod/config/config.json C:/custom/file.txt +UIMod/config/customdetections.json From 9c303e1581d8397877226ad83eb97389f293c9c1 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 03:32:19 +0200 Subject: [PATCH 30/33] re-add Arguments in log output for consistency with windows versions --- src/gamemgr/processmanagement.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gamemgr/processmanagement.go b/src/gamemgr/processmanagement.go index 63efc119..08a9285e 100644 --- a/src/gamemgr/processmanagement.go +++ b/src/gamemgr/processmanagement.go @@ -102,12 +102,14 @@ func InternalStartServer() error { logger.Core.Info("BepInEx/Doorstop environment configured for server process") } logger.Core.Info("• Executable: " + config.ExePath + " (with SSCM)") + logger.Core.Info("• Arguments: " + strings.Join(args, " ")) } if !config.IsSSCMEnabled && runtime.GOOS == "linux" { // Use ExePath directly as the command cmd = exec.Command(config.ExePath, args...) logger.Core.Info("• Executable: " + config.ExePath) + logger.Core.Info("• Arguments: " + strings.Join(args, " ")) } if runtime.GOOS == "windows" { From 66e1d506dde09ab2ce81b8f2407a46262b514a0c Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 03:33:06 +0200 Subject: [PATCH 31/33] update default world type references to use 'Vulcan' in order to work on stable and beta / newterrain as Moon is now called Lunar --- UIMod/onboard_bundled/ui/config.html | 2 +- src/config/config.go | 2 +- src/web/TwoBoxForm.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/UIMod/onboard_bundled/ui/config.html b/UIMod/onboard_bundled/ui/config.html index f6ef7afd..53369cd4 100644 --- a/UIMod/onboard_bundled/ui/config.html +++ b/UIMod/onboard_bundled/ui/config.html @@ -75,7 +75,7 @@

Basic Server Settings

pattern="^[A-Z].*(\s[A-Z].*)?$" required>
Name of save folder. Must be capitalized. To create a new world, provide the - World type to generate. (MyMoonMap Moon)
+ World type to generate. (MyVulcanMap Vulcan)
diff --git a/src/config/config.go b/src/config/config.go index 40ce8a6f..d7f8b270 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -146,7 +146,7 @@ func applyConfig(cfg *JsonConfig) { 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") + SaveInfo = getString(cfg.SaveInfo, "SAVE_INFO", "Vulcan Vulcan") ServerMaxPlayers = getString(cfg.ServerMaxPlayers, "SERVER_MAX_PLAYERS", "6") ServerPassword = getString(cfg.ServerPassword, "SERVER_PASSWORD", "") ServerAuthSecret = getString(cfg.ServerAuthSecret, "SERVER_AUTH_SECRET", "") diff --git a/src/web/TwoBoxForm.go b/src/web/TwoBoxForm.go index 667343a8..0d71f492 100644 --- a/src/web/TwoBoxForm.go +++ b/src/web/TwoBoxForm.go @@ -99,7 +99,7 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { ID: "save_identifier", Title: "Stationeers Server UI", HeaderTitle: "Save Identifier Setup", - StepMessage: "Set a save identifier like 'SpaceStation13 Moon'. Capitalize the first letter of each word. Possible World types can be found in the Stationeers Wiki or the Stationeers Server UI GitHub Wiki.", + StepMessage: "Set a save identifier like 'SpaceStation13 Vulcan'. Capitalize the first letter of each word. Possible World types can be found in the Stationeers Wiki -> Dedicated Server", PrimaryPlaceholderText: "Requires a SaveName and WorldType for first start!", PrimaryLabel: "Save Identifier", SecondaryLabel: "", From f795fe2d2545736730f435e65e7888dbef8ef28c Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 04:07:01 +0200 Subject: [PATCH 32/33] refactor backup manager initialization to be asynchronous and wait for stationeers to create the world folder before starting the watcher in order for stationeers to be able to detect that it needs to create a new world (stationeers uses DoesFolderExist there) --- src/backupmgr/backupinterface.go | 17 +++++++--- src/backupmgr/manager.go | 55 +++++++++++++++++++++++++++----- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/backupmgr/backupinterface.go b/src/backupmgr/backupinterface.go index 1c040fcb..9b3abd07 100644 --- a/src/backupmgr/backupinterface.go +++ b/src/backupmgr/backupinterface.go @@ -5,6 +5,7 @@ import ( "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" ) // GlobalBackupManager is the singleton instance of the backup manager @@ -20,16 +21,24 @@ func InitGlobalBackupManager(config BackupConfig) error { } GlobalBackupManager = NewBackupManager(config) - if err := GlobalBackupManager.Initialize(); err != nil { - return err - } + + // Start initialization asynchronously + _ = GlobalBackupManager.Initialize() // Update all active HTTP handlers with the new manager for _, handler := range activeHTTPHandlers { handler.manager = GlobalBackupManager } - return GlobalBackupManager.Start() + // Start the backup manager in a goroutine to avoid blocking + go func() { + if err := GlobalBackupManager.Start(); err != nil { + logger.Backup.Error("Failed to start global backup manager: " + err.Error()) + } + }() + + // Return immediately, initialization will complete in the background + return nil } // RegisterHTTPHandler registers an HTTP handler to be updated when the manager changes diff --git a/src/backupmgr/manager.go b/src/backupmgr/manager.go index 0bda65cf..0142309d 100644 --- a/src/backupmgr/manager.go +++ b/src/backupmgr/manager.go @@ -20,22 +20,61 @@ Background routines (file watching and cleanup) only start when Start() is calle can coexist but may conflict if configured with overlapping directories. */ -// Initialize sets up required directories -func (m *BackupManager) Initialize() error { +// Initialize checks for BackupDir and waits until it exists, then ensures SafeBackupDir exists. +// It returns a channel that signals when initialization is complete or an error occurs. +func (m *BackupManager) Initialize() <-chan error { m.mu.Lock() defer m.mu.Unlock() - if err := os.MkdirAll(m.config.BackupDir, os.ModePerm); err != nil { - return err - } - return os.MkdirAll(m.config.SafeBackupDir, os.ModePerm) + result := make(chan error, 1) + + go func() { + const timeout = 90 * time.Minute + const pollInterval = 1000 * time.Millisecond + deadline := time.Now().Add(timeout) + + // Wait for BackupDir to exist + for { + if _, err := os.Stat(m.config.BackupDir); err == nil { + // Directory exists, proceed + break + } else if !os.IsNotExist(err) { + // An error other than "not exists" occurred + result <- fmt.Errorf("error checking backup directory %s: %v", m.config.BackupDir, err) + return + } + + if time.Now().After(deadline) { + result <- fmt.Errorf("timeout waiting for backup directory %s to be created", m.config.BackupDir) + return + } + + // Wait before checking again + time.Sleep(pollInterval) + //logger.Backup.Warn("Backup manager waiting for save folder to be created...") + } + + // Ensure SafeBackupDir exists, create it if it doesn't + if err := os.MkdirAll(m.config.SafeBackupDir, os.ModePerm); err != nil { + result <- fmt.Errorf("error creating safe backup directory %s: %v", m.config.SafeBackupDir, err) + return + } + + result <- nil // Signal successful initialization + }() + + return result } // Start begins the backup monitoring and cleanup routines func (m *BackupManager) Start() error { - if err := m.Initialize(); err != nil { - return fmt.Errorf("failed to initialize backup directories: %w", err) + // Wait for initialization to complete + logger.Backup.Warn("Backup manager waiting for save folder initialization...") + initResult := <-m.Initialize() + if initResult != nil { + return fmt.Errorf("failed to initialize backup manager: %w", initResult) } + logger.Backup.Warn("Backup manager initialized") // Start file watcher watcher, err := newFsWatcher(m.config.BackupDir) From b626d0910203117ab228c64a161cf455c28e57c2 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 4 Aug 2025 04:18:19 +0200 Subject: [PATCH 33/33] Updated UI texts and removed big warning box --- .../assets/js/console-manager.js | 2 +- UIMod/onboard_bundled/ui/config.html | 2 +- UIMod/onboard_bundled/ui/index.html | 17 ++++++++--------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/UIMod/onboard_bundled/assets/js/console-manager.js b/UIMod/onboard_bundled/assets/js/console-manager.js index 82f62c2e..70f9c04a 100644 --- a/UIMod/onboard_bundled/assets/js/console-manager.js +++ b/UIMod/onboard_bundled/assets/js/console-manager.js @@ -216,7 +216,7 @@ function handleConsole() { createCommandInput(); // Add input after boot addMessage(bootCompleteMessage, '#0f0'); //addMessage("StationeersServerUI is becoming SteamServerUI!", '#ff4500'); - addMessage("Please mind the New Terrain System warning below", '#ff4500'); + //addMessage("Please mind the New Terrain System warning below", '#ff4500'); consoleElement.scrollTop = consoleElement.scrollHeight; }, 500); } diff --git a/UIMod/onboard_bundled/ui/config.html b/UIMod/onboard_bundled/ui/config.html index 53369cd4..99fbc9e6 100644 --- a/UIMod/onboard_bundled/ui/config.html +++ b/UIMod/onboard_bundled/ui/config.html @@ -75,7 +75,7 @@

Basic Server Settings

pattern="^[A-Z].*(\s[A-Z].*)?$" required>
Name of save folder. Must be capitalized. To create a new world, provide the - World type to generate. (MyVulcanMap Vulcan)
+ World type to generate. (MyVulcanMap Vulcan) WorldTypes can be found in the Stationeers Wiki -> Dedicated Server page.
diff --git a/UIMod/onboard_bundled/ui/index.html b/UIMod/onboard_bundled/ui/index.html index 3a6f00c4..784a2522 100644 --- a/UIMod/onboard_bundled/ui/index.html +++ b/UIMod/onboard_bundled/ui/index.html @@ -143,7 +143,7 @@

Backups List

    -
    +