Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ rocketstation_DedicatedServer_Data/
UnityCrashHandler64.exe
UnityPlayer.dll
setting.xml
rocketstation_DedicatedServer.exe
rocketstation_DedicatedServer*
/saves/*
.github/workflows/nightly-sync.yml
repos.md
Expand All @@ -22,3 +22,16 @@ Blacklist.txt
UIMod/detectionmanager/customdetections.json
UIMod/tls/cert.pem
UIMod/tls/key.pem
steamapps/**
steamcmd/**
rocketstation_BurstDebugInformation_DoNotShip/**
StationeersServerControlv*
UnityPlayer.so
BepInEx/**
*doorstep*
*doorstop*
run_bepinex.sh
debug.log
modconfig.xml
UIMod/config/config.json
C:/custom/file.txt
48 changes: 47 additions & 1 deletion UIMod/ui/config.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ <h1>Server Configuration</h1>
<button class="section-nav-button" data-section="basic-settings">Basic Settings</button>
<button class="section-nav-button" data-section="network-settings">Network Settings</button>
<button class="section-nav-button" data-section="advanced-settings">Advanced Settings</button>
<button class="section-nav-button" data-section="beta-settings">Beta Settings</button>
</div>

<form id="server-config-form" action="/saveconfigasjson" method="post">
Expand All @@ -76,7 +77,7 @@ <h3 class="section-title">Basic Server Settings</h3>
provide the
World type to generate. (MyMoonMap Moon)</div>
</div>

<div class="form-group">
<label for="ServerMaxPlayers">Max Players:</label>
<input type="text" id="ServerMaxPlayers" name="ServerMaxPlayers"
Expand Down Expand Up @@ -220,6 +221,51 @@ <h3 class="section-title">Advanced Configuration</h3>
value="{{AutoRestartServerTimer}}">
<div class="input-info">Timeframe in <strong> minutes </strong> 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 <strong> ingame </strong> before restarting.</div>
</div>

<div class="form-group">
<label for="gameBranch">Game Branch</label>
<input type="text" id="gameBranch" name="gameBranch"
value="{{gameBranch}}">
<div class="input-info">Branch of the game to use. When changed, requires to restart SSUI!</div>
</div>

</div>
</div>

<div id="beta-settings" class="config-section">
<h3 class="section-title">BETA ONLY: NEW TERRAIN AND SAVE SYSTEM SETTINGS</h3>
<div class = "beta-warning"></div>
<p>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 <a href="https://sst.jxsn.dev">Migration tool.</a>. Related "how to" Information on the FAQ there also applies to the Dedicated Server.</a>.</p>
<div class="channel-grid">
<div class="form-group">
<label for="IsNewTerrainAndSaveSystem">Use New Terrain and Save System:</label>
<select id="IsNewTerrainAndSaveSystem" name="IsNewTerrainAndSaveSystem" required>
<option value="true" {{IsNewTerrainAndSaveSystemTrueSelected}}>TRUE</option>
<option value="false" {{IsNewTerrainAndSaveSystemFalseSelected}}>FALSE</option>
</select>
<div class="input-info">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.</div>
</div>

<div class="form-group">
<label for="Difficulty">Difficulty:</label>
<input type="text" id="Difficulty" name="Difficulty"
value="{{Difficulty}}" pattern="^\S*$" required>
<div class="input-info">Difficulty to create the world with. Defaults to Normal if empty.</div>
</div>

<div class="form-group">
<label for="StartCondition">Start Condition:</label>
<input type="text" id="StartCondition" name="StartCondition"
value="{{StartCondition}}" pattern="^\S*$" required>
<div class="input-info">Start condition to create the world with. Defaults to the default start condition for the world type if empty.</div>
</div>

<div class="form-group">
<label for="StartLocation">Start Location:</label>
<input type="text" id="StartLocation" name="StartLocation"
value="{{StartLocation}}" pattern="^\S*$" required>
<div class="input-info">Start location to create the world with. Defaults to "DefaultStartLocation" if empty.</div>
</div>
</div>
</div>

Expand Down
7 changes: 0 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down
3 changes: 3 additions & 0 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -44,5 +45,7 @@ func main() {
loader.ReloadAll()
loader.InitDetector()

terminal.StartConsole(&wg)

web.StartWebServer(&wg)
}
46 changes: 31 additions & 15 deletions src/backupmgr/cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,48 +124,64 @@ 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
}

group := groups[index]
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
}

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)
}
}
Expand Down
14 changes: 12 additions & 2 deletions src/backupmgr/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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())
Expand Down
94 changes: 75 additions & 19 deletions src/backupmgr/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"time"

"github.com/JacksonTheMaster/StationeersServerUI/v5/src/config"
"github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger"
)

Expand All @@ -15,32 +16,87 @@ func (m *BackupManager) RestoreBackup(index int) error {
defer m.mu.Unlock()
logger.Backup.Info("Restoring backup with index " + fmt.Sprintf("%d", index))

files := []struct {
backupName string
backupNameAlt string
destName string
}{
{fmt.Sprintf("world_meta(%d).xml", index), fmt.Sprintf("world_meta(%d)_AutoSave.xml", index), "world_meta.xml"},
{fmt.Sprintf("world(%d).xml", index), fmt.Sprintf("world(%d)_AutoSave.xml", index), "world.xml"},
{fmt.Sprintf("world(%d).bin", index), fmt.Sprintf("world(%d)_AutoSave.bin", index), "world.bin"},
groups, err := m.getBackupGroups()
if err != nil {
return fmt.Errorf("failed to get backup groups: %w", err)
}

var targetGroup BackupGroup
for _, group := range groups {
if group.Index == index {
targetGroup = group
break
}
}

if targetGroup.Index == 0 {
return fmt.Errorf("no backup found with index %d", index)
}

restoredFiles := make(map[string]string)

for _, file := range files {
backupFile := filepath.Join(m.config.SafeBackupDir, file.backupName)
destFile := filepath.Join("./saves/"+config.WorldName, file.destName)
// Handle .save file or old-style trio
if targetGroup.BinFile != "" && strings.HasSuffix(targetGroup.BinFile, ".save") {
// .save file case
backupFile := targetGroup.BinFile
destFile := filepath.Join("./saves/"+m.config.WorldName, m.config.WorldName+".save")

if err := copyFile(backupFile, destFile); err != nil {
// Try alternative name
backupFileAlt := filepath.Join(m.config.SafeBackupDir, file.backupNameAlt)
if err := copyFile(backupFileAlt, destFile); err != nil {
m.revertRestore(restoredFiles)
return fmt.Errorf("failed to restore %s: %w", file.backupName, err)
// Before restore, check if we have existing .save files in the root saves/WorldName dir
saveDir := filepath.Join("./saves/", m.config.WorldName)
files, err := os.ReadDir(saveDir)
if err != nil {
return fmt.Errorf("failed to read save directory %s: %w", saveDir, err)
}

for _, file := range files {
if file.IsDir() {
continue
}
if strings.HasSuffix(file.Name(), ".save") {
existingFile := filepath.Join(saveDir, file.Name())
// Move existing .save file to SafeBackupDir with timestamp to avoid overwrites
timestamp := time.Now().Format("2006-01-02_15-04-05")
savedPreviousHeadSaveFilePath := filepath.Join(m.config.SafeBackupDir, fmt.Sprintf("%s_%s_%s", "oldHeadSaveBackup", timestamp, file.Name()))
if err := os.Rename(existingFile, savedPreviousHeadSaveFilePath); err != nil {
return fmt.Errorf("failed to move existing HEAD .save file %s to %s: %w", existingFile, savedPreviousHeadSaveFilePath, err)
}
logger.Backup.Info("Moved previous HEAD .save file to: " + savedPreviousHeadSaveFilePath)
}
backupFile = backupFileAlt
}

// Now copy the new .save file
if err := copyFile(backupFile, destFile); err != nil {
m.revertRestore(restoredFiles)
return fmt.Errorf("failed to restore .save file %s: %w", backupFile, err)
}
restoredFiles[destFile] = backupFile
} else {
// Old-style trio (world_meta.xml, world.xml, world.bin)
files := []struct {
backupName string
backupNameAlt string
destName string
}{
{fmt.Sprintf("world_meta(%d).xml", index), fmt.Sprintf("world_meta(%d)_AutoSave.xml", index), "world_meta.xml"},
{fmt.Sprintf("world(%d).xml", index), fmt.Sprintf("world(%d)_AutoSave.xml", index), "world.xml"},
{fmt.Sprintf("world(%d).bin", index), fmt.Sprintf("world(%d)_AutoSave.bin", index), "world.bin"},
}

for _, file := range files {
backupFile := filepath.Join(m.config.SafeBackupDir, file.backupName)
destFile := filepath.Join("./saves/"+m.config.WorldName, file.destName)

if err := copyFile(backupFile, destFile); err != nil {
// Try alternative name
backupFileAlt := filepath.Join(m.config.SafeBackupDir, file.backupNameAlt)
if err := copyFile(backupFileAlt, destFile); err != nil {
m.revertRestore(restoredFiles)
return fmt.Errorf("failed to restore %s: %w", file.backupName, err)
}
backupFile = backupFileAlt
}
restoredFiles[destFile] = backupFile
}
}
logger.Backup.Debug(fmt.Sprintf("%v", restoredFiles))

Expand Down
Loading