From 35a0838569feffaea178cdf3437fff2c5a130302 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:04:01 +0200 Subject: [PATCH 01/37] reordered JsonConfig fields for improved clarity, backwards compatibile --- src/config/config.go | 137 ++++++++++++++++++++++++------------------- 1 file changed, 76 insertions(+), 61 deletions(-) diff --git a/src/config/config.go b/src/config/config.go index a6fcccd6..edcfa72d 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -11,71 +11,86 @@ import ( var ( // All configuration variables can be found in vars.go - Version = "5.6.2" + Version = "5.6.3" Branch = "release" ) 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"` // Number of most recent backups to keep (default: 2000) - IsCleanupEnabled *bool `json:"isCleanupEnabled"` // Enable automatic cleanup of backups (default: false) - BackupKeepDailyFor int `json:"backupKeepDailyFor"` // Retention period in hours for daily backups - BackupKeepWeeklyFor int `json:"backupKeepWeeklyFor"` // Retention period in hours for weekly backups - BackupKeepMonthlyFor int `json:"backupKeepMonthlyFor"` // Retention period in hours for monthly backups - BackupCleanupInterval int `json:"backupCleanupInterval"` // Hours between backup cleanup operations - BackupWaitTime int `json:"backupWaitTime"` // Seconds to wait before copying backups - IsNewTerrainAndSaveSystem *bool `json:"IsNewTerrainAndSaveSystem"` // Use new terrain and save system - 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"` - LogClutterToConsole *bool `json:"LogClutterToConsole"` - SubsystemFilters []string `json:"subsystemFilters"` - IsUpdateEnabled *bool `json:"IsUpdateEnabled"` - IsSSCMEnabled *bool `json:"IsSSCMEnabled"` - AutoRestartServerTimer string `json:"AutoRestartServerTimer"` - AllowPrereleaseUpdates *bool `json:"AllowPrereleaseUpdates"` - AllowMajorUpdates *bool `json:"AllowMajorUpdates"` - IsConsoleEnabled *bool `json:"IsConsoleEnabled"` - LanguageSetting string `json:"LanguageSetting"` - AutoStartServerOnStartup *bool `json:"AutoStartServerOnStartup"` - SSUIIdentifier string `json:"SSUIIdentifier"` - SSUIWebPort string `json:"SSUIWebPort"` + // reordered in 5.6.4 to simplify the order of the config file. + + // Gameserver Settings + 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"` + AdditionalParams string `json:"AdditionalParams"` + + // Logging and debug settings + Debug *bool `json:"Debug"` + CreateSSUILogFile *bool `json:"CreateSSUILogFile"` + LogLevel int `json:"LogLevel"` + SubsystemFilters []string `json:"subsystemFilters"` + + // Authentication Settings + 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"` + + // SSUI Settings + IsNewTerrainAndSaveSystem *bool `json:"IsNewTerrainAndSaveSystem"` // Use new terrain and save system + ExePath string `json:"ExePath"` + LogClutterToConsole *bool `json:"LogClutterToConsole"` + IsUpdateEnabled *bool `json:"IsUpdateEnabled"` + IsSSCMEnabled *bool `json:"IsSSCMEnabled"` + AutoRestartServerTimer string `json:"AutoRestartServerTimer"` + IsConsoleEnabled *bool `json:"IsConsoleEnabled"` + LanguageSetting string `json:"LanguageSetting"` + AutoStartServerOnStartup *bool `json:"AutoStartServerOnStartup"` + SSUIIdentifier string `json:"SSUIIdentifier"` + SSUIWebPort string `json:"SSUIWebPort"` + + // Update Settings + AllowPrereleaseUpdates *bool `json:"AllowPrereleaseUpdates"` + AllowMajorUpdates *bool `json:"AllowMajorUpdates"` + + // Discord Settings + 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"` + + //Backup Settings + BackupKeepLastN int `json:"backupKeepLastN"` // Number of most recent backups to keep (default: 2000) + IsCleanupEnabled *bool `json:"isCleanupEnabled"` // Enable automatic cleanup of backups (default: false) + BackupKeepDailyFor int `json:"backupKeepDailyFor"` // Retention period in hours for daily backups + BackupKeepWeeklyFor int `json:"backupKeepWeeklyFor"` // Retention period in hours for weekly backups + BackupKeepMonthlyFor int `json:"backupKeepMonthlyFor"` // Retention period in hours for monthly backups + BackupCleanupInterval int `json:"backupCleanupInterval"` // Hours between backup cleanup operations + BackupWaitTime int `json:"backupWaitTime"` // Seconds to wait before copying backups } // LoadConfig loads and initializes the configuration From ffa4e0ff27cc92eeb1f0c56e872c24354d4292b1 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:26:07 +0200 Subject: [PATCH 02/37] fixed steamcmd update via discord, updated /help --- src/discordbot/handleSlashcommands.go | 41 +++++++++++++++++++++------ 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/discordbot/handleSlashcommands.go b/src/discordbot/handleSlashcommands.go index fd110b89..a599d0c6 100644 --- a/src/discordbot/handleSlashcommands.go +++ b/src/discordbot/handleSlashcommands.go @@ -97,24 +97,45 @@ func handleStatus(s *discordgo.Session, i *discordgo.InteractionCreate, data Emb } func handleUpdate(s *discordgo.Session, i *discordgo.InteractionCreate, data EmbedData) error { + thinkingData := EmbedData{ + Title: "🎮 Gameserver Update", + Description: "The Backend is processing the gameserver update via SteamCMD. Please wait, this may take a while...", + Color: 0xFFA500, // Orange color for in-progress + } + + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{generateEmbed(thinkingData)}, + }, + }) + if err != nil { + return err + } data.Title = "🎮 Gameserver Update" - data.Description = "Updating the gameserver via SteamCMD..." - data.Color = 0xFFA500 + data.Description = "Gameserver update completed." + data.Color = 0x00FF00 // Green for completion (will adjust if error) + if gamemgr.InternalIsServerRunning() { - SendMessageToControlChannel("❗ Server is running, stopping server first...") gamemgr.InternalStopServer() - time.Sleep(10000 * time.Millisecond) + time.Sleep(10 * time.Second) // Wait for server to stop } - _, err := setup.InstallAndRunSteamCMD() + _, err = setup.InstallAndRunSteamCMD() data.Fields = []EmbedField{ {Name: "Update Status:", Value: map[bool]string{true: "🟢 Success", false: "🔴 Failed"}[err == nil], Inline: true}, } if err != nil { + data.Color = 0xFF0000 // Red for error data.Fields = append(data.Fields, EmbedField{Name: "Error:", Value: err.Error(), Inline: true}) } - return respond(s, i, data) + + // Edit the original message with "update completed" embed + _, err = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ + Embeds: &[]*discordgo.MessageEmbed{generateEmbed(data)}, + }) + return err } func handleHelp(s *discordgo.Session, i *discordgo.InteractionCreate, data EmbedData) error { @@ -122,12 +143,14 @@ func handleHelp(s *discordgo.Session, i *discordgo.InteractionCreate, data Embed data.Fields = []EmbedField{ {Name: "/start", Value: "Starts the server"}, {Name: "/stop", Value: "Stops the server"}, - {Name: "/restore ", Value: "Restores a backup"}, + {Name: "/status", Value: "Gets the running status of the gameserver process"}, + {Name: "/update", Value: "Updates the gameserver via SteamCMD"}, {Name: "/list [limit]", Value: "Lists recent backups (default: 5)"}, - {Name: "/help", Value: "Shows this help"}, + {Name: "/restore ", Value: "Restores a backup"}, {Name: "/bansteamid ", Value: "Bans a player"}, {Name: "/unbansteamid ", Value: "Unbans a player"}, - {Name: "/update", Value: "Updates the gameserver via SteamCMD"}, + {Name: "/command ", Value: "Sends a command to the gameserver console"}, + {Name: "/help", Value: "Shows this help"}, } return respond(s, i, data) } From 9ea6a090fd6ed04cdf5c3194e3ee413b700b39c2 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Thu, 11 Sep 2025 17:07:49 +0200 Subject: [PATCH 03/37] Update control panel (now pretty embed) and add steamcmd update functionality --- src/discordbot/controlpanel.go | 150 ++++++++++++++++++++++++++++++ src/discordbot/handleReactions.go | 54 ----------- src/discordbot/sendMessage.go | 43 ++++----- 3 files changed, 170 insertions(+), 77 deletions(-) create mode 100644 src/discordbot/controlpanel.go diff --git a/src/discordbot/controlpanel.go b/src/discordbot/controlpanel.go new file mode 100644 index 00000000..140fa555 --- /dev/null +++ b/src/discordbot/controlpanel.go @@ -0,0 +1,150 @@ +package discordbot + +import ( + "fmt" + "time" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" + "github.com/bwmarrin/discordgo" +) + +func sendControlPanel() { + if !config.GetIsDiscordEnabled() { + return + } + + // Create an embed for the control panel + embed := &discordgo.MessageEmbed{ + Title: "🚀 SSUI Control Panel", + Description: "Use the reactions below to manage the server:", + Color: 0x1e90ff, // Vibrant blue color + Fields: []*discordgo.MessageEmbedField{ + { + Name: "🟢 Start", + Value: "Launch the gameserver", + Inline: false, + }, + { + Name: "🔴 Stop", + Value: "Stop the gameserver", + Inline: false, + }, + { + Name: "🔄 Restart", + Value: "Restart the gameserver", + Inline: false, + }, + { + Name: "♻️ Update", + Value: "Update the gameserver via SteamCMD", + Inline: false, + }, + }, + Timestamp: time.Now().Format(time.RFC3339), + } + + // Send the embed message + msg, err := config.DiscordSession.ChannelMessageSendEmbed(config.GetControlPanelChannelID(), embed) + if err != nil { + logger.Discord.Error("Error sending control panel embed: " + err.Error()) + return + } + + clearMessagesAboveLastN(config.GetControlPanelChannelID(), 1) // Clear all old control panel messages + + // Add reactions (acting as buttons) to the control message + config.DiscordSession.MessageReactionAdd(config.GetControlPanelChannelID(), msg.ID, "🟢") // Start + config.DiscordSession.MessageReactionAdd(config.GetControlPanelChannelID(), msg.ID, "🔴") // Stop + config.DiscordSession.MessageReactionAdd(config.GetControlPanelChannelID(), msg.ID, "🔄") // Restart + config.DiscordSession.MessageReactionAdd(config.GetControlPanelChannelID(), msg.ID, "♻️") // Update + ControlMessageID = msg.ID +} + +func handleControlReactions(s *discordgo.Session, r *discordgo.MessageReactionAdd) { + // Ignore reactions from the bot itself + if r.UserID == s.State.User.ID { + return + } + + var actionMessage string + + switch r.Emoji.Name { + case "🟢": // Start action + gamemgr.InternalStartServer() + actionMessage = "🟢 Server is Starting..." + case "🔴": // Stop action + gamemgr.InternalStopServer() + actionMessage = "🔴 Server is Stopping..." + case "🔄": // Restart action + actionMessage = "🔄 Server is restarting..." + go func() { + // Perform stop operation + gamemgr.InternalStopServer() + + // Non-blocking delay using channel and goroutine + delayChan := make(chan bool) + go func() { + time.Sleep(5 * time.Second) + delayChan <- true + }() + + // Wait for delay to complete + <-delayChan + + // Start server after delay + gamemgr.InternalStartServer() + }() + case "♻️": // Update action + actionMessage = "♻️ Server is updating, this may take a while..." + go func() { + // Perform stop operation + gamemgr.InternalStopServer() + + // Non-blocking delay using channel and goroutine + delayChan := make(chan bool) + go func() { + time.Sleep(5 * time.Second) + delayChan <- true + }() + + // Wait for delay to complete + <-delayChan + + _, err := setup.InstallAndRunSteamCMD() + + Value := map[bool]string{true: "🟢 Success", false: "🔴 Failed"}[err == nil] + SendMessageToStatusChannel(fmt.Sprintf("SteamCMD Update status: %s", Value)) + sendTemporaryMessage(s, config.GetControlPanelChannelID(), fmt.Sprintf("SteamCMD Update status: %s", Value), 30*time.Second) + if err != nil { + SendMessageToStatusChannel(fmt.Sprintf("Update failed: %v", err.Error())) + } + }() + + default: + logger.Discord.Debug("Unknown reaction: " + r.Emoji.Name) + return + } + + // Get the user who triggered the action + user, err := s.User(r.UserID) + if err != nil { + logger.Discord.Error("Error fetching user details: " + err.Error()) + return + } + username := user.Username + + // Send a temporary confirmation message to the control panel channel + sendTemporaryMessage(s, config.GetControlPanelChannelID(), actionMessage, 30*time.Second) + + // Send the action message to the status channel + SendMessageToStatusChannel(fmt.Sprintf("%s triggered by %s.", actionMessage, username)) + + // Remove the reaction after processing + err = s.MessageReactionRemove(config.GetControlPanelChannelID(), r.MessageID, r.Emoji.APIName(), r.UserID) + if err != nil { + logger.Discord.Error("Error removing reaction: " + err.Error()) + } +} diff --git a/src/discordbot/handleReactions.go b/src/discordbot/handleReactions.go index c46cab44..351c9d2b 100644 --- a/src/discordbot/handleReactions.go +++ b/src/discordbot/handleReactions.go @@ -33,60 +33,6 @@ func listenToDiscordReactions(s *discordgo.Session, r *discordgo.MessageReaction // Optionally, we could add more message-specific handlers here for other features } -func handleControlReactions(s *discordgo.Session, r *discordgo.MessageReactionAdd) { - // handleControlReactions - Handles reactions for server control actions - var actionMessage string - - switch r.Emoji.Name { - case "▶️": // Start action - gamemgr.InternalStartServer() - actionMessage = "🕛Server is Starting..." - case "⏹️": // Stop action - gamemgr.InternalStopServer() - actionMessage = "🛑Server is Stopping..." - case "♻️": // Restart action - actionMessage = "♻️Server is restarting..." - go func() { - // Perform stop operation - gamemgr.InternalStopServer() - - // Non-blocking delay using channel and goroutine - delayChan := make(chan bool) - go func() { - time.Sleep(5 * time.Second) - delayChan <- true - }() - - // Wait for delay to complete - <-delayChan - - // Start server after delay - gamemgr.InternalStartServer() - }() - - default: - logger.Discord.Debug("Unknown reaction: " + r.Emoji.Name) - return - } - - // Get the user who triggered the action - user, err := s.User(r.UserID) - if err != nil { - logger.Discord.Error("Error fetching user details: " + err.Error()) - return - } - username := user.Username - - // Send the action message to the control channel - SendMessageToStatusChannel(fmt.Sprintf("%s triggered by %s.", actionMessage, username)) - - // Remove the reaction after processing - err = s.MessageReactionRemove(config.GetControlPanelChannelID(), r.MessageID, r.Emoji.APIName(), r.UserID) - if err != nil { - logger.Discord.Error("Error removing reaction: " + err.Error()) - } -} - // v4 FIXED, Unused in v4.3 func handleExceptionReactions(s *discordgo.Session, r *discordgo.MessageReactionAdd) { var actionMessage string diff --git a/src/discordbot/sendMessage.go b/src/discordbot/sendMessage.go index 367c1a24..f91ff83c 100644 --- a/src/discordbot/sendMessage.go +++ b/src/discordbot/sendMessage.go @@ -2,6 +2,7 @@ package discordbot import ( "strings" + "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" @@ -149,29 +150,6 @@ func sendMessageToErrorChannel(message string) []*discordgo.Message { return sentMessages } -func sendControlPanel() { - if !config.GetIsDiscordEnabled() { - return - } - messageContent := "Control Panel:\n\nReact with the following to perform actions:\n" + - "▶️ Start the server\n\n" + - "⏹️ Stop the server\n\n" + - "♻️ Restart the server\n\n" - - msg, err := config.DiscordSession.ChannelMessageSend(config.GetControlPanelChannelID(), messageContent) - if err != nil { - logger.Discord.Error("Error sending control panel: " + err.Error()) - return - } - - // Add reactions (acting as buttons) to the control message - config.DiscordSession.MessageReactionAdd(config.GetControlPanelChannelID(), msg.ID, "▶️") // Start - config.DiscordSession.MessageReactionAdd(config.GetControlPanelChannelID(), msg.ID, "⏹️") // Stop - config.DiscordSession.MessageReactionAdd(config.GetControlPanelChannelID(), msg.ID, "♻️") // Restart - ControlMessageID = msg.ID - clearMessagesAboveLastN(config.GetControlPanelChannelID(), 1) // Clear all old control panel messages -} - // This function is used to clear messages above the last N messages in a channel. If you call this with 5, it will clear all messages in the channel besides the most recent 5. func clearMessagesAboveLastN(channelID string, keep int) { go func() { @@ -201,3 +179,22 @@ func clearMessagesAboveLastN(channelID string, keep int) { } }() } + +// sendTemporaryMessage sends a message to the specified channel and deletes it after the given duration. +func sendTemporaryMessage(s *discordgo.Session, channelID, message string, duration time.Duration) { + // Send the message + msg, err := s.ChannelMessageSend(channelID, message) + if err != nil { + logger.Discord.Error("Error sending temporary message: " + err.Error()) + return + } + + // Schedule deletion after the specified duration + go func() { + time.Sleep(duration) + err := s.ChannelMessageDelete(channelID, msg.ID) + if err != nil { + logger.Discord.Error("Error deleting temporary message: " + err.Error()) + } + }() +} From 3ab312684fcc19e2f40f8cf5178ca99eea250d86 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Thu, 11 Sep 2025 17:15:11 +0200 Subject: [PATCH 04/37] Updated Discord Panel screenshot --- media/discord-panel.png | Bin 41436 -> 60329 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/media/discord-panel.png b/media/discord-panel.png index 49cfac043ca019c1936503475c3b5715a615ead1..9c38cf9fe4fe5d8cc1c2ac285caa5890eb0772c7 100644 GIT binary patch literal 60329 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�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 z%0A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 Jyry0FzW{4nQ2qb_ From 555d84109bd1d90d4e5019fef1d489266ae210d7 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Thu, 11 Sep 2025 18:45:43 +0200 Subject: [PATCH 05/37] added firstpass on gameserver auto updater, needs further testing --- src/config/getters.go | 6 ++ src/config/setters.go | 8 ++ src/config/vars.go | 1 + src/setup/install.go | 1 + src/setup/steamcmd-getappinfo.go | 133 +++++++++++++++++++++++++++++++ 5 files changed, 149 insertions(+) create mode 100644 src/setup/steamcmd-getappinfo.go diff --git a/src/config/getters.go b/src/config/getters.go index c7220d69..3196f24d 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -475,3 +475,9 @@ func GetGameServerAppID() string { defer ConfigMu.RUnlock() return GameServerAppID } + +func GetCurrentBranchBuildID() string { + ConfigMu.RLock() + defer ConfigMu.RUnlock() + return CurrentBranchBuildID +} diff --git a/src/config/setters.go b/src/config/setters.go index 19e58065..fa5608ad 100644 --- a/src/config/setters.go +++ b/src/config/setters.go @@ -30,6 +30,14 @@ func SetIsSSCMEnabled(value bool) error { return safeSaveConfig() } +func SetCurrentBranchBuildID(value string) error { + ConfigMu.Lock() + defer ConfigMu.Unlock() + + CurrentBranchBuildID = value + return nil +} + // ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT // ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT // ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT diff --git a/src/config/vars.go b/src/config/vars.go index e8e3cda8..478812f6 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -61,6 +61,7 @@ var ( LanguageSetting string AutoStartServerOnStartup bool SSUIIdentifier string + CurrentBranchBuildID string // ONLY RUNTIME ) // Discord integration diff --git a/src/setup/install.go b/src/setup/install.go index fd66e9d2..f3b93201 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -42,6 +42,7 @@ func Install(wg *sync.WaitGroup) { // Step 3: Install and run SteamCMD logger.Install.Info("🔄Installing and running SteamCMD...") InstallAndRunSteamCMD() + initAppInfoPoller() // init the steamcmd app info poll check to check for new gameserver updates logger.Install.Info("✅Setup complete!") } diff --git a/src/setup/steamcmd-getappinfo.go b/src/setup/steamcmd-getappinfo.go new file mode 100644 index 00000000..2506cdf7 --- /dev/null +++ b/src/setup/steamcmd-getappinfo.go @@ -0,0 +1,133 @@ +package setup + +import ( + "bytes" + "fmt" + "maps" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + "sync" + "time" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" +) + +var ( + branches = make(map[string]string) + branchesLock sync.RWMutex // Protects branches map for concurrent access +) + +func initAppInfoPoller() { + go func() { + for { + err := getAppInfo() + if err != nil { + logger.Install.Error("❌ Failed to get app info: " + err.Error() + "\n") + } + time.Sleep(5 * time.Minute) + } + }() +} + +// getAppInfo fetches the branches and their build IDs for the specified app ID using SteamCMD +// and stores them in the package-level branches map. +func getAppInfo() error { + steamcmddir := SteamCMDLinuxDir + executable := "steamcmd.sh" + appid := config.GetGameServerAppID() + + if runtime.GOOS == "windows" { + executable = "steamcmd.exe" + steamcmddir = SteamCMDWindowsDir + } + + // Build SteamCMD command with +app_info_update to ensure fresh data + cmd := exec.Command(filepath.Join(steamcmddir, executable), "+login", "anonymous", "+app_info_update", "1", "+app_info_print", appid, "+quit") + + // Capture output instead of printing directly + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + // Log the command + if config.GetLogLevel() == 10 { + cmdString := strings.Join(cmd.Args, " ") + logger.Install.Debug("🕑 Running SteamCMD for app info: " + cmdString) + } + + // Run the command + err := cmd.Run() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + logger.Install.Errorf("❌ SteamCMD app info failed (code %d): %s\n", exitErr.ExitCode(), stderr.String()) + return fmt.Errorf("SteamCMD app info failed with exit code %d: %w", exitErr.ExitCode(), err) + } + logger.Install.Errorf("❌ Error running SteamCMD app info: %s\n", err.Error()) + return fmt.Errorf("failed to run SteamCMD app info: %w", err) + } + + // Extract branches and build IDs + newBranches, err := extractBranches(stdout.String()) + if err != nil { + logger.Install.Debug("❌ Failed to extract branches: " + err.Error() + "\n") + return err + } + + // Update package-level branches map + branchesLock.Lock() + maps.Copy(branches, newBranches) + branchesLock.Unlock() + + currentBranch := config.GetGameBranch() + if buildID, ok := branches[currentBranch]; ok { + if config.GetCurrentBranchBuildID() != "" && config.GetCurrentBranchBuildID() != buildID { + logger.Install.Info("❗New gameserver update detected!") + if config.GetIsUpdateEnabled() { + logger.Install.Info("🔍 Updating gameserver via SteamCMD...") + if gamemgr.InternalIsServerRunning() { + gamemgr.InternalStopServer() + } + _, err := InstallAndRunSteamCMD() + if err != nil { + logger.Install.Error("❌ Failed to update gameserver: " + err.Error() + "\n") + } + gamemgr.InternalStartServer() + } + } + config.SetCurrentBranchBuildID(buildID) + } + + return nil +} + +// extractBranches uses regex to extract branch names and their build IDs from SteamCMD output. +func extractBranches(output string) (map[string]string, error) { + // Regex to match the entire branches section, handling nested braces + pattern := regexp.MustCompile(`"branches"\s*\{([\s\S]*?)\}\s*\}`) + branchSection := pattern.FindStringSubmatch(output) + if len(branchSection) < 2 { + return nil, fmt.Errorf("branches section not found in output") + } + + // Regex to match individual branch blocks + branchPattern := regexp.MustCompile(`"([^"]+)"\s*\{[\s\S]*?"buildid"\s*"(\d+)"[\s\S]*?\}`) + matches := branchPattern.FindAllStringSubmatch(branchSection[1], -1) + + branches := make(map[string]string) + for _, match := range matches { + if len(match) >= 3 { + branches[match[1]] = match[2] + } + } + + if len(branches) == 0 { + return nil, fmt.Errorf("no branches with build IDs found") + } + + return branches, nil +} From 2ca880aeea00d1b5ebd6fd1b7d05511241306f3f Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Thu, 11 Sep 2025 21:26:49 +0200 Subject: [PATCH 06/37] Add AllowAutoGameServerUpdates config option and related UI elements --- UIMod/onboard_bundled/localization/en-US.json | 4 +- UIMod/onboard_bundled/ui/config.html | 9 + src/config/config.go | 142 +++++------ src/config/getters.go | 6 + src/config/setters.go | 8 + src/config/vars.go | 9 +- src/setup/install.go | 4 +- src/setup/steamcmd-getappinfo.go | 2 +- src/setup/steamcmd.go | 4 - src/web/configpage.go | 229 +++++++++--------- src/web/templatevars.go | 221 ++++++++--------- 11 files changed, 343 insertions(+), 295 deletions(-) diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index 5cc0140d..4780ddcc 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -75,7 +75,9 @@ "UIText_AutoRestartServerTimer": "Scheduled Gameserver Restart", "UIText_AutoRestartServerTimerInfo": "

Timeframe in minutes or time format (e.g., 15:04 or 03:04PM) to schedule an automatic gameserver restart. 0 = disabled, 1440 = 24 hours, etc. You will see 'Attention, server is restarting in 30/20/10/5 seconds!' messages ingame before the restart.

", "UIText_GameBranch": "Game Branch", - "UIText_GameBranchInfo": "Branch of the game to use. When changed, requires to restart SSUI!" + "UIText_GameBranchInfo": "Branch of the game to use. When changed, requires to restart SSUI!", + "UIText_AllowAutoGameServerUpdates": "Enable Auto Game Server Updates", + "UIText_AllowAutoGameServerUpdatesInfo": "Allow the gameserver to automatically query for and update to the latest version. Attention: Restarts the server when a new version was found and installed." }, "beta": { "UIText_BetaOnlySettings": "BETA ONLY: NEW TERRAIN AND SAVE SYSTEM SETTINGS", diff --git a/UIMod/onboard_bundled/ui/config.html b/UIMod/onboard_bundled/ui/config.html index 3a36a9f3..0b27d486 100644 --- a/UIMod/onboard_bundled/ui/config.html +++ b/UIMod/onboard_bundled/ui/config.html @@ -240,6 +240,15 @@

{{.UIText_AdvancedConfiguration}}

{{.UIText_AutoStartServerOnStartupInfo}}
+ +
+ + +
{{.UIText_AllowAutoGameServerUpdatesInfo}}
+
diff --git a/src/config/config.go b/src/config/config.go index edcfa72d..59fc5605 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.6.3" - Branch = "release" + Branch = "indev-no-steamcmd" ) type JsonConfig struct { @@ -20,16 +20,13 @@ type JsonConfig struct { // Gameserver Settings GameBranch string `json:"gameBranch"` - Difficulty string `json:"Difficulty"` - StartCondition string `json:"StartCondition"` - StartLocation string `json:"StartLocation"` + GamePort string `json:"GamePort"` 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"` @@ -40,6 +37,9 @@ type JsonConfig struct { ServerVisible *bool `json:"ServerVisible"` UseSteamP2P *bool `json:"UseSteamP2P"` AdditionalParams string `json:"AdditionalParams"` + Difficulty string `json:"Difficulty"` + StartCondition string `json:"StartCondition"` + StartLocation string `json:"StartLocation"` // Logging and debug settings Debug *bool `json:"Debug"` @@ -57,7 +57,6 @@ type JsonConfig struct { IsNewTerrainAndSaveSystem *bool `json:"IsNewTerrainAndSaveSystem"` // Use new terrain and save system ExePath string `json:"ExePath"` LogClutterToConsole *bool `json:"LogClutterToConsole"` - IsUpdateEnabled *bool `json:"IsUpdateEnabled"` IsSSCMEnabled *bool `json:"IsSSCMEnabled"` AutoRestartServerTimer string `json:"AutoRestartServerTimer"` IsConsoleEnabled *bool `json:"IsConsoleEnabled"` @@ -67,8 +66,10 @@ type JsonConfig struct { SSUIWebPort string `json:"SSUIWebPort"` // Update Settings - AllowPrereleaseUpdates *bool `json:"AllowPrereleaseUpdates"` - AllowMajorUpdates *bool `json:"AllowMajorUpdates"` + IsUpdateEnabled *bool `json:"IsUpdateEnabled"` + AllowPrereleaseUpdates *bool `json:"AllowPrereleaseUpdates"` + AllowMajorUpdates *bool `json:"AllowMajorUpdates"` + AllowAutoGameServerUpdates *bool `json:"AllowAutoGameServerUpdates"` // Discord Settings DiscordToken string `json:"discordToken"` @@ -231,6 +232,10 @@ func applyConfig(cfg *JsonConfig) { AllowMajorUpdates = allowMajorUpdatesVal cfg.AllowMajorUpdates = &allowMajorUpdatesVal + allowAutoGameServerUpdatesVal := getBool(cfg.AllowAutoGameServerUpdates, "ALLOW_AUTO_GAME_SERVER_UPDATES", false) + AllowAutoGameServerUpdates = allowAutoGameServerUpdatesVal + cfg.AllowAutoGameServerUpdates = &allowAutoGameServerUpdatesVal + SubsystemFilters = getStringSlice(cfg.SubsystemFilters, "SUBSYSTEM_FILTERS", []string{}) AutoRestartServerTimer = getString(cfg.AutoRestartServerTimer, "AUTO_RESTART_SERVER_TIMER", "0") isSSCMEnabledVal := getBool(cfg.IsSSCMEnabled, "IS_SSCM_ENABLED", true) @@ -275,66 +280,67 @@ func applyConfig(cfg *JsonConfig) { func safeSaveConfig() error { cfg := JsonConfig{ - DiscordToken: DiscordToken, - ControlChannelID: ControlChannelID, - StatusChannelID: StatusChannelID, - ConnectionListChannelID: ConnectionListChannelID, - LogChannelID: LogChannelID, - SaveChannelID: SaveChannelID, - ControlPanelChannelID: ControlPanelChannelID, - DiscordCharBufferSize: DiscordCharBufferSize, - BlackListFilePath: BlackListFilePath, - IsDiscordEnabled: &IsDiscordEnabled, - ErrorChannelID: ErrorChannelID, - BackupKeepLastN: BackupKeepLastN, - IsCleanupEnabled: &IsCleanupEnabled, - BackupKeepDailyFor: int(BackupKeepDailyFor / time.Hour), // Convert to hours - BackupKeepWeeklyFor: int(BackupKeepWeeklyFor / time.Hour), // Convert to hours - BackupKeepMonthlyFor: int(BackupKeepMonthlyFor / time.Hour), // Convert to hours - BackupCleanupInterval: int(BackupCleanupInterval / time.Hour), // Convert to hours - BackupWaitTime: int(BackupWaitTime / time.Second), // Convert to seconds - IsNewTerrainAndSaveSystem: &IsNewTerrainAndSaveSystem, - GameBranch: GameBranch, - Difficulty: Difficulty, - StartCondition: StartCondition, - StartLocation: StartLocation, - ServerName: ServerName, - SaveInfo: SaveInfo, - ServerMaxPlayers: ServerMaxPlayers, - ServerPassword: ServerPassword, - ServerAuthSecret: ServerAuthSecret, - AdminPassword: AdminPassword, - GamePort: GamePort, - UpdatePort: UpdatePort, - UPNPEnabled: &UPNPEnabled, - AutoSave: &AutoSave, - SaveInterval: SaveInterval, - AutoPauseServer: &AutoPauseServer, - LocalIpAddress: LocalIpAddress, - StartLocalHost: &StartLocalHost, - ServerVisible: &ServerVisible, - UseSteamP2P: &UseSteamP2P, - ExePath: ExePath, - AdditionalParams: AdditionalParams, - Users: Users, - AuthEnabled: &AuthEnabled, - JwtKey: JwtKey, - AuthTokenLifetime: AuthTokenLifetime, - Debug: &IsDebugMode, - CreateSSUILogFile: &CreateSSUILogFile, - LogLevel: LogLevel, - LogClutterToConsole: &LogClutterToConsole, - SubsystemFilters: SubsystemFilters, - IsUpdateEnabled: &IsUpdateEnabled, - IsSSCMEnabled: &IsSSCMEnabled, - AutoRestartServerTimer: AutoRestartServerTimer, - AllowPrereleaseUpdates: &AllowPrereleaseUpdates, - AllowMajorUpdates: &AllowMajorUpdates, - IsConsoleEnabled: &IsConsoleEnabled, - LanguageSetting: LanguageSetting, - AutoStartServerOnStartup: &AutoStartServerOnStartup, - SSUIIdentifier: SSUIIdentifier, - SSUIWebPort: SSUIWebPort, + DiscordToken: DiscordToken, + ControlChannelID: ControlChannelID, + StatusChannelID: StatusChannelID, + ConnectionListChannelID: ConnectionListChannelID, + LogChannelID: LogChannelID, + SaveChannelID: SaveChannelID, + ControlPanelChannelID: ControlPanelChannelID, + DiscordCharBufferSize: DiscordCharBufferSize, + BlackListFilePath: BlackListFilePath, + IsDiscordEnabled: &IsDiscordEnabled, + ErrorChannelID: ErrorChannelID, + BackupKeepLastN: BackupKeepLastN, + IsCleanupEnabled: &IsCleanupEnabled, + BackupKeepDailyFor: int(BackupKeepDailyFor / time.Hour), // Convert to hours + BackupKeepWeeklyFor: int(BackupKeepWeeklyFor / time.Hour), // Convert to hours + BackupKeepMonthlyFor: int(BackupKeepMonthlyFor / time.Hour), // Convert to hours + BackupCleanupInterval: int(BackupCleanupInterval / time.Hour), // Convert to hours + BackupWaitTime: int(BackupWaitTime / time.Second), // Convert to seconds + IsNewTerrainAndSaveSystem: &IsNewTerrainAndSaveSystem, + GameBranch: GameBranch, + Difficulty: Difficulty, + StartCondition: StartCondition, + StartLocation: StartLocation, + ServerName: ServerName, + SaveInfo: SaveInfo, + ServerMaxPlayers: ServerMaxPlayers, + ServerPassword: ServerPassword, + ServerAuthSecret: ServerAuthSecret, + AdminPassword: AdminPassword, + GamePort: GamePort, + UpdatePort: UpdatePort, + UPNPEnabled: &UPNPEnabled, + AutoSave: &AutoSave, + SaveInterval: SaveInterval, + AutoPauseServer: &AutoPauseServer, + LocalIpAddress: LocalIpAddress, + StartLocalHost: &StartLocalHost, + ServerVisible: &ServerVisible, + UseSteamP2P: &UseSteamP2P, + ExePath: ExePath, + AdditionalParams: AdditionalParams, + Users: Users, + AuthEnabled: &AuthEnabled, + JwtKey: JwtKey, + AuthTokenLifetime: AuthTokenLifetime, + Debug: &IsDebugMode, + CreateSSUILogFile: &CreateSSUILogFile, + LogLevel: LogLevel, + LogClutterToConsole: &LogClutterToConsole, + SubsystemFilters: SubsystemFilters, + IsUpdateEnabled: &IsUpdateEnabled, + IsSSCMEnabled: &IsSSCMEnabled, + AutoRestartServerTimer: AutoRestartServerTimer, + AllowPrereleaseUpdates: &AllowPrereleaseUpdates, + AllowMajorUpdates: &AllowMajorUpdates, + AllowAutoGameServerUpdates: &AllowAutoGameServerUpdates, + IsConsoleEnabled: &IsConsoleEnabled, + LanguageSetting: LanguageSetting, + AutoStartServerOnStartup: &AutoStartServerOnStartup, + SSUIIdentifier: SSUIIdentifier, + SSUIWebPort: SSUIWebPort, } file, err := os.Create(ConfigPath) diff --git a/src/config/getters.go b/src/config/getters.go index 3196f24d..f07be86a 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -481,3 +481,9 @@ func GetCurrentBranchBuildID() string { defer ConfigMu.RUnlock() return CurrentBranchBuildID } + +func GetAllowAutoGameServerUpdates() bool { + ConfigMu.RLock() + defer ConfigMu.RUnlock() + return AllowAutoGameServerUpdates +} diff --git a/src/config/setters.go b/src/config/setters.go index fa5608ad..10bf86fd 100644 --- a/src/config/setters.go +++ b/src/config/setters.go @@ -638,3 +638,11 @@ func SetIsConsoleEnabled(value bool) error { IsConsoleEnabled = value return safeSaveConfig() } + +func SetAllowAutoGameServerUpdates(value bool) error { + ConfigMu.Lock() + defer ConfigMu.Unlock() + + AllowAutoGameServerUpdates = value + return safeSaveConfig() +} diff --git a/src/config/vars.go b/src/config/vars.go index 478812f6..3c37b51d 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -104,11 +104,12 @@ var ( SSUIWebPort string ) -// SSUI Updates +// SSUI Updates and Game Server Updates var ( - IsUpdateEnabled bool - AllowPrereleaseUpdates bool - AllowMajorUpdates bool + IsUpdateEnabled bool + AllowPrereleaseUpdates bool + AllowMajorUpdates bool + AllowAutoGameServerUpdates bool ) // SSCM (Stationeers Server Command Manager) settings diff --git a/src/setup/install.go b/src/setup/install.go index f3b93201..e4420ecf 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -41,7 +41,9 @@ func Install(wg *sync.WaitGroup) { logger.Install.Info("✅Blacklist.txt verified or created.") // Step 3: Install and run SteamCMD logger.Install.Info("🔄Installing and running SteamCMD...") - InstallAndRunSteamCMD() + if config.GetBranch() != "indev-no-steamcmd" { + InstallAndRunSteamCMD() + } initAppInfoPoller() // init the steamcmd app info poll check to check for new gameserver updates logger.Install.Info("✅Setup complete!") } diff --git a/src/setup/steamcmd-getappinfo.go b/src/setup/steamcmd-getappinfo.go index 2506cdf7..c4601b83 100644 --- a/src/setup/steamcmd-getappinfo.go +++ b/src/setup/steamcmd-getappinfo.go @@ -87,7 +87,7 @@ func getAppInfo() error { if buildID, ok := branches[currentBranch]; ok { if config.GetCurrentBranchBuildID() != "" && config.GetCurrentBranchBuildID() != buildID { logger.Install.Info("❗New gameserver update detected!") - if config.GetIsUpdateEnabled() { + if config.GetAllowAutoGameServerUpdates() { logger.Install.Info("🔍 Updating gameserver via SteamCMD...") if gamemgr.InternalIsServerRunning() { gamemgr.InternalStopServer() diff --git a/src/setup/steamcmd.go b/src/setup/steamcmd.go index 1364ce29..04109a3f 100644 --- a/src/setup/steamcmd.go +++ b/src/setup/steamcmd.go @@ -29,10 +29,6 @@ const ( // InstallAndRunSteamCMD installs and runs SteamCMD based on the platform (Windows/Linux). // It returns the exit status of the SteamCMD execution and any error encountered. func InstallAndRunSteamCMD() (int, error) { - if config.GetBranch() == "indev-no-steamcmd" || config.GetIsDebugMode() { - logger.Install.Info("🔍 Detected indev-no-steamcmd branch or debug=true, skipping SteamCMD run") - return 0, nil - } if runtime.GOOS == "windows" { return installSteamCMDWindows() diff --git a/src/web/configpage.go b/src/web/configpage.go index 705b45ea..152f6a91 100644 --- a/src/web/configpage.go +++ b/src/web/configpage.go @@ -100,61 +100,72 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { steamP2PFalseSelected = "selected" } + autoGameServerUpdatesTrueSelected := "" + autoGameServerUpdatesFalseSelected := "" + if config.GetAllowAutoGameServerUpdates() { + autoGameServerUpdatesTrueSelected = "selected" + } else { + autoGameServerUpdatesFalseSelected = "selected" + } + data := ConfigTemplateData{ // Config values - DiscordToken: config.GetDiscordToken(), - ControlChannelID: config.GetControlChannelID(), - StatusChannelID: config.GetStatusChannelID(), - ConnectionListChannelID: config.GetConnectionListChannelID(), - LogChannelID: config.GetLogChannelID(), - SaveChannelID: config.GetSaveChannelID(), - ControlPanelChannelID: config.GetControlPanelChannelID(), - BlackListFilePath: config.GetBlackListFilePath(), - ErrorChannelID: config.GetErrorChannelID(), - IsDiscordEnabled: fmt.Sprintf("%v", config.GetIsDiscordEnabled()), - IsDiscordEnabledTrueSelected: discordTrueSelected, - IsDiscordEnabledFalseSelected: discordFalseSelected, - GameBranch: config.GetGameBranch(), - Difficulty: config.GetDifficulty(), - StartCondition: config.GetStartCondition(), - StartLocation: config.GetStartLocation(), - ServerName: config.GetServerName(), - SaveInfo: config.GetSaveInfo(), - ServerMaxPlayers: config.GetServerMaxPlayers(), - ServerPassword: config.GetServerPassword(), - ServerAuthSecret: config.GetServerAuthSecret(), - AdminPassword: config.GetAdminPassword(), - GamePort: config.GetGamePort(), - UpdatePort: config.GetUpdatePort(), - UPNPEnabled: fmt.Sprintf("%v", config.GetUPNPEnabled()), - UPNPEnabledTrueSelected: upnpTrueSelected, - UPNPEnabledFalseSelected: upnpFalseSelected, - AutoSave: fmt.Sprintf("%v", config.GetAutoSave()), - AutoSaveTrueSelected: autoSaveTrueSelected, - AutoSaveFalseSelected: autoSaveFalseSelected, - SaveInterval: config.GetSaveInterval(), - AutoPauseServer: fmt.Sprintf("%v", config.GetAutoPauseServer()), - AutoPauseServerTrueSelected: autoPauseTrueSelected, - AutoPauseServerFalseSelected: autoPauseFalseSelected, - LocalIpAddress: config.GetLocalIpAddress(), - StartLocalHost: fmt.Sprintf("%v", config.GetStartLocalHost()), - StartLocalHostTrueSelected: startLocalTrueSelected, - StartLocalHostFalseSelected: startLocalFalseSelected, - ServerVisible: fmt.Sprintf("%v", config.GetServerVisible()), - ServerVisibleTrueSelected: serverVisibleTrueSelected, - ServerVisibleFalseSelected: serverVisibleFalseSelected, - UseSteamP2P: fmt.Sprintf("%v", config.GetUseSteamP2P()), - UseSteamP2PTrueSelected: steamP2PTrueSelected, - UseSteamP2PFalseSelected: steamP2PFalseSelected, - ExePath: config.GetExePath(), - AdditionalParams: config.GetAdditionalParams(), - AutoRestartServerTimer: config.GetAutoRestartServerTimer(), - IsNewTerrainAndSaveSystem: fmt.Sprintf("%v", config.GetIsNewTerrainAndSaveSystem()), - IsNewTerrainAndSaveSystemTrueSelected: isNewTerrainAndSaveSystemTrueSelected, - IsNewTerrainAndSaveSystemFalseSelected: isNewTerrainAndSaveSystemFalseSelected, - AutoStartServerOnStartup: fmt.Sprintf("%v", config.GetAutoStartServerOnStartup()), - AutoStartServerOnStartupTrueSelected: autoStartServerTrueSelected, - AutoStartServerOnStartupFalseSelected: autoStartServerFalseSelected, + DiscordToken: config.GetDiscordToken(), + ControlChannelID: config.GetControlChannelID(), + StatusChannelID: config.GetStatusChannelID(), + ConnectionListChannelID: config.GetConnectionListChannelID(), + LogChannelID: config.GetLogChannelID(), + SaveChannelID: config.GetSaveChannelID(), + ControlPanelChannelID: config.GetControlPanelChannelID(), + BlackListFilePath: config.GetBlackListFilePath(), + ErrorChannelID: config.GetErrorChannelID(), + IsDiscordEnabled: fmt.Sprintf("%v", config.GetIsDiscordEnabled()), + IsDiscordEnabledTrueSelected: discordTrueSelected, + IsDiscordEnabledFalseSelected: discordFalseSelected, + GameBranch: config.GetGameBranch(), + Difficulty: config.GetDifficulty(), + StartCondition: config.GetStartCondition(), + StartLocation: config.GetStartLocation(), + ServerName: config.GetServerName(), + SaveInfo: config.GetSaveInfo(), + ServerMaxPlayers: config.GetServerMaxPlayers(), + ServerPassword: config.GetServerPassword(), + ServerAuthSecret: config.GetServerAuthSecret(), + AdminPassword: config.GetAdminPassword(), + GamePort: config.GetGamePort(), + UpdatePort: config.GetUpdatePort(), + UPNPEnabled: fmt.Sprintf("%v", config.GetUPNPEnabled()), + UPNPEnabledTrueSelected: upnpTrueSelected, + UPNPEnabledFalseSelected: upnpFalseSelected, + AutoSave: fmt.Sprintf("%v", config.GetAutoSave()), + AutoSaveTrueSelected: autoSaveTrueSelected, + AutoSaveFalseSelected: autoSaveFalseSelected, + SaveInterval: config.GetSaveInterval(), + AutoPauseServer: fmt.Sprintf("%v", config.GetAutoPauseServer()), + AutoPauseServerTrueSelected: autoPauseTrueSelected, + AutoPauseServerFalseSelected: autoPauseFalseSelected, + LocalIpAddress: config.GetLocalIpAddress(), + StartLocalHost: fmt.Sprintf("%v", config.GetStartLocalHost()), + StartLocalHostTrueSelected: startLocalTrueSelected, + StartLocalHostFalseSelected: startLocalFalseSelected, + ServerVisible: fmt.Sprintf("%v", config.GetServerVisible()), + ServerVisibleTrueSelected: serverVisibleTrueSelected, + ServerVisibleFalseSelected: serverVisibleFalseSelected, + UseSteamP2P: fmt.Sprintf("%v", config.GetUseSteamP2P()), + UseSteamP2PTrueSelected: steamP2PTrueSelected, + UseSteamP2PFalseSelected: steamP2PFalseSelected, + ExePath: config.GetExePath(), + AdditionalParams: config.GetAdditionalParams(), + AutoRestartServerTimer: config.GetAutoRestartServerTimer(), + IsNewTerrainAndSaveSystem: fmt.Sprintf("%v", config.GetIsNewTerrainAndSaveSystem()), + IsNewTerrainAndSaveSystemTrueSelected: isNewTerrainAndSaveSystemTrueSelected, + IsNewTerrainAndSaveSystemFalseSelected: isNewTerrainAndSaveSystemFalseSelected, + AutoStartServerOnStartup: fmt.Sprintf("%v", config.GetAutoStartServerOnStartup()), + AutoStartServerOnStartupTrueSelected: autoStartServerTrueSelected, + AutoStartServerOnStartupFalseSelected: autoStartServerFalseSelected, + AllowAutoGameServerUpdates: fmt.Sprintf("%v", config.GetAllowAutoGameServerUpdates()), + AllowAutoGameServerUpdatesTrueSelected: autoGameServerUpdatesTrueSelected, + AllowAutoGameServerUpdatesFalseSelected: autoGameServerUpdatesFalseSelected, // Localized UI text UIText_ServerConfig: localization.GetString("UIText_ServerConfig"), @@ -169,61 +180,63 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { UIText_BetaSettings: localization.GetString("UIText_BetaSettings"), UIText_BasicServerSettings: localization.GetString("UIText_BasicServerSettings"), - UIText_ServerName: localization.GetString("UIText_ServerName"), - UIText_ServerNameInfo: localization.GetString("UIText_ServerNameInfo"), - UIText_SaveFileName: localization.GetString("UIText_SaveFileName"), - UIText_SaveFileNameInfo: localization.GetString("UIText_SaveFileNameInfo"), - UIText_MaxPlayers: localization.GetString("UIText_MaxPlayers"), - UIText_MaxPlayersInfo: localization.GetString("UIText_MaxPlayersInfo"), - UIText_ServerPassword: localization.GetString("UIText_ServerPassword"), - UIText_ServerPasswordInfo: localization.GetString("UIText_ServerPasswordInfo"), - UIText_AdminPassword: localization.GetString("UIText_AdminPassword"), - UIText_AdminPasswordInfo: localization.GetString("UIText_AdminPasswordInfo"), - UIText_AutoSave: localization.GetString("UIText_AutoSave"), - UIText_AutoSaveInfo: localization.GetString("UIText_AutoSaveInfo"), - UIText_SaveInterval: localization.GetString("UIText_SaveInterval"), - UIText_SaveIntervalInfo: localization.GetString("UIText_SaveIntervalInfo"), - UIText_AutoPauseServer: localization.GetString("UIText_AutoPauseServer"), - UIText_AutoPauseServerInfo: localization.GetString("UIText_AutoPauseServerInfo"), - UIText_NetworkConfiguration: localization.GetString("UIText_NetworkConfiguration"), - UIText_GamePort: localization.GetString("UIText_GamePort"), - UIText_GamePortInfo: localization.GetString("UIText_GamePortInfo"), - UIText_UpdatePort: localization.GetString("UIText_UpdatePort"), - UIText_UpdatePortInfo: localization.GetString("UIText_UpdatePortInfo"), - UIText_UPNPEnabled: localization.GetString("UIText_UPNPEnabled"), - UIText_UPNPEnabledInfo: localization.GetString("UIText_UPNPEnabledInfo"), - UIText_LocalIpAddress: localization.GetString("UIText_LocalIpAddress"), - UIText_LocalIpAddressInfo: localization.GetString("UIText_LocalIpAddressInfo"), - UIText_StartLocalHost: localization.GetString("UIText_StartLocalHost"), - UIText_StartLocalHostInfo: localization.GetString("UIText_StartLocalHostInfo"), - UIText_ServerVisible: localization.GetString("UIText_ServerVisible"), - UIText_ServerVisibleInfo: localization.GetString("UIText_ServerVisibleInfo"), - UIText_UseSteamP2P: localization.GetString("UIText_UseSteamP2P"), - UIText_UseSteamP2PInfo: localization.GetString("UIText_UseSteamP2PInfo"), - UIText_AdvancedConfiguration: localization.GetString("UIText_AdvancedConfiguration"), - UIText_ServerAuthSecret: localization.GetString("UIText_ServerAuthSecret"), - UIText_ServerAuthSecretInfo: localization.GetString("UIText_ServerAuthSecretInfo"), - UIText_ServerExePath: localization.GetString("UIText_ServerExePath"), - UIText_ServerExePathInfo: localization.GetString("UIText_ServerExePathInfo"), - UIText_ServerExePathInfo2: localization.GetString("UIText_ServerExePathInfo2"), - UIText_AdditionalParams: localization.GetString("UIText_AdditionalParams"), - UIText_AdditionalParamsInfo: localization.GetString("UIText_AdditionalParamsInfo"), - UIText_AutoRestartServerTimer: localization.GetString("UIText_AutoRestartServerTimer"), - UIText_AutoRestartServerTimerInfo: localization.GetString("UIText_AutoRestartServerTimerInfo"), - UIText_GameBranch: localization.GetString("UIText_GameBranch"), - UIText_GameBranchInfo: localization.GetString("UIText_GameBranchInfo"), - UIText_BetaOnlySettings: localization.GetString("UIText_BetaOnlySettings"), - UIText_BetaWarning: localization.GetString("UIText_BetaWarning"), - UIText_UseNewTerrainAndSave: localization.GetString("UIText_UseNewTerrainAndSave"), - UIText_UseNewTerrainAndSaveInfo: localization.GetString("UIText_UseNewTerrainAndSaveInfo"), - UIText_Difficulty: localization.GetString("UIText_Difficulty"), - UIText_DifficultyInfo: localization.GetString("UIText_DifficultyInfo"), - UIText_StartCondition: localization.GetString("UIText_StartCondition"), - UIText_StartConditionInfo: localization.GetString("UIText_StartConditionInfo"), - UIText_StartLocation: localization.GetString("UIText_StartLocation"), - UIText_StartLocationInfo: localization.GetString("UIText_StartLocationInfo"), - UIText_AutoStartServerOnStartup: localization.GetString("UIText_AutoStartServerOnStartup"), - UIText_AutoStartServerOnStartupInfo: localization.GetString("UIText_AutoStartServerOnStartupInfo"), + UIText_ServerName: localization.GetString("UIText_ServerName"), + UIText_ServerNameInfo: localization.GetString("UIText_ServerNameInfo"), + UIText_SaveFileName: localization.GetString("UIText_SaveFileName"), + UIText_SaveFileNameInfo: localization.GetString("UIText_SaveFileNameInfo"), + UIText_MaxPlayers: localization.GetString("UIText_MaxPlayers"), + UIText_MaxPlayersInfo: localization.GetString("UIText_MaxPlayersInfo"), + UIText_ServerPassword: localization.GetString("UIText_ServerPassword"), + UIText_ServerPasswordInfo: localization.GetString("UIText_ServerPasswordInfo"), + UIText_AdminPassword: localization.GetString("UIText_AdminPassword"), + UIText_AdminPasswordInfo: localization.GetString("UIText_AdminPasswordInfo"), + UIText_AutoSave: localization.GetString("UIText_AutoSave"), + UIText_AutoSaveInfo: localization.GetString("UIText_AutoSaveInfo"), + UIText_SaveInterval: localization.GetString("UIText_SaveInterval"), + UIText_SaveIntervalInfo: localization.GetString("UIText_SaveIntervalInfo"), + UIText_AutoPauseServer: localization.GetString("UIText_AutoPauseServer"), + UIText_AutoPauseServerInfo: localization.GetString("UIText_AutoPauseServerInfo"), + UIText_NetworkConfiguration: localization.GetString("UIText_NetworkConfiguration"), + UIText_GamePort: localization.GetString("UIText_GamePort"), + UIText_GamePortInfo: localization.GetString("UIText_GamePortInfo"), + UIText_UpdatePort: localization.GetString("UIText_UpdatePort"), + UIText_UpdatePortInfo: localization.GetString("UIText_UpdatePortInfo"), + UIText_UPNPEnabled: localization.GetString("UIText_UPNPEnabled"), + UIText_UPNPEnabledInfo: localization.GetString("UIText_UPNPEnabledInfo"), + UIText_LocalIpAddress: localization.GetString("UIText_LocalIpAddress"), + UIText_LocalIpAddressInfo: localization.GetString("UIText_LocalIpAddressInfo"), + UIText_StartLocalHost: localization.GetString("UIText_StartLocalHost"), + UIText_StartLocalHostInfo: localization.GetString("UIText_StartLocalHostInfo"), + UIText_ServerVisible: localization.GetString("UIText_ServerVisible"), + UIText_ServerVisibleInfo: localization.GetString("UIText_ServerVisibleInfo"), + UIText_UseSteamP2P: localization.GetString("UIText_UseSteamP2P"), + UIText_UseSteamP2PInfo: localization.GetString("UIText_UseSteamP2PInfo"), + UIText_AdvancedConfiguration: localization.GetString("UIText_AdvancedConfiguration"), + UIText_ServerAuthSecret: localization.GetString("UIText_ServerAuthSecret"), + UIText_ServerAuthSecretInfo: localization.GetString("UIText_ServerAuthSecretInfo"), + UIText_ServerExePath: localization.GetString("UIText_ServerExePath"), + UIText_ServerExePathInfo: localization.GetString("UIText_ServerExePathInfo"), + UIText_ServerExePathInfo2: localization.GetString("UIText_ServerExePathInfo2"), + UIText_AdditionalParams: localization.GetString("UIText_AdditionalParams"), + UIText_AdditionalParamsInfo: localization.GetString("UIText_AdditionalParamsInfo"), + UIText_AutoRestartServerTimer: localization.GetString("UIText_AutoRestartServerTimer"), + UIText_AutoRestartServerTimerInfo: localization.GetString("UIText_AutoRestartServerTimerInfo"), + UIText_GameBranch: localization.GetString("UIText_GameBranch"), + UIText_GameBranchInfo: localization.GetString("UIText_GameBranchInfo"), + UIText_BetaOnlySettings: localization.GetString("UIText_BetaOnlySettings"), + UIText_BetaWarning: localization.GetString("UIText_BetaWarning"), + UIText_UseNewTerrainAndSave: localization.GetString("UIText_UseNewTerrainAndSave"), + UIText_UseNewTerrainAndSaveInfo: localization.GetString("UIText_UseNewTerrainAndSaveInfo"), + UIText_Difficulty: localization.GetString("UIText_Difficulty"), + UIText_DifficultyInfo: localization.GetString("UIText_DifficultyInfo"), + UIText_StartCondition: localization.GetString("UIText_StartCondition"), + UIText_StartConditionInfo: localization.GetString("UIText_StartConditionInfo"), + UIText_StartLocation: localization.GetString("UIText_StartLocation"), + UIText_StartLocationInfo: localization.GetString("UIText_StartLocationInfo"), + UIText_AutoStartServerOnStartup: localization.GetString("UIText_AutoStartServerOnStartup"), + UIText_AutoStartServerOnStartupInfo: localization.GetString("UIText_AutoStartServerOnStartupInfo"), + UIText_AllowAutoGameServerUpdates: localization.GetString("UIText_AllowAutoGameServerUpdates"), + UIText_AllowAutoGameServerUpdatesInfo: localization.GetString("UIText_AllowAutoGameServerUpdatesInfo"), UIText_DiscordIntegrationTitle: localization.GetString("UIText_DiscordIntegrationTitle"), UIText_DiscordBotToken: localization.GetString("UIText_DiscordBotToken"), diff --git a/src/web/templatevars.go b/src/web/templatevars.go index a3eb24ba..08d5bcad 100644 --- a/src/web/templatevars.go +++ b/src/web/templatevars.go @@ -23,59 +23,62 @@ type IndexTemplateData struct { // ConfigTemplateData holds data for the config page template type ConfigTemplateData struct { // Config values - DiscordToken string - ControlChannelID string - StatusChannelID string - ConnectionListChannelID string - LogChannelID string - SaveChannelID string - ControlPanelChannelID string - BlackListFilePath string - ErrorChannelID string - IsDiscordEnabled string - IsDiscordEnabledTrueSelected string - IsDiscordEnabledFalseSelected string - GameBranch string - Difficulty string - StartCondition string - StartLocation string - ServerName string - SaveInfo string - ServerMaxPlayers string - ServerPassword string - ServerAuthSecret string - AdminPassword string - GamePort string - UpdatePort string - UPNPEnabled string - UPNPEnabledTrueSelected string - UPNPEnabledFalseSelected string - AutoSave string - AutoSaveTrueSelected string - AutoSaveFalseSelected string - SaveInterval string - AutoPauseServer string - AutoPauseServerTrueSelected string - AutoPauseServerFalseSelected string - LocalIpAddress string - StartLocalHost string - StartLocalHostTrueSelected string - StartLocalHostFalseSelected string - ServerVisible string - ServerVisibleTrueSelected string - ServerVisibleFalseSelected string - UseSteamP2P string - UseSteamP2PTrueSelected string - UseSteamP2PFalseSelected string - ExePath string - AdditionalParams string - AutoRestartServerTimer string - IsNewTerrainAndSaveSystem string - IsNewTerrainAndSaveSystemTrueSelected string - IsNewTerrainAndSaveSystemFalseSelected string - AutoStartServerOnStartup string - AutoStartServerOnStartupTrueSelected string - AutoStartServerOnStartupFalseSelected string + DiscordToken string + ControlChannelID string + StatusChannelID string + ConnectionListChannelID string + LogChannelID string + SaveChannelID string + ControlPanelChannelID string + BlackListFilePath string + ErrorChannelID string + IsDiscordEnabled string + IsDiscordEnabledTrueSelected string + IsDiscordEnabledFalseSelected string + GameBranch string + Difficulty string + StartCondition string + StartLocation string + ServerName string + SaveInfo string + ServerMaxPlayers string + ServerPassword string + ServerAuthSecret string + AdminPassword string + GamePort string + UpdatePort string + UPNPEnabled string + UPNPEnabledTrueSelected string + UPNPEnabledFalseSelected string + AutoSave string + AutoSaveTrueSelected string + AutoSaveFalseSelected string + SaveInterval string + AutoPauseServer string + AutoPauseServerTrueSelected string + AutoPauseServerFalseSelected string + LocalIpAddress string + StartLocalHost string + StartLocalHostTrueSelected string + StartLocalHostFalseSelected string + ServerVisible string + ServerVisibleTrueSelected string + ServerVisibleFalseSelected string + UseSteamP2P string + UseSteamP2PTrueSelected string + UseSteamP2PFalseSelected string + ExePath string + AdditionalParams string + AutoRestartServerTimer string + IsNewTerrainAndSaveSystem string + IsNewTerrainAndSaveSystemTrueSelected string + IsNewTerrainAndSaveSystemFalseSelected string + AutoStartServerOnStartup string + AutoStartServerOnStartupTrueSelected string + AutoStartServerOnStartupFalseSelected string + AllowAutoGameServerUpdates string + AllowAutoGameServerUpdatesTrueSelected string + AllowAutoGameServerUpdatesFalseSelected string UIText_ServerConfig string UIText_DiscordIntegration string @@ -89,61 +92,63 @@ type ConfigTemplateData struct { UIText_BetaSettings string UIText_BasicServerSettings string - UIText_ServerName string - UIText_ServerNameInfo string - UIText_SaveFileName string - UIText_SaveFileNameInfo string - UIText_MaxPlayers string - UIText_MaxPlayersInfo string - UIText_ServerPassword string - UIText_ServerPasswordInfo string - UIText_AdminPassword string - UIText_AdminPasswordInfo string - UIText_AutoSave string - UIText_AutoSaveInfo string - UIText_SaveInterval string - UIText_SaveIntervalInfo string - UIText_AutoPauseServer string - UIText_AutoPauseServerInfo string - UIText_NetworkConfiguration string - UIText_GamePort string - UIText_GamePortInfo string - UIText_UpdatePort string - UIText_UpdatePortInfo string - UIText_UPNPEnabled string - UIText_UPNPEnabledInfo string - UIText_LocalIpAddress string - UIText_LocalIpAddressInfo string - UIText_StartLocalHost string - UIText_StartLocalHostInfo string - UIText_ServerVisible string - UIText_ServerVisibleInfo string - UIText_UseSteamP2P string - UIText_UseSteamP2PInfo string - UIText_AdvancedConfiguration string - UIText_ServerAuthSecret string - UIText_ServerAuthSecretInfo string - UIText_ServerExePath string - UIText_ServerExePathInfo string - UIText_ServerExePathInfo2 string - UIText_AdditionalParams string - UIText_AdditionalParamsInfo string - UIText_AutoRestartServerTimer string - UIText_AutoRestartServerTimerInfo string - UIText_GameBranch string - UIText_GameBranchInfo string - UIText_BetaOnlySettings string - UIText_BetaWarning string - UIText_UseNewTerrainAndSave string - UIText_UseNewTerrainAndSaveInfo string - UIText_Difficulty string - UIText_DifficultyInfo string - UIText_StartCondition string - UIText_StartConditionInfo string - UIText_StartLocation string - UIText_StartLocationInfo string - UIText_AutoStartServerOnStartup string - UIText_AutoStartServerOnStartupInfo string + UIText_ServerName string + UIText_ServerNameInfo string + UIText_SaveFileName string + UIText_SaveFileNameInfo string + UIText_MaxPlayers string + UIText_MaxPlayersInfo string + UIText_ServerPassword string + UIText_ServerPasswordInfo string + UIText_AdminPassword string + UIText_AdminPasswordInfo string + UIText_AutoSave string + UIText_AutoSaveInfo string + UIText_SaveInterval string + UIText_SaveIntervalInfo string + UIText_AutoPauseServer string + UIText_AutoPauseServerInfo string + UIText_NetworkConfiguration string + UIText_GamePort string + UIText_GamePortInfo string + UIText_UpdatePort string + UIText_UpdatePortInfo string + UIText_UPNPEnabled string + UIText_UPNPEnabledInfo string + UIText_LocalIpAddress string + UIText_LocalIpAddressInfo string + UIText_StartLocalHost string + UIText_StartLocalHostInfo string + UIText_ServerVisible string + UIText_ServerVisibleInfo string + UIText_UseSteamP2P string + UIText_UseSteamP2PInfo string + UIText_AdvancedConfiguration string + UIText_ServerAuthSecret string + UIText_ServerAuthSecretInfo string + UIText_ServerExePath string + UIText_ServerExePathInfo string + UIText_ServerExePathInfo2 string + UIText_AdditionalParams string + UIText_AdditionalParamsInfo string + UIText_AutoRestartServerTimer string + UIText_AutoRestartServerTimerInfo string + UIText_GameBranch string + UIText_GameBranchInfo string + UIText_BetaOnlySettings string + UIText_BetaWarning string + UIText_UseNewTerrainAndSave string + UIText_UseNewTerrainAndSaveInfo string + UIText_Difficulty string + UIText_DifficultyInfo string + UIText_StartCondition string + UIText_StartConditionInfo string + UIText_StartLocation string + UIText_StartLocationInfo string + UIText_AutoStartServerOnStartup string + UIText_AutoStartServerOnStartupInfo string + UIText_AllowAutoGameServerUpdates string + UIText_AllowAutoGameServerUpdatesInfo string UIText_DiscordIntegrationTitle string UIText_DiscordBotToken string From a001e6e40efd22c023eee7a811ccadbf141e6b13 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Thu, 11 Sep 2025 21:36:13 +0200 Subject: [PATCH 07/37] moved steamcmd to own package --- src/cli/runtimecommands.go | 4 ++-- src/discordbot/controlpanel.go | 4 ++-- src/discordbot/handleSlashcommands.go | 4 ++-- src/setup/install.go | 5 +++-- src/setup/sscm.go | 11 ++++++----- src/{setup => steamcmd}/steamcmd-getappinfo.go | 4 ++-- src/{setup => steamcmd}/steamcmd-helper.go | 4 ++-- src/{setup => steamcmd}/steamcmd.go | 11 ++++++----- src/web/http.go | 4 ++-- 9 files changed, 27 insertions(+), 24 deletions(-) rename src/{setup => steamcmd}/steamcmd-getappinfo.go (98%) rename src/{setup => steamcmd}/steamcmd-helper.go (99%) rename src/{setup => steamcmd}/steamcmd.go (98%) diff --git a/src/cli/runtimecommands.go b/src/cli/runtimecommands.go index 39c3a91b..bc42c631 100644 --- a/src/cli/runtimecommands.go +++ b/src/cli/runtimecommands.go @@ -23,7 +23,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" ) // ANSI escape codes for green text and reset @@ -197,7 +197,7 @@ func runSteamCMD() { time.Sleep(10000 * time.Millisecond) } logger.Core.Info("Running SteamCMD") - setup.InstallAndRunSteamCMD() + steamcmd.InstallAndRunSteamCMD() } func testLocalization() { diff --git a/src/discordbot/controlpanel.go b/src/discordbot/controlpanel.go index 140fa555..323e472d 100644 --- a/src/discordbot/controlpanel.go +++ b/src/discordbot/controlpanel.go @@ -7,7 +7,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" "github.com/bwmarrin/discordgo" ) @@ -113,7 +113,7 @@ func handleControlReactions(s *discordgo.Session, r *discordgo.MessageReactionAd // Wait for delay to complete <-delayChan - _, err := setup.InstallAndRunSteamCMD() + _, err := steamcmd.InstallAndRunSteamCMD() Value := map[bool]string{true: "🟢 Success", false: "🔴 Failed"}[err == nil] SendMessageToStatusChannel(fmt.Sprintf("SteamCMD Update status: %s", Value)) diff --git a/src/discordbot/handleSlashcommands.go b/src/discordbot/handleSlashcommands.go index a599d0c6..b6637259 100644 --- a/src/discordbot/handleSlashcommands.go +++ b/src/discordbot/handleSlashcommands.go @@ -12,7 +12,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/backupmgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/commandmgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" "github.com/bwmarrin/discordgo" ) @@ -121,7 +121,7 @@ func handleUpdate(s *discordgo.Session, i *discordgo.InteractionCreate, data Emb time.Sleep(10 * time.Second) // Wait for server to stop } - _, err = setup.InstallAndRunSteamCMD() + _, err = steamcmd.InstallAndRunSteamCMD() data.Fields = []EmbedField{ {Name: "Update Status:", Value: map[bool]string{true: "🟢 Success", false: "🔴 Failed"}[err == nil], Inline: true}, diff --git a/src/setup/install.go b/src/setup/install.go index e4420ecf..b3618f35 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -17,6 +17,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup/update" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" ) var downloadBranch string // Holds the branch to download from @@ -42,9 +43,9 @@ func Install(wg *sync.WaitGroup) { // Step 3: Install and run SteamCMD logger.Install.Info("🔄Installing and running SteamCMD...") if config.GetBranch() != "indev-no-steamcmd" { - InstallAndRunSteamCMD() + steamcmd.InstallAndRunSteamCMD() } - initAppInfoPoller() // init the steamcmd app info poll check to check for new gameserver updates + steamcmd.InitAppInfoPoller() // init the steamcmd app info poll check to check for new gameserver updates logger.Install.Info("✅Setup complete!") } diff --git a/src/setup/sscm.go b/src/setup/sscm.go index fde685f4..a41cb8f3 100644 --- a/src/setup/sscm.go +++ b/src/setup/sscm.go @@ -8,6 +8,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" ) // BepInEx version: 5.4.23.2 or v5-lts @@ -136,7 +137,7 @@ func downloadAndInstallBepInEx(url string) error { // Extract the zip file to the current directory logger.Install.Info("📦Extracting BepInEx to current directory") - err = unzip(zipFile, fileInfo.Size(), ".") + err = steamcmd.Unzip(zipFile, fileInfo.Size(), ".") if err != nil { return fmt.Errorf("failed to extract BepInEx: %w", err) } @@ -149,10 +150,10 @@ func downloadAndInstallBepInEx(url string) error { } } - if runtime.GOOS == "linux" { - // make sure run_bepinex.sh is executable - if err := os.Chmod("./run_bepinex.sh", os.ModePerm); err != nil { - logger.Install.Warn(fmt.Sprintf("⚠️Failed to make run_bepinex.sh executable: %v", err)) + if runtime.GOOS != "linux" { + err = os.Remove("./run_bepinex.sh") + if err != nil { + logger.Install.Warn(fmt.Sprintf("⚠️Failed to remove obsoleterun_bepinex.sh: %v", err)) } } diff --git a/src/setup/steamcmd-getappinfo.go b/src/steamcmd/steamcmd-getappinfo.go similarity index 98% rename from src/setup/steamcmd-getappinfo.go rename to src/steamcmd/steamcmd-getappinfo.go index c4601b83..5dcfdd2e 100644 --- a/src/setup/steamcmd-getappinfo.go +++ b/src/steamcmd/steamcmd-getappinfo.go @@ -1,4 +1,4 @@ -package setup +package steamcmd import ( "bytes" @@ -22,7 +22,7 @@ var ( branchesLock sync.RWMutex // Protects branches map for concurrent access ) -func initAppInfoPoller() { +func InitAppInfoPoller() { go func() { for { err := getAppInfo() diff --git a/src/setup/steamcmd-helper.go b/src/steamcmd/steamcmd-helper.go similarity index 99% rename from src/setup/steamcmd-helper.go rename to src/steamcmd/steamcmd-helper.go index 84cd1577..23e2b977 100644 --- a/src/setup/steamcmd-helper.go +++ b/src/steamcmd/steamcmd-helper.go @@ -1,4 +1,4 @@ -package setup +package steamcmd import ( "archive/tar" @@ -181,7 +181,7 @@ func untar(dest string, r io.Reader) error { } // unzip extracts a zip archive. -func unzip(zipReader io.ReaderAt, size int64, dest string) error { +func Unzip(zipReader io.ReaderAt, size int64, dest string) error { reader, err := zip.NewReader(zipReader, size) if err != nil { return fmt.Errorf("failed to create zip reader: %w", err) diff --git a/src/setup/steamcmd.go b/src/steamcmd/steamcmd.go similarity index 98% rename from src/setup/steamcmd.go rename to src/steamcmd/steamcmd.go index 04109a3f..f0801bdd 100644 --- a/src/setup/steamcmd.go +++ b/src/steamcmd/steamcmd.go @@ -1,4 +1,4 @@ -package setup +package steamcmd import ( "fmt" @@ -30,11 +30,12 @@ const ( // It returns the exit status of the SteamCMD execution and any error encountered. func InstallAndRunSteamCMD() (int, error) { - if runtime.GOOS == "windows" { + switch runtime.GOOS { + case "windows": return installSteamCMDWindows() - } else if runtime.GOOS == "linux" { + case "linux": return installSteamCMDLinux() - } else { + default: err := fmt.Errorf("SteamCMD installation is not supported on this OS") logger.Install.Error("❌ " + err.Error() + "\n") return -1, err @@ -103,7 +104,7 @@ func installSteamCMDLinux() (int, error) { // installSteamCMDWindows downloads and installs SteamCMD on Windows. func installSteamCMDWindows() (int, error) { - return installSteamCMD("Windows", SteamCMDWindowsDir, SteamCMDWindowsURL, unzip) + return installSteamCMD("Windows", SteamCMDWindowsDir, SteamCMDWindowsURL, Unzip) } // runSteamCMD runs the SteamCMD command to update the game and returns its exit status and any error. diff --git a/src/web/http.go b/src/web/http.go index 1eb4a324..f5c119a7 100644 --- a/src/web/http.go +++ b/src/web/http.go @@ -15,7 +15,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/commandmgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/detectionmgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" ) // StartServer HTTP handler @@ -143,7 +143,7 @@ func HandleRunSteamCMD(w http.ResponseWriter, r *http.Request) { time.Sleep(10000 * time.Millisecond) } logger.Core.Info("Running SteamCMD") - _, err := setup.InstallAndRunSteamCMD() + _, err := steamcmd.InstallAndRunSteamCMD() // Update last execution time lastSteamCMDExecution = time.Now() From 99ed8da57c6ed22faa54d398df45f536692336ac Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Thu, 11 Sep 2025 21:51:00 +0200 Subject: [PATCH 08/37] refactored multiple sleep statements waiting for server to top before running steamcmd into steamcmd function --- src/cli/runtimecommands.go | 6 --- src/discordbot/handleSlashcommands.go | 5 -- src/setup/install.go | 4 +- src/steamcmd/install.go | 72 +++++++++++++++++++++++++ src/steamcmd/steamcmd.go | 75 ++++----------------------- src/web/http.go | 5 -- 6 files changed, 84 insertions(+), 83 deletions(-) create mode 100644 src/steamcmd/install.go diff --git a/src/cli/runtimecommands.go b/src/cli/runtimecommands.go index bc42c631..9eeae196 100644 --- a/src/cli/runtimecommands.go +++ b/src/cli/runtimecommands.go @@ -191,12 +191,6 @@ func deleteConfig() { } func runSteamCMD() { - if gamemgr.InternalIsServerRunning() { - logger.Core.Warn("Server is running, stopping server first...") - gamemgr.InternalStopServer() - time.Sleep(10000 * time.Millisecond) - } - logger.Core.Info("Running SteamCMD") steamcmd.InstallAndRunSteamCMD() } diff --git a/src/discordbot/handleSlashcommands.go b/src/discordbot/handleSlashcommands.go index b6637259..20d315aa 100644 --- a/src/discordbot/handleSlashcommands.go +++ b/src/discordbot/handleSlashcommands.go @@ -116,11 +116,6 @@ func handleUpdate(s *discordgo.Session, i *discordgo.InteractionCreate, data Emb data.Description = "Gameserver update completed." data.Color = 0x00FF00 // Green for completion (will adjust if error) - if gamemgr.InternalIsServerRunning() { - gamemgr.InternalStopServer() - time.Sleep(10 * time.Second) // Wait for server to stop - } - _, err = steamcmd.InstallAndRunSteamCMD() data.Fields = []EmbedField{ diff --git a/src/setup/install.go b/src/setup/install.go index b3618f35..250c2bd2 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -33,9 +33,9 @@ func Install(wg *sync.WaitGroup) { } // Step 1: Check and download the UIMod folder contents - logger.Install.Info("🔄Checking UIMod folder...") + logger.Install.Debug("🔄Checking UIMod folder...") CheckAndDownloadUIMod() - logger.Install.Info("✅UIMod folder setup complete.") + logger.Install.Debug("✅UIMod folder setup complete.") // Step 2: Check for Blacklist.txt and create it if it doesn't exist logger.Install.Info("🔄Checking for Blacklist.txt...") checkAndCreateBlacklist() diff --git a/src/steamcmd/install.go b/src/steamcmd/install.go new file mode 100644 index 00000000..7be15268 --- /dev/null +++ b/src/steamcmd/install.go @@ -0,0 +1,72 @@ +package steamcmd + +import ( + "os" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" +) + +func installSteamCMD(platform string, steamCMDDir string, downloadURL string, extractFunc ExtractorFunc) (int, error) { + // Check if SteamCMD is already installed + if _, err := os.Stat(steamCMDDir); os.IsNotExist(err) { + logger.Install.Warn("⚠️ SteamCMD not found for " + platform + ", downloading...\n") + + // Create SteamCMD directory + if err := createSteamCMDDirectory(steamCMDDir); err != nil { + logger.Install.Error("❌ Error creating SteamCMD directory: " + err.Error() + "\n") + return -1, err + } + + // Ensure cleanup on failure + success := false + defer func() { + if !success { + logger.Install.Warn("⚠️ Cleaning up due to failure...\n") + os.RemoveAll(steamCMDDir) + } + }() + + // Install required libraries + if err := installRequiredLibraries(); err != nil { + logger.Install.Error("❌ Error installing required libraries: " + err.Error() + "\n") + return -1, err + } + + // Download and extract SteamCMD + if err := downloadAndExtractSteamCMD(downloadURL, steamCMDDir, extractFunc); err != nil { + logger.Install.Error("❌ " + err.Error() + "\n") + return -1, err + } + + // Set executable permissions for SteamCMD files + if err := setExecutablePermissions(steamCMDDir); err != nil { + logger.Install.Error("❌ Error setting executable permissions: " + err.Error() + "\n") + return -1, err + } + + // Verify the steamcmd binary + if err := verifySteamCMDBinary(steamCMDDir); err != nil { + logger.Install.Error("❌ " + err.Error() + "\n") + return -1, err + } + + // Mark installation as successful + success = true + logger.Install.Info("✅ SteamCMD installed successfully.\n") + } else { + logger.Install.Info("✅ SteamCMD is already installed.") + } + + // Run SteamCMD and return its exit status and error + return runSteamCMD(steamCMDDir) +} + +// installSteamCMDLinux downloads and installs SteamCMD on Linux. +func installSteamCMDLinux() (int, error) { + return installSteamCMD("Linux", SteamCMDLinuxDir, SteamCMDLinuxURL, untarWrapper) +} + +// installSteamCMDWindows downloads and installs SteamCMD on Windows. +func installSteamCMDWindows() (int, error) { + return installSteamCMD("Windows", SteamCMDWindowsDir, SteamCMDWindowsURL, Unzip) +} diff --git a/src/steamcmd/steamcmd.go b/src/steamcmd/steamcmd.go index f0801bdd..e6dbb0fc 100644 --- a/src/steamcmd/steamcmd.go +++ b/src/steamcmd/steamcmd.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" ) @@ -30,6 +31,15 @@ const ( // It returns the exit status of the SteamCMD execution and any error encountered. func InstallAndRunSteamCMD() (int, error) { + if gamemgr.InternalIsServerRunning() { + logger.Core.Warn("Server is running, stopping server first...") + err := gamemgr.InternalStopServer() + if err != nil { + logger.Core.Error("Error stopping server before running Steamcmd: " + err.Error()) + } + } + logger.Core.Info("Running SteamCMD") + switch runtime.GOOS { case "windows": return installSteamCMDWindows() @@ -42,71 +52,6 @@ func InstallAndRunSteamCMD() (int, error) { } } -func installSteamCMD(platform string, steamCMDDir string, downloadURL string, extractFunc ExtractorFunc) (int, error) { - // Check if SteamCMD is already installed - if _, err := os.Stat(steamCMDDir); os.IsNotExist(err) { - logger.Install.Warn("⚠️ SteamCMD not found for " + platform + ", downloading...\n") - - // Create SteamCMD directory - if err := createSteamCMDDirectory(steamCMDDir); err != nil { - logger.Install.Error("❌ Error creating SteamCMD directory: " + err.Error() + "\n") - return -1, err - } - - // Ensure cleanup on failure - success := false - defer func() { - if !success { - logger.Install.Warn("⚠️ Cleaning up due to failure...\n") - os.RemoveAll(steamCMDDir) - } - }() - - // Install required libraries - if err := installRequiredLibraries(); err != nil { - logger.Install.Error("❌ Error installing required libraries: " + err.Error() + "\n") - return -1, err - } - - // Download and extract SteamCMD - if err := downloadAndExtractSteamCMD(downloadURL, steamCMDDir, extractFunc); err != nil { - logger.Install.Error("❌ " + err.Error() + "\n") - return -1, err - } - - // Set executable permissions for SteamCMD files - if err := setExecutablePermissions(steamCMDDir); err != nil { - logger.Install.Error("❌ Error setting executable permissions: " + err.Error() + "\n") - return -1, err - } - - // Verify the steamcmd binary - if err := verifySteamCMDBinary(steamCMDDir); err != nil { - logger.Install.Error("❌ " + err.Error() + "\n") - return -1, err - } - - // Mark installation as successful - success = true - logger.Install.Info("✅ SteamCMD installed successfully.\n") - } else { - logger.Install.Info("✅ SteamCMD is already installed.") - } - - // Run SteamCMD and return its exit status and error - return runSteamCMD(steamCMDDir) -} - -// installSteamCMDLinux downloads and installs SteamCMD on Linux. -func installSteamCMDLinux() (int, error) { - return installSteamCMD("Linux", SteamCMDLinuxDir, SteamCMDLinuxURL, untarWrapper) -} - -// installSteamCMDWindows downloads and installs SteamCMD on Windows. -func installSteamCMDWindows() (int, error) { - return installSteamCMD("Windows", SteamCMDWindowsDir, SteamCMDWindowsURL, Unzip) -} - // runSteamCMD runs the SteamCMD command to update the game and returns its exit status and any error. func runSteamCMD(steamCMDDir string) (int, error) { currentDir, err := os.Getwd() diff --git a/src/web/http.go b/src/web/http.go index f5c119a7..5f99f9fe 100644 --- a/src/web/http.go +++ b/src/web/http.go @@ -137,11 +137,6 @@ func HandleRunSteamCMD(w http.ResponseWriter, r *http.Request) { return } - if gamemgr.InternalIsServerRunning() { - logger.Core.Warn("Server is running, stopping server first...") - gamemgr.InternalStopServer() - time.Sleep(10000 * time.Millisecond) - } logger.Core.Info("Running SteamCMD") _, err := steamcmd.InstallAndRunSteamCMD() From 233a9bb546d1466a6e41b200822ce5da58ef0fcc Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Thu, 11 Sep 2025 21:55:52 +0200 Subject: [PATCH 09/37] renamed steamcmd app info file --- src/steamcmd/{steamcmd-getappinfo.go => getappinfo.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/steamcmd/{steamcmd-getappinfo.go => getappinfo.go} (100%) diff --git a/src/steamcmd/steamcmd-getappinfo.go b/src/steamcmd/getappinfo.go similarity index 100% rename from src/steamcmd/steamcmd-getappinfo.go rename to src/steamcmd/getappinfo.go From 3d4b3c4929cfcc0883b2bca06a751b3937a44f61 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Thu, 11 Sep 2025 21:56:06 +0200 Subject: [PATCH 10/37] added runtime command to get buildID for debugging --- src/cli/runtimecommands.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/cli/runtimecommands.go b/src/cli/runtimecommands.go index 9eeae196..0ceae14c 100644 --- a/src/cli/runtimecommands.go +++ b/src/cli/runtimecommands.go @@ -160,6 +160,7 @@ func init() { RegisterCommand("testlocalization", WrapNoReturn(testLocalization), "tl") RegisterCommand("supportmode", WrapNoReturn(supportMode), "sm") RegisterCommand("supportpackage", WrapNoReturn(supportPackage), "sp") + RegisterCommand("getbuildid", WrapNoReturn(getBuildID), "gbid") } func startServer() { @@ -194,6 +195,15 @@ func runSteamCMD() { steamcmd.InstallAndRunSteamCMD() } +func getBuildID() { + buildID := config.GetCurrentBranchBuildID() + if buildID == "" { + logger.Core.Error("Build ID not found, empty string returned") + return + } + logger.Core.Info("Build ID: " + buildID) +} + func testLocalization() { currentLanguageSetting := config.GetLanguageSetting() s := localization.GetString("UIText_StartButton") From 1697645fc4c95550198b2facf241b7f0cb508877 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:03:38 +0200 Subject: [PATCH 11/37] added restart warnings when auto updating --- src/steamcmd/getappinfo.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/steamcmd/getappinfo.go b/src/steamcmd/getappinfo.go index 5dcfdd2e..426017c2 100644 --- a/src/steamcmd/getappinfo.go +++ b/src/steamcmd/getappinfo.go @@ -14,6 +14,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/commandmgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" ) @@ -90,6 +91,21 @@ func getAppInfo() error { if config.GetAllowAutoGameServerUpdates() { logger.Install.Info("🔍 Updating gameserver via SteamCMD...") if gamemgr.InternalIsServerRunning() { + commandmgr.WriteCommand("say Update found, stopping server in 60 seconds...") + logger.Install.Info("❗Stopping server in 60 seconds...") + time.Sleep(10 * time.Second) + commandmgr.WriteCommand("say Update found, stopping server in 50 seconds...") + time.Sleep(10 * time.Second) + commandmgr.WriteCommand("say Update found, stopping server in 40 seconds...") + time.Sleep(10 * time.Second) + commandmgr.WriteCommand("say Update found, stopping server in 30 seconds...") + time.Sleep(3 * time.Second) + commandmgr.WriteCommand("SAVE") + time.Sleep(7 * time.Second) + commandmgr.WriteCommand("say Update found, stopping server in 20 seconds. World was Saved. ") + time.Sleep(10 * time.Second) + commandmgr.WriteCommand("say Update found, stopping server in 10 seconds...") + time.Sleep(10 * time.Second) gamemgr.InternalStopServer() } _, err := InstallAndRunSteamCMD() From adf980dc70f0e96f377131cc92675f87873af437 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:04:09 +0200 Subject: [PATCH 12/37] added localization strings for auto gameserver updater --- UIMod/onboard_bundled/localization/de-DE.json | 5 ++++- UIMod/onboard_bundled/localization/en-US.json | 2 +- UIMod/onboard_bundled/localization/sv-SE.json | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/UIMod/onboard_bundled/localization/de-DE.json index 311c62dd..d0934970 100644 --- a/UIMod/onboard_bundled/localization/de-DE.json +++ b/UIMod/onboard_bundled/localization/de-DE.json @@ -89,7 +89,10 @@ "UIText_StartLocation": "Startort", "UIText_StartLocationInfo": "Startort für Welterstellung. Standard DefaultStartLocation wenn leer.", "UIText_AutoStartServerOnStartup": "Server Auto-Start beim Hochfahren", - "UIText_AutoStartServerOnStartupInfo": "Gameserver automatisch starten wenn SSUI gestartet wird. Standard false." + "UIText_AutoStartServerOnStartupInfo": "Gameserver automatisch starten wenn SSUI gestartet wird. Standard false.", + "UIText_AllowAutoGameServerUpdates": "Automatische Spielserver-Updates aktivieren", + "UIText_AllowAutoGameServerUpdatesInfo": "Erlaubt dem Spielserver, automatisch nach der neuesten Version zu suchen und diese zu installieren. Achtung: Der Server wird neu gestartet, wenn eine neue Version gefunden und installiert wurde. 60 bis 10 Sekunden vor dem Neustart werden mehrere Warnmeldungen mit SAY-Befehlen an den Server gesendet." + }, "discord": { "UIText_DiscordIntegrationTitle": "Discord Integration Vorteile", diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index 4780ddcc..ee234e71 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -77,7 +77,7 @@ "UIText_GameBranch": "Game Branch", "UIText_GameBranchInfo": "Branch of the game to use. When changed, requires to restart SSUI!", "UIText_AllowAutoGameServerUpdates": "Enable Auto Game Server Updates", - "UIText_AllowAutoGameServerUpdatesInfo": "Allow the gameserver to automatically query for and update to the latest version. Attention: Restarts the server when a new version was found and installed." + "UIText_AllowAutoGameServerUpdatesInfo": "Allow the gameserver to automatically query for and update to the latest version. Attention: Restarts the server when a new version was found and installed. Will send multiple warning messages to the sever with SAY commands 60-10 seconds before the restart." }, "beta": { "UIText_BetaOnlySettings": "BETA ONLY: NEW TERRAIN AND SAVE SYSTEM SETTINGS", diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/UIMod/onboard_bundled/localization/sv-SE.json index 8ec17c98..3ec2eb47 100644 --- a/UIMod/onboard_bundled/localization/sv-SE.json +++ b/UIMod/onboard_bundled/localization/sv-SE.json @@ -75,7 +75,9 @@ "UIText_AutoRestartServerTimer": "Schemalagd spelserveromstart", "UIText_AutoRestartServerTimerInfo": "Tidsram i minuter för att schemalägga en automatisk spelserveromstart. 0 = inaktiverad, 1440 = 24 timmar, osv. Om SSCM är aktiverat visas meddelanden som \"Varning, servern startar om om 30/20/10/5 sekunder!\" i spelet före omstart.", "UIText_GameBranch": "Spelgren", - "UIText_GameBranchInfo": "Spelgren att använda. Vid ändring krävs omstart av SSUI!" + "UIText_GameBranchInfo": "Spelgren att använda. Vid ändring krävs omstart av SSUI!", + "UIText_AllowAutoGameServerUpdates": "Aktivera automatiska uppdateringar av spelservern", + "UIText_AllowAutoGameServerUpdatesInfo": "Tillåt spelservern att automatiskt söka efter och uppdatera till den senaste versionen. Obs! Servern startas om när en ny version har hittats och installerats. Flera varningsmeddelanden skickas till servern med SAY-kommandon 60–10 sekunder före omstarten." }, "beta": { "UIText_BetaOnlySettings": "ENDAST BETA: INSTÄLLNINGAR FÖR NYTT TERRÄNG- OCH SPARSYSTEM", From 4be06c0c12a7d7080ecef0498aeddba56caf6609 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:08:38 +0200 Subject: [PATCH 13/37] added info text to config file --- src/config/config.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/config/config.go b/src/config/config.go index 59fc5605..ba76ea8e 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -15,6 +15,13 @@ var ( Branch = "indev-no-steamcmd" ) +/* +If you read this, you are likely a developer. I sincerly apologize for the way the config works. +While I would love to refactor the config to not write to file then read the file every time a config value is changed, +I have not found the time to do so. So, for now, we save to file, then read the file and rely on whatever the file says. Although this is not ideal, it works for now. Deal with it. +JacksonTheMaster +*/ + type JsonConfig struct { // reordered in 5.6.4 to simplify the order of the config file. From 2ce70cd3a44b04c24c60653eae86f58047c77c10 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:15:19 +0200 Subject: [PATCH 14/37] added discordgo-license.md and note in license as Discordgo requires this (whoops) --- src/discordbot/discordgo-license.md | 27 +++++++++++++++++++++++++++ sscm/LICENSE | 4 ++++ 2 files changed, 31 insertions(+) create mode 100644 src/discordbot/discordgo-license.md diff --git a/src/discordbot/discordgo-license.md b/src/discordbot/discordgo-license.md new file mode 100644 index 00000000..f24cf6f5 --- /dev/null +++ b/src/discordbot/discordgo-license.md @@ -0,0 +1,27 @@ +Copyright (c) 2015, Bruce Marriner +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of discordgo nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/sscm/LICENSE b/sscm/LICENSE index 46e41b50..48aadff9 100644 --- a/sscm/LICENSE +++ b/sscm/LICENSE @@ -76,6 +76,10 @@ Unless permitted by applicable law, the Licensee shall not: 10.3 Severability: If any provision is unenforceable, the remaining provisions remain in effect. 10.4 Language: This Agreement is in English. +## Open source licenses + +This project uses Discordgo, Copyright (c) 2015 Bruce Marriner, under the BSD 3-Clause License. The copyright notice, conditions, and disclaimer are reproduced in the discordbot package. + ## CONTACT For inquiries, contact the Licensor at github.com/JacksonTheMaster. From a9d10ec667e5fef9de3870b8520470085591723d Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 01:08:14 +0200 Subject: [PATCH 15/37] updated misleading UIText_StartLocalHostInfo localization key --- UIMod/onboard_bundled/localization/de-DE.json | 2 +- UIMod/onboard_bundled/localization/en-US.json | 2 +- UIMod/onboard_bundled/localization/sv-SE.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/UIMod/onboard_bundled/localization/de-DE.json index d0934970..f1f971e4 100644 --- a/UIMod/onboard_bundled/localization/de-DE.json +++ b/UIMod/onboard_bundled/localization/de-DE.json @@ -57,7 +57,7 @@ "UIText_LocalIpAddress": "Lokale IP Adresse", "UIText_LocalIpAddressInfo": "IP Adresse zum Binden", "UIText_StartLocalHost": "Lokalen Host Starten", - "UIText_StartLocalHostInfo": "Auf TRUE setzen um nur im lokalen Netzwerk zu hören", + "UIText_StartLocalHostInfo": "Auf TRUE setzen. Dies ist erforderlich für den Server um zu funktionieren.", "UIText_ServerVisible": "Server Sichtbar", "UIText_ServerVisibleInfo": "Auf TRUE setzen um Server öffentlich zu listen", "UIText_UseSteamP2P": "Steam P2P Nutzen", diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index ee234e71..d396ecdb 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -57,7 +57,7 @@ "UIText_LocalIpAddress": "Local IP Address", "UIText_LocalIpAddressInfo": "IP address to bind to", "UIText_StartLocalHost": "Start Local Host", - "UIText_StartLocalHostInfo": "Set to TRUE to listen only on local network", + "UIText_StartLocalHostInfo": "Keep this true. This is required for the server to work.", "UIText_ServerVisible": "Server Visible", "UIText_ServerVisibleInfo": "Set to TRUE to list server publicly", "UIText_UseSteamP2P": "Use Steam P2P", diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/UIMod/onboard_bundled/localization/sv-SE.json index 3ec2eb47..54b5074c 100644 --- a/UIMod/onboard_bundled/localization/sv-SE.json +++ b/UIMod/onboard_bundled/localization/sv-SE.json @@ -57,7 +57,7 @@ "UIText_LocalIpAddress": "Lokal IP-adress", "UIText_LocalIpAddressInfo": "IP-adress att binda till", "UIText_StartLocalHost": "Starta lokal värd", - "UIText_StartLocalHostInfo": "Sätt till TRUE för att endast lyssna på lokalt nätverk", + "UIText_StartLocalHostInfo": "Sätt till TRUE. Detta är nödvändigt för att servern ska fungera.", "UIText_ServerVisible": "Server synlig", "UIText_ServerVisibleInfo": "Sätt till TRUE för att visa servern offentligt", "UIText_UseSteamP2P": "Använd Steam P2P", From a639237755b71d0791dc9241b3ff2f46044c3027 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 01:09:46 +0200 Subject: [PATCH 16/37] updated config version and branch --- src/config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/config.go b/src/config/config.go index ba76ea8e..f1b304e7 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.6.3" - Branch = "indev-no-steamcmd" + Version = "5.6.4" + Branch = "release" ) /* From c3b551b3a14bfae1ac3512dde1919251aead9826 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 03:40:41 +0200 Subject: [PATCH 17/37] renamed devcontainer --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index dbf2dcea..0ed4e03b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "name": "StationeersServerUI (Go+Svelte)", + "name": "StationeersServerUI", "build": { "dockerfile": "Dockerfile", "args": { "TZ": "${localEnv:TZ:Europe/Stockholm}" } From bc1fd14b1c28b06f891937b11b1dae8eb61e5bf2 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:12:32 +0200 Subject: [PATCH 18/37] - Removed initialization of the SteamCMD app info poller in the install process. - Refactored InitAppInfoPoller to manage poller lifecycle more effectively. - Added Poller to loader instead --- src/setup/install.go | 1 - src/steamcmd/getappinfo.go | 48 +++++++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/setup/install.go b/src/setup/install.go index 250c2bd2..12a33e80 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -45,7 +45,6 @@ func Install(wg *sync.WaitGroup) { if config.GetBranch() != "indev-no-steamcmd" { steamcmd.InstallAndRunSteamCMD() } - steamcmd.InitAppInfoPoller() // init the steamcmd app info poll check to check for new gameserver updates logger.Install.Info("✅Setup complete!") } diff --git a/src/steamcmd/getappinfo.go b/src/steamcmd/getappinfo.go index 426017c2..bde13ecc 100644 --- a/src/steamcmd/getappinfo.go +++ b/src/steamcmd/getappinfo.go @@ -8,7 +8,6 @@ import ( "path/filepath" "regexp" "runtime" - "strings" "sync" "time" @@ -20,17 +19,44 @@ import ( var ( branches = make(map[string]string) - branchesLock sync.RWMutex // Protects branches map for concurrent access + branchesLock sync.RWMutex // Protects branches map for concurrent access + stopPoller = make(chan struct{}) // Channel to signal poller cancellation ) -func InitAppInfoPoller() { +// InitAppInfoPoller starts a goroutine that periodically fetches app info and stops any previous poller. +func AppInfoPoller() { + // Signal previous poller to stop + select { + case stopPoller <- struct{}{}: + // Previous poller was signaled to stop + default: + // No previous poller running + } + + // if AutoGameServerUpdates is disabled, dont start the poller. + if !config.GetAllowAutoGameServerUpdates() { + return + } + // Start new poller go func() { for { - err := getAppInfo() - if err != nil { - logger.Install.Error("❌ Failed to get app info: " + err.Error() + "\n") + select { + case <-stopPoller: + logger.Install.Debug("🛑 Previous app info poller stopped") + return + default: + err := getAppInfo() + if err != nil { + logger.Install.Warn("❌ Failed to get Update info: " + err.Error()) + } + select { + case <-stopPoller: + logger.Install.Debug("🛑 App info poller stopped") + return + case <-time.After(5 * time.Minute): + // Continue to next iteration + } } - time.Sleep(5 * time.Minute) } }() } @@ -56,10 +82,10 @@ func getAppInfo() error { cmd.Stderr = &stderr // Log the command - if config.GetLogLevel() == 10 { - cmdString := strings.Join(cmd.Args, " ") - logger.Install.Debug("🕑 Running SteamCMD for app info: " + cmdString) - } + //if config.GetLogLevel() == 10 { + // cmdString := strings.Join(cmd.Args, " ") + // logger.Install.Debug("🕑 Running SteamCMD for app info: " + cmdString) + //} // Run the command err := cmd.Run() From bcef1c21586974c29e054d75d6642616cc71e946 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:12:52 +0200 Subject: [PATCH 19/37] Add printconfig command and enhance PrintConfigDetails logging --- src/cli/runtimecommands.go | 5 +++++ src/core/loader/helpers.go | 22 +++++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/cli/runtimecommands.go b/src/cli/runtimecommands.go index 0ceae14c..f495a287 100644 --- a/src/cli/runtimecommands.go +++ b/src/cli/runtimecommands.go @@ -161,6 +161,7 @@ func init() { RegisterCommand("supportmode", WrapNoReturn(supportMode), "sm") RegisterCommand("supportpackage", WrapNoReturn(supportPackage), "sp") RegisterCommand("getbuildid", WrapNoReturn(getBuildID), "gbid") + RegisterCommand("printconfig", WrapNoReturn(printConfig), "pc") } func startServer() { @@ -195,6 +196,10 @@ func runSteamCMD() { steamcmd.InstallAndRunSteamCMD() } +func printConfig() { + loader.PrintConfigDetails("Info") +} + func getBuildID() { buildID := config.GetCurrentBranchBuildID() if buildID == "" { diff --git a/src/core/loader/helpers.go b/src/core/loader/helpers.go index 7a02792a..92d6f897 100644 --- a/src/core/loader/helpers.go +++ b/src/core/loader/helpers.go @@ -8,15 +8,25 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" ) -func PrintConfigDetails() { +func PrintConfigDetails(logLevel ...string) { logger.Config.Debug("=== Game Server Configuration Details ===") // Helper function to print sections printSection := func(title string, fields map[string]string) { - logger.Config.Debug(fmt.Sprintf("\n%s", title)) - logger.Config.Debug(strings.Repeat("-", len(title))) - for key, value := range fields { - logger.Config.Debug(fmt.Sprintf("%-30s: %s", key, value)) + + if logLevel == nil { + logger.Config.Debug(fmt.Sprintf("\n%s", title)) + logger.Config.Debug(strings.Repeat("-", len(title))) + for key, value := range fields { + logger.Config.Debug(fmt.Sprintf("%-30s: %s", key, value)) + } + } + if len(logLevel) > 0 && logLevel[0] == "Info" { + logger.Config.Info(fmt.Sprintf("\n%s", title)) + logger.Config.Info(strings.Repeat("-", len(title))) + for key, value := range fields { + logger.Config.Info(fmt.Sprintf("%-30s: %s", key, value)) + } } } @@ -112,6 +122,8 @@ func PrintConfigDetails() { "AllowPrereleaseUpdates": fmt.Sprintf("%v", config.GetAllowPrereleaseUpdates()), "AllowMajorUpdates": fmt.Sprintf("%v", config.GetAllowMajorUpdates()), "AutoRestartServerTimer": config.GetAutoRestartServerTimer(), + "AutoGameServerUpdates": fmt.Sprintf("%v", config.GetAllowAutoGameServerUpdates()), + "CurrentBranchBuildID": config.GetCurrentBranchBuildID(), } printSection("Updater Configuration", updater) From dc1a1092836f31e412b3126850f4efa514f919f7 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:37:57 +0200 Subject: [PATCH 20/37] added cmdargs (from SteamServerUI) --- server.go | 1 + src/core/loader/cmdargs.go | 95 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 src/core/loader/cmdargs.go diff --git a/server.go b/server.go index 90a4ecb0..7c5c6677 100644 --- a/server.go +++ b/server.go @@ -39,6 +39,7 @@ func main() { logger.ConfigureConsole() logger.Install.Info("Starting setup...") loader.ReloadConfig() // Load the config file before starting the setup process + loader.LoadCmdArgs() setup.Install(&wg) wg.Wait() logger.Main.Debug("Initializing resources...") diff --git a/src/core/loader/cmdargs.go b/src/core/loader/cmdargs.go new file mode 100644 index 00000000..69784e7e --- /dev/null +++ b/src/core/loader/cmdargs.go @@ -0,0 +1,95 @@ +package loader + +import ( + "flag" + "fmt" + "strings" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/security" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" +) + +// LoadCmdArgs parses command-line arguments ONCE at startup (called from func main) and applies them using the config setters. +// Because this is using the config rather than adding features to it, it is a part of the loader package. +func LoadCmdArgs() { + // Define flags matching the config variable names + var backendEndpointPort string + var gameBranch string + var logLevel int + var isDebugMode bool + var createSSUILogFile bool + var recoveryPassword string + var devMode bool + + flag.StringVar(&backendEndpointPort, "BackendEndpointPort", "", "Override the backend endpoint port (e.g., 8080)") + flag.StringVar(&backendEndpointPort, "p", "", "(Alias) Override the backend endpoint port (e.g., 8080)") + flag.StringVar(&gameBranch, "GameBranch", "", "Override the game branch (e.g., beta)") + flag.StringVar(&gameBranch, "b", "", "(Alias) Override the game branch (e.g., beta)") + flag.StringVar(&recoveryPassword, "RecoveryPassword", "", "Enable recovery user and OVERWRITES all existing users (expects password as argument)") + flag.StringVar(&recoveryPassword, "r", "", "(Alias) Enable recovery user and OVERWRITES all existing users (expects password as argument)") + flag.BoolVar(&devMode, "dev", false, "Enable dev mode: Auth, OVERWRITES all existing users with admin:admin->superadmin, and enables cli-console. For development only.") + flag.IntVar(&logLevel, "LogLevel", 0, "Override the log level (e.g., 10)") + flag.IntVar(&logLevel, "ll", 0, "(Alias) Override the log level (e.g., 10)") + flag.BoolVar(&isDebugMode, "IsDebugMode", false, "Enable debug mode") + flag.BoolVar(&isDebugMode, "debug", false, "(Alias) Enable debug mode") + flag.BoolVar(&createSSUILogFile, "CreateSSUILogFile", false, "Create a log file for SSUI") + flag.BoolVar(&createSSUILogFile, "lf", false, "(Alias) Create a log file for SSUI") + + // Parse command-line flags + flag.Parse() + + if devMode { + config.SetAuthEnabled(true) + config.SetIsFirstTimeSetup(false) + config.SetUsers(map[string]string{"admin": "$2a$10$7QQhPkNAfT.MXhJhnnodXOyn3KKE/1eu7nYb0y2O1UBoAWc0Y/fda"}) // admin:admin + config.SetIsConsoleEnabled(true) + logger.Main.Info("Dev mode enabled: Auth enabled, admin user set to admin:admin:superadmin, console enabled") + } + + if backendEndpointPort != "" && backendEndpointPort != "8443" { + oldPort := config.GetSSUIWebPort() + config.SetSSUIWebPort(backendEndpointPort) + logger.Main.Info(fmt.Sprintf("Overriding SetSSUIWebPort from command line: Before=%s, Now=%s", oldPort, backendEndpointPort)) + } + + if gameBranch != "" { + oldBranch := config.GetGameBranch() + config.SetGameBranch(gameBranch) + logger.Main.Info(fmt.Sprintf("Overriding GameBranch from command line: Before=%s, Now=%s", oldBranch, gameBranch)) + } + + if recoveryPassword != "" { + recoveryPassword = strings.TrimSpace(recoveryPassword) + if recoveryPassword == "" { + logger.Main.Error("Recovery flag provided but password is empty. Skipping recovery user creation.") + } else { + hashedPassword, err := security.HashPassword(recoveryPassword) + if err != nil { + logger.Main.Error(fmt.Sprintf("Failed to hash recovery password: %v", err)) + return + } + config.SetUsers(map[string]string{"recovery": hashedPassword}) + logger.Main.Warn(fmt.Sprintf("Recovery user added with access level superadmin. Login with username 'recovery' and password '%s'", recoveryPassword)) + } + } + + if logLevel != 0 { + oldLevel := config.GetLogLevel() + config.SetLogLevel(logLevel) + logger.Main.Info(fmt.Sprintf("Overriding LogLevel from command line: Before=%d, Now=%d", oldLevel, logLevel)) + } + + if isDebugMode { + oldDebug := config.GetIsDebugMode() + config.SetIsDebugMode(true) + config.SetLogLevel(10) + logger.Main.Info(fmt.Sprintf("Overriding IsDebugMode from command line: Before=%t, Now=true", oldDebug)) + } + + if createSSUILogFile { + oldCreateSSUILogFile := config.GetCreateSSUILogFile() + config.SetCreateSSUILogFile(true) + logger.Main.Info(fmt.Sprintf("Overriding CreateSSUILogFile from command line: Before=%t, Now=true", oldCreateSSUILogFile)) + } +} From f565580a900c277ddab7f512335b358a22a2e45a Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:38:10 +0200 Subject: [PATCH 21/37] fixed flags showing on login page --- UIMod/onboard_bundled/twoboxform/twoboxform.html | 2 +- src/web/TwoBoxForm.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.html b/UIMod/onboard_bundled/twoboxform/twoboxform.html index abd09bc1..5fa32691 100644 --- a/UIMod/onboard_bundled/twoboxform/twoboxform.html +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.html @@ -20,7 +20,7 @@

Preparing...

Preparing {{.Title}}

- {{if ne .Step "welcome"}} + {{if and (eq .Mode "setup") (ne .Step "welcome")}}
English German diff --git a/src/web/TwoBoxForm.go b/src/web/TwoBoxForm.go index bbee909f..e509020f 100644 --- a/src/web/TwoBoxForm.go +++ b/src/web/TwoBoxForm.go @@ -476,6 +476,7 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { data.SecondaryLabelType = "password" data.SubmitButtonText = localization.GetString("UIText_Login_SubmitButton") data.Mode = "login" + data.Step = "" data.ShowExtraButtons = false } From 226ffd2463b066b57e6d00c21887a06f0ccd99ee Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:43:40 +0200 Subject: [PATCH 22/37] twoboxform recieved a makeover - removed discord settings entirely - updated localizations - added a progress bar --- UIMod/onboard_bundled/localization/de-DE.json | 90 +++-------- UIMod/onboard_bundled/localization/en-US.json | 95 ++--------- UIMod/onboard_bundled/localization/sv-SE.json | 90 +++-------- .../onboard_bundled/twoboxform/twoboxform.css | 67 ++++++++ .../twoboxform/twoboxform.html | 12 ++ .../onboard_bundled/twoboxform/twoboxform.js | 4 +- src/web/TwoBoxForm.go | 147 +++--------------- 7 files changed, 152 insertions(+), 353 deletions(-) diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/UIMod/onboard_bundled/localization/de-DE.json index f1f971e4..88b7e365 100644 --- a/UIMod/onboard_bundled/localization/de-DE.json +++ b/UIMod/onboard_bundled/localization/de-DE.json @@ -128,111 +128,56 @@ "UIText_FooterText": "Hilfe benötigt? Schaue ins Stationeers Server UI Github Wiki.", "UIText_SSCM_FooterText": "Nutze SSCM für das mächtigste Stationeers Server Management! Du kannst Befehle von der Web-Konsole ausführen ohne Vanilla-Verhalten zu stören!", "UIText_Welcome_Title": "Stationeers Server UI", + "UIText_Welcome_HeaderTitle": "Willkommen!", "UIText_Welcome_SubmitButton": "Setup Starten", "UIText_Welcome_SkipButton": "Setup Überspringen", "UIText_PlsRead_Title": "Bitte lesen!", - "UIText_PlsRead_HeaderTitle": "Wir empfehlen stark, die Texte in diesem Setup-Assistenten zu lesen!", - "UIText_PlsRead_StepMessage": "Die meisten gemeldeten Probleme entstehen durch Fehlkonfiguration.", + "UIText_PlsRead_HeaderTitle": "Hinweis", + "UIText_PlsRead_StepMessage": "Wir empfehlen stark, die Texte in diesem Setup-Assistenten zu lesen! Die meisten gemeldeten Probleme entstehen durch Fehlkonfiguration.", "UIText_PlsRead_SubmitButton": "Verstanden", "UIText_PlsRead_SkipButton": "Verstanden", "UIText_ServerName_Title": "Stationeers Server UI", - "UIText_ServerName_HeaderTitle": "Servername Setup", + "UIText_ServerName_HeaderTitle": "Servername", "UIText_ServerName_StepMessage": "Gib deinem Server einen Namen wie 'Weltraumstation 13'", "UIText_ServerName_PrimaryPlaceholder": "Mein Stationeers Server mit UI", "UIText_ServerName_PrimaryLabel": "Servername", "UIText_ServerName_SubmitButton": "Speichern & Weiter", "UIText_ServerName_SkipButton": "Überspringen", "UIText_SaveIdentifier_Title": "Stationeers Server UI", - "UIText_SaveIdentifier_HeaderTitle": "Speicher-Identifikator Setup", + "UIText_SaveIdentifier_HeaderTitle": "Speicher-Identifikator", "UIText_SaveIdentifier_StepMessage": "Setze einen Speicher-Identifikator wie 'Weltraumstation13 Vulcan'. Ersten Buchstaben jedes Wortes groß schreiben. Welttypen im Stationeers Wiki -> Dedicated Server", "UIText_SaveIdentifier_PrimaryPlaceholder": "Benötigt SaveName und WorldType für ersten Start!", "UIText_SaveIdentifier_PrimaryLabel": "Speicher-Identifikator", "UIText_SaveIdentifier_SubmitButton": "Speichern & Weiter", "UIText_SaveIdentifier_SkipButton": "Überspringen", "UIText_MaxPlayers_Title": "Stationeers Server UI", - "UIText_MaxPlayers_HeaderTitle": "Spielerlimit Setup", + "UIText_MaxPlayers_HeaderTitle": "Spielerlimit", "UIText_MaxPlayers_StepMessage": "Wähle die maximale Anzahl Spieler die sich verbinden können.", "UIText_MaxPlayers_PrimaryPlaceholder": "8", "UIText_MaxPlayers_PrimaryLabel": "Max Spieler", "UIText_MaxPlayers_SubmitButton": "Speichern & Weiter", "UIText_MaxPlayers_SkipButton": "Überspringen", "UIText_ServerPassword_Title": "Stationeers Server UI", - "UIText_ServerPassword_HeaderTitle": "Server Passwort Setup", + "UIText_ServerPassword_HeaderTitle": "Server Passwort", "UIText_ServerPassword_StepMessage": "Setze ein Gameserver Passwort oder überspringe diesen Schritt.", "UIText_ServerPassword_PrimaryPlaceholder": "Server Passwort", "UIText_ServerPassword_PrimaryLabel": "Server Passwort", "UIText_ServerPassword_SubmitButton": "Speichern & Weiter", "UIText_ServerPassword_SkipButton": "Überspringen", "UIText_GameBranch_Title": "Stationeers Server UI", - "UIText_GameBranch_HeaderTitle": "Spiel Branch Setup", + "UIText_GameBranch_HeaderTitle": "Spiel Branch", "UIText_GameBranch_StepMessage": "Gib einen Beta-Branch ein oder überspringe für Release-Version. Bei Branch-Wechsel SSUI nach Assistenten n e u s t a r t e n.", "UIText_GameBranch_PrimaryPlaceholder": "beta", "UIText_GameBranch_PrimaryLabel": "Spiel Branch", "UIText_GameBranch_SubmitButton": "Speichern & Weiter", "UIText_GameBranch_SkipButton": "Release Version nutzen", - "UIText_NewTerrainAndSaveSystem_Title": "TERRAINSYSTEM WÄHLEN", - "UIText_NewTerrainAndSaveSystem_HeaderTitle": "Sehr wichtiger Schritt!", + "UIText_NewTerrainAndSaveSystem_Title": "Wichtig", + "UIText_NewTerrainAndSaveSystem_HeaderTitle": "Terrainsystem wählen", "UIText_NewTerrainAndSaveSystem_StepMessage": "Gerade zu Beta gewechselt? Terrain- und Speichersystem umschalten! 'ja' eingeben zum Aktivieren oder 'nein' zum Deaktivieren.", "UIText_NewTerrainAndSaveSystem_PrimaryPlaceholder": "ja/nein", "UIText_NewTerrainAndSaveSystem_PrimaryLabel": "Neues System aktivieren", "UIText_NewTerrainAndSaveSystem_SubmitButton": "Speichern & Weiter", "UIText_NewTerrainAndSaveSystem_SkipButton": "Überspringen", - "UIText_DiscordEnabled_Title": "Stationeers Server UI", - "UIText_DiscordEnabled_HeaderTitle": "Discord Integration", - "UIText_DiscordEnabled_StepMessage": "Discord Integration aktivieren? 'ja' eingeben zum Aktivieren oder Überspringen zum Deaktivieren.", - "UIText_DiscordEnabled_PrimaryPlaceholder": "ja", - "UIText_DiscordEnabled_PrimaryLabel": "Discord Aktivieren", - "UIText_DiscordEnabled_SubmitButton": "Speichern & Weiter", - "UIText_DiscordEnabled_SkipButton": "Überspringen (Discord Deaktivieren)", - "UIText_DiscordToken_Title": "Stationeers Server UI", - "UIText_DiscordToken_HeaderTitle": "Discord Bot Token", - "UIText_DiscordToken_StepMessage": "Gib deinen Discord Bot Token für Server Integration ein", - "UIText_DiscordToken_PrimaryPlaceholder": "Discord Bot Token", - "UIText_DiscordToken_PrimaryLabel": "Discord Token", - "UIText_DiscordToken_SubmitButton": "Speichern & Weiter", - "UIText_DiscordToken_SkipButton": "Überspringen", - "UIText_ControlPanelChannel_Title": "Stationeers Server UI", - "UIText_ControlPanelChannel_HeaderTitle": "Discord Channel Setup (1/6)", - "UIText_ControlPanelChannel_StepMessage": "Discord Kontrollpanel Channel ID eingeben", - "UIText_ControlPanelChannel_PrimaryPlaceholder": "Channel ID", - "UIText_ControlPanelChannel_PrimaryLabel": "Kontrollpanel Channel ID", - "UIText_ControlPanelChannel_SubmitButton": "Speichern & Weiter", - "UIText_ControlPanelChannel_SkipButton": "Überspringen", - "UIText_SaveChannel_Title": "Stationeers Server UI", - "UIText_SaveChannel_HeaderTitle": "Discord Channel Setup (2/6)", - "UIText_SaveChannel_StepMessage": "Discord Speicher Channel ID eingeben", - "UIText_SaveChannel_PrimaryPlaceholder": "Channel ID", - "UIText_SaveChannel_PrimaryLabel": "Speicher Channel ID", - "UIText_SaveChannel_SubmitButton": "Speichern & Weiter", - "UIText_SaveChannel_SkipButton": "Überspringen", - "UIText_LogChannel_Title": "Stationeers Server UI", - "UIText_LogChannel_HeaderTitle": "Discord Channel Setup (3/6)", - "UIText_LogChannel_StepMessage": "Discord Log Channel ID eingeben", - "UIText_LogChannel_PrimaryPlaceholder": "Channel ID", - "UIText_LogChannel_PrimaryLabel": "Log Channel ID", - "UIText_LogChannel_SubmitButton": "Speichern & Weiter", - "UIText_LogChannel_SkipButton": "Überspringen", - "UIText_ConnectionListChannel_Title": "Stationeers Server UI", - "UIText_ConnectionListChannel_HeaderTitle": "Discord Channel Setup (4/6)", - "UIText_ConnectionListChannel_StepMessage": "Discord Verbindungslisten Channel ID eingeben", - "UIText_ConnectionListChannel_PrimaryPlaceholder": "Channel ID", - "UIText_ConnectionListChannel_PrimaryLabel": "Verbindungslisten Channel ID", - "UIText_ConnectionListChannel_SubmitButton": "Speichern & Weiter", - "UIText_ConnectionListChannel_SkipButton": "Überspringen", - "UIText_StatusChannel_Title": "Stationeers Server UI", - "UIText_StatusChannel_HeaderTitle": "Discord Channel Setup (5/6)", - "UIText_StatusChannel_StepMessage": "Discord Status Channel ID eingeben", - "UIText_StatusChannel_PrimaryPlaceholder": "Channel ID", - "UIText_StatusChannel_PrimaryLabel": "Status Channel ID", - "UIText_StatusChannel_SubmitButton": "Speichern & Weiter", - "UIText_StatusChannel_SkipButton": "Überspringen", - "UIText_ControlChannel_Title": "Stationeers Server UI", - "UIText_ControlChannel_HeaderTitle": "Discord Channel Setup (6/6)", - "UIText_ControlChannel_StepMessage": "Discord Control Channel ID eingeben", - "UIText_ControlChannel_PrimaryPlaceholder": "Channel ID", - "UIText_ControlChannel_PrimaryLabel": "Control Channel ID", - "UIText_ControlChannel_SubmitButton": "Speichern & Weiter", - "UIText_ControlChannel_SkipButton": "Überspringen", "UIText_NetworkConfigChoice_Title": "Stationeers Server UI", "UIText_NetworkConfigChoice_HeaderTitle": "Netzwerk Konfiguration", "UIText_NetworkConfigChoice_StepMessage": "Netzwerkeinstellungen konfigurieren? 'ja' für Konfiguration oder Überspringen für Standards. Hinweis: Netzwerkkonfiguration besonders wichtig auf Linux Servern.", @@ -241,36 +186,36 @@ "UIText_NetworkConfigChoice_SubmitButton": "Weiter", "UIText_NetworkConfigChoice_SkipButton": "Überspringen (Standards nutzen)", "UIText_GamePort_Title": "Stationeers Server UI", - "UIText_GamePort_HeaderTitle": "Netzwerk Setup (4/4)", + "UIText_GamePort_HeaderTitle": "Netzwerk (1/4)", "UIText_GamePort_StepMessage": "Port-Nummer für Spielverbindungen eingeben", "UIText_GamePort_PrimaryPlaceholder": "27016", "UIText_GamePort_PrimaryLabel": "Spiel Port", "UIText_GamePort_SubmitButton": "Speichern & Weiter", "UIText_GamePort_SkipButton": "Überspringen", "UIText_UpdatePort_Title": "Stationeers Server UI", - "UIText_UpdatePort_HeaderTitle": "Netzwerk Setup (4/4)", + "UIText_UpdatePort_HeaderTitle": "Netzwerk (2/4)", "UIText_UpdatePort_StepMessage": "Port-Nummer für Update-Verbindungen eingeben", "UIText_UpdatePort_PrimaryPlaceholder": "27015", "UIText_UpdatePort_PrimaryLabel": "Update Port", "UIText_UpdatePort_SubmitButton": "Speichern & Weiter", "UIText_UpdatePort_SkipButton": "Überspringen", "UIText_UPnPEnabled_Title": "Stationeers Server UI", - "UIText_UPnPEnabled_HeaderTitle": "Netzwerk Setup (4/4)", + "UIText_UPnPEnabled_HeaderTitle": "Netzwerk (3/4)", "UIText_UPnPEnabled_StepMessage": "UPnP aktivieren? 'ja' zum Aktivieren oder 'nein' zum Deaktivieren.", "UIText_UPnPEnabled_PrimaryPlaceholder": "ja/nein", "UIText_UPnPEnabled_PrimaryLabel": "UPnP Aktivieren", "UIText_UPnPEnabled_SubmitButton": "Speichern & Weiter", "UIText_UPnPEnabled_SkipButton": "Überspringen", "UIText_LocalIPAddress_Title": "Stationeers Server UI", - "UIText_LocalIPAddress_HeaderTitle": "Netzwerk Setup (4/4)", + "UIText_LocalIPAddress_HeaderTitle": "Netzwerk (4/4)", "UIText_LocalIPAddress_StepMessage": "Lokale IP-Adresse des Servers im Format 0.0.0.0 eingeben (keine CIDR Notation)", "UIText_LocalIPAddress_PrimaryPlaceholder": "0.0.0.0", "UIText_LocalIPAddress_PrimaryLabel": "Lokale IP-Adresse", "UIText_LocalIPAddress_SubmitButton": "Speichern & Weiter", "UIText_LocalIPAddress_SkipButton": "Überspringen", "UIText_AdminAccount_Title": "Stationeers Server UI", - "UIText_AdminAccount_HeaderTitle": "Admin Account Setup", - "UIText_AdminAccount_StepMessage": "Richte deinen Admin-Account ein.", + "UIText_AdminAccount_HeaderTitle": "Adminkonto", + "UIText_AdminAccount_StepMessage": "Richte dein Adminkonto ein.", "UIText_AdminAccount_PrimaryPlaceholder": "Benutzername", "UIText_AdminAccount_PrimaryLabel": "Benutzername", "UIText_AdminAccount_SecondaryLabel": "Passwort", @@ -284,7 +229,8 @@ "UIText_SSCM_PrimaryLabel": "Deaktivierung NICHT empfohlen.", "UIText_SSCM_SubmitButton": "Weiter", "UIText_SSCM_SkipButton": "Aktiviert lassen", - "UIText_Finalize_Title": "Setup Abschließen", + "UIText_Finalize_Title": "Das wars!", + "UIText_Finalize_HeaderTitle": "Setup Abschließen", "UIText_Finalize_StepMessage": "Bereit zum Abschließen? Deine Konfiguration wurde bereits während des Setups gespeichert. Für Änderungen klicke 'Zurück zum Start' und überspringe was behalten werden soll. Meiste Optionen auch im Config Tab änderbar.", "UIText_Finalize_SubmitButton": "Zurück zum Start", "UIText_Finalize_SkipButton": "Authentifizierung Überspringen", diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index d396ecdb..342f581b 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -127,111 +127,56 @@ "UIText_FooterText": "Need help? Check the Stationeers Server UI Github Wiki.", "UIText_SSCM_FooterText": "Use SSCM for the most powerful Stationeers server management! You can run commands from the Web console without disrupting vanilla behaviour!", "UIText_Welcome_Title": "Stationeers Server UI", + "UIText_Welcome_HeaderTitle": "Welcome!", "UIText_Welcome_SubmitButton": "Start Setup", "UIText_Welcome_SkipButton": "Skip Setup", "UIText_PlsRead_Title": "Please read!", - "UIText_PlsRead_HeaderTitle": "We strongly recommend you to read the texts in this setup wizard!", - "UIText_PlsRead_StepMessage": "Most reported issues occur because of a misconfiguration.", + "UIText_PlsRead_HeaderTitle": "Disclaimer", + "UIText_PlsRead_StepMessage": "We strongly recommend you to read the texts in this setup wizard! Most reported issues occur because of a misconfiguration.", "UIText_PlsRead_SubmitButton": "I understand", "UIText_PlsRead_SkipButton": "I understand", "UIText_ServerName_Title": "Stationeers Server UI", - "UIText_ServerName_HeaderTitle": "Server Name Setup", + "UIText_ServerName_HeaderTitle": "Server Name", "UIText_ServerName_StepMessage": "Give your server a name like 'Space Station 13'", "UIText_ServerName_PrimaryPlaceholder": "My Stationeers Server with UI", "UIText_ServerName_PrimaryLabel": "Server Name", "UIText_ServerName_SubmitButton": "Save & Continue", "UIText_ServerName_SkipButton": "Skip", "UIText_SaveIdentifier_Title": "Stationeers Server UI", - "UIText_SaveIdentifier_HeaderTitle": "Save Identifier Setup", + "UIText_SaveIdentifier_HeaderTitle": "Save Identifier", "UIText_SaveIdentifier_StepMessage": "Name of save folder, like 'MySave Lunar'. Capitalize the first letter of each word. To create a new world, provide the World type to generate. (MyLunarMap Lunar) Possible World types: Moon, Mars, Europa, Mimas, Vulcan, Space, Venus -- BETA BRANCH: Mars2, Europa3, MimasHerschel, Vulcan, Venus, Lunar", "UIText_SaveIdentifier_PrimaryPlaceholder": "Requires a SaveName and WorldType for first start!", "UIText_SaveIdentifier_PrimaryLabel": "Save Identifier", "UIText_SaveIdentifier_SubmitButton": "Save & Continue", "UIText_SaveIdentifier_SkipButton": "Skip", "UIText_MaxPlayers_Title": "Stationeers Server UI", - "UIText_MaxPlayers_HeaderTitle": "Player Limit Setup", + "UIText_MaxPlayers_HeaderTitle": "Player Limit", "UIText_MaxPlayers_StepMessage": "Choose the maximum number of players that can connect to the server. Recommended to not exceed 20", "UIText_MaxPlayers_PrimaryPlaceholder": "8", "UIText_MaxPlayers_PrimaryLabel": "Max Players", "UIText_MaxPlayers_SubmitButton": "Save & Continue", "UIText_MaxPlayers_SkipButton": "Skip", "UIText_ServerPassword_Title": "Stationeers Server UI", - "UIText_ServerPassword_HeaderTitle": "Server Password Setup", + "UIText_ServerPassword_HeaderTitle": "Server Password", "UIText_ServerPassword_StepMessage": "Set a gameserver password or skip this step.", "UIText_ServerPassword_PrimaryPlaceholder": "Server Password", "UIText_ServerPassword_PrimaryLabel": "Server Password", "UIText_ServerPassword_SubmitButton": "Save & Continue", "UIText_ServerPassword_SkipButton": "Skip", "UIText_GameBranch_Title": "Stationeers Server UI", - "UIText_GameBranch_HeaderTitle": "Game Branch Setup", + "UIText_GameBranch_HeaderTitle": "Game Branch", "UIText_GameBranch_StepMessage": "Enter a beta branch or skip this to use the release version. If switching branches, make sure to click Update Server on the Main dashboard after completing this wizzard.", "UIText_GameBranch_PrimaryPlaceholder": "beta", "UIText_GameBranch_PrimaryLabel": "Game Branch", "UIText_GameBranch_SubmitButton": "Save & Continue", "UIText_GameBranch_SkipButton": "Use Normal Version", - "UIText_NewTerrainAndSaveSystem_Title": "CHOOSE TERRAIN SYSTEM", - "UIText_NewTerrainAndSaveSystem_HeaderTitle": "Very important step!", + "UIText_NewTerrainAndSaveSystem_Title": "IMPORTANT", + "UIText_NewTerrainAndSaveSystem_HeaderTitle": "Select Terrain System", "UIText_NewTerrainAndSaveSystem_StepMessage": "Just switched to Beta? If yes, enable handling of the new terrain and save system here. Enter 'yes' to enable or 'no' to use the old system.", "UIText_NewTerrainAndSaveSystem_PrimaryPlaceholder": "yes/no", "UIText_NewTerrainAndSaveSystem_PrimaryLabel": "Enable new System", "UIText_NewTerrainAndSaveSystem_SubmitButton": "Save & Continue", "UIText_NewTerrainAndSaveSystem_SkipButton": "Skip", - "UIText_DiscordEnabled_Title": "Stationeers Server UI", - "UIText_DiscordEnabled_HeaderTitle": "Discord Integration", - "UIText_DiscordEnabled_StepMessage": "Do you want to enable Discord integration? Enter 'yes' to enable or Skip to disable.", - "UIText_DiscordEnabled_PrimaryPlaceholder": "yes", - "UIText_DiscordEnabled_PrimaryLabel": "Enable Discord", - "UIText_DiscordEnabled_SubmitButton": "Save & Continue", - "UIText_DiscordEnabled_SkipButton": "Skip (Disable Discord)", - "UIText_DiscordToken_Title": "Stationeers Server UI", - "UIText_DiscordToken_HeaderTitle": "Discord Bot Token", - "UIText_DiscordToken_StepMessage": "Enter your Discord bot token for server integration", - "UIText_DiscordToken_PrimaryPlaceholder": "Discord Bot Token", - "UIText_DiscordToken_PrimaryLabel": "Discord Token", - "UIText_DiscordToken_SubmitButton": "Save & Continue", - "UIText_DiscordToken_SkipButton": "Skip", - "UIText_ControlPanelChannel_Title": "Stationeers Server UI", - "UIText_ControlPanelChannel_HeaderTitle": "Discord Channel Setup (1/6)", - "UIText_ControlPanelChannel_StepMessage": "Enter Discord Control Panel Channel ID", - "UIText_ControlPanelChannel_PrimaryPlaceholder": "Channel ID", - "UIText_ControlPanelChannel_PrimaryLabel": "Control Panel Channel ID", - "UIText_ControlPanelChannel_SubmitButton": "Save & Continue", - "UIText_ControlPanelChannel_SkipButton": "Skip", - "UIText_SaveChannel_Title": "Stationeers Server UI", - "UIText_SaveChannel_HeaderTitle": "Discord Channel Setup (2/6)", - "UIText_SaveChannel_StepMessage": "Enter Discord Save Channel ID", - "UIText_SaveChannel_PrimaryPlaceholder": "Channel ID", - "UIText_SaveChannel_PrimaryLabel": "Save Channel ID", - "UIText_SaveChannel_SubmitButton": "Save & Continue", - "UIText_SaveChannel_SkipButton": "Skip", - "UIText_LogChannel_Title": "Stationeers Server UI", - "UIText_LogChannel_HeaderTitle": "Discord Channel Setup (3/6)", - "UIText_LogChannel_StepMessage": "Enter Discord Log Channel ID", - "UIText_LogChannel_PrimaryPlaceholder": "Channel ID", - "UIText_LogChannel_PrimaryLabel": "Log Channel ID", - "UIText_LogChannel_SubmitButton": "Save & Continue", - "UIText_LogChannel_SkipButton": "Skip", - "UIText_ConnectionListChannel_Title": "Stationeers Server UI", - "UIText_ConnectionListChannel_HeaderTitle": "Discord Channel Setup (4/6)", - "UIText_ConnectionListChannel_StepMessage": "Enter Discord Connection List Channel ID", - "UIText_ConnectionListChannel_PrimaryPlaceholder": "Channel ID", - "UIText_ConnectionListChannel_PrimaryLabel": "Connection List Channel ID", - "UIText_ConnectionListChannel_SubmitButton": "Save & Continue", - "UIText_ConnectionListChannel_SkipButton": "Skip", - "UIText_StatusChannel_Title": "Stationeers Server UI", - "UIText_StatusChannel_HeaderTitle": "Discord Channel Setup (5/6)", - "UIText_StatusChannel_StepMessage": "Enter Discord Status Channel ID", - "UIText_StatusChannel_PrimaryPlaceholder": "Channel ID", - "UIText_StatusChannel_PrimaryLabel": "Status Channel ID", - "UIText_StatusChannel_SubmitButton": "Save & Continue", - "UIText_StatusChannel_SkipButton": "Skip", - "UIText_ControlChannel_Title": "Stationeers Server UI", - "UIText_ControlChannel_HeaderTitle": "Discord Channel Setup (6/6)", - "UIText_ControlChannel_StepMessage": "Enter Discord Control Channel ID", - "UIText_ControlChannel_PrimaryPlaceholder": "Channel ID", - "UIText_ControlChannel_PrimaryLabel": "Control Channel ID", - "UIText_ControlChannel_SubmitButton": "Save & Continue", - "UIText_ControlChannel_SkipButton": "Skip", "UIText_NetworkConfigChoice_Title": "Stationeers Server UI", "UIText_NetworkConfigChoice_HeaderTitle": "Network Configuration", "UIText_NetworkConfigChoice_StepMessage": "Do you want to configure network settings? Enter 'yes' to configure or Skip to use defaults. Note: Network configuration is especially important on Linux servers.", @@ -240,35 +185,35 @@ "UIText_NetworkConfigChoice_SubmitButton": "Continue", "UIText_NetworkConfigChoice_SkipButton": "Skip (Use Defaults)", "UIText_GamePort_Title": "Stationeers Server UI", - "UIText_GamePort_HeaderTitle": "Network Setup (1/4)", + "UIText_GamePort_HeaderTitle": "Network (1/4)", "UIText_GamePort_StepMessage": "Enter the port number for game connections", "UIText_GamePort_PrimaryPlaceholder": "27016", "UIText_GamePort_PrimaryLabel": "Game Port", "UIText_GamePort_SubmitButton": "Save & Continue", "UIText_GamePort_SkipButton": "Skip", "UIText_UpdatePort_Title": "Stationeers Server UI", - "UIText_UpdatePort_HeaderTitle": "Network Setup (2/4)", + "UIText_UpdatePort_HeaderTitle": "Network (2/4)", "UIText_UpdatePort_StepMessage": "Enter the port number for update connections", "UIText_UpdatePort_PrimaryPlaceholder": "27015", "UIText_UpdatePort_PrimaryLabel": "Update Port", "UIText_UpdatePort_SubmitButton": "Save & Continue", "UIText_UpdatePort_SkipButton": "Skip", "UIText_UPnPEnabled_Title": "Stationeers Server UI", - "UIText_UPnPEnabled_HeaderTitle": "Network Setup (3/4)", + "UIText_UPnPEnabled_HeaderTitle": "Network (3/4)", "UIText_UPnPEnabled_StepMessage": "Enable UPnP? Enter 'yes' to enable or 'no' to disable.", "UIText_UPnPEnabled_PrimaryPlaceholder": "yes/no", "UIText_UPnPEnabled_PrimaryLabel": "Enable UPnP", "UIText_UPnPEnabled_SubmitButton": "Save & Continue", "UIText_UPnPEnabled_SkipButton": "Skip", "UIText_LocalIPAddress_Title": "Stationeers Server UI", - "UIText_LocalIPAddress_HeaderTitle": "Network Setup (4/4)", + "UIText_LocalIPAddress_HeaderTitle": "Network (4/4)", "UIText_LocalIPAddress_StepMessage": "Enter server's local IP address in format 0.0.0.0 (no CIDR notation)", "UIText_LocalIPAddress_PrimaryPlaceholder": "0.0.0.0", "UIText_LocalIPAddress_PrimaryLabel": "Local IP Address", "UIText_LocalIPAddress_SubmitButton": "Save & Continue", "UIText_LocalIPAddress_SkipButton": "Skip", "UIText_AdminAccount_Title": "Stationeers Server UI", - "UIText_AdminAccount_HeaderTitle": "Admin Account Setup", + "UIText_AdminAccount_HeaderTitle": "Admin Account", "UIText_AdminAccount_StepMessage": "Set up your SSUI admin account.", "UIText_AdminAccount_PrimaryPlaceholder": "Username", "UIText_AdminAccount_PrimaryLabel": "Username", @@ -276,14 +221,8 @@ "UIText_AdminAccount_SecondaryPlaceholder": "Password", "UIText_AdminAccount_SubmitButton": "Save & Continue", "UIText_AdminAccount_SkipButton": "Skip Authentication", - "UIText_SSCM_Title": "Stationeers Server Command Manager", - "UIText_SSCM_HeaderTitle": "Unique Feature", - "UIText_SSCM_StepMessage": "SSCM is a custom server plugin that allows you to execute server commands directly from SSUI. It gives you the ability to run commands from the Web console without disrupting vanlilla behaviour, you dont need any client side mods.", - "UIText_SSCM_PrimaryPlaceholder": "type 'no' to disable", - "UIText_SSCM_PrimaryLabel": "Opting out is NOT recommended.", - "UIText_SSCM_SubmitButton": "Continue", - "UIText_SSCM_SkipButton": "Keep enabled", - "UIText_Finalize_Title": "Finalize Setup", + "UIText_Finalize_Title": "You did it", + "UIText_Finalize_HeaderTitle": "Finalize Setup", "UIText_Finalize_StepMessage": "Ready to finalize? Your configuration has already been saved while you completed this setup. If you want to change any of the settings, you may click Return to Start and skip whatever you want to keep. Most options can also be changed on the config Tab in the UI later.", "UIText_Finalize_SubmitButton": "Return to Start", "UIText_Finalize_SkipButton": "Skip Authentication", diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/UIMod/onboard_bundled/localization/sv-SE.json index 54b5074c..388835ec 100644 --- a/UIMod/onboard_bundled/localization/sv-SE.json +++ b/UIMod/onboard_bundled/localization/sv-SE.json @@ -127,148 +127,93 @@ "UIText_FooterText": "Behöver du hjälp? Kolla Stationeers Server UI Github Wiki.", "UIText_SSCM_FooterText": "Använd SSCM för den mest kraftfulla hanteringen av Stationeers-servrar! Du kan köra kommandon från webbkonsolen utan att störa vanliga funktioner!", "UIText_Welcome_Title": "Stationeers Server UI", + "UIText_Welcome_HeaderTitle": "Välkommen!", "UIText_Welcome_SubmitButton": "Börja konfigurera", "UIText_Welcome_SkipButton": "Hoppa över konfiguration", "UIText_PlsRead_Title": "Läs detta!", - "UIText_PlsRead_HeaderTitle": "Viktigt att läsa!", - "UIText_PlsRead_StepMessage": "De flesta rapporterade problemen beror på felkonfiguration.", + "UIText_PlsRead_HeaderTitle": "Viktigt", + "UIText_PlsRead_StepMessage": "Läs texterna ordentligt! De flesta rapporterade problem beror på felaktiga inställningar.", "UIText_PlsRead_SubmitButton": "Jag förstår", "UIText_PlsRead_SkipButton": "Jag förstår", "UIText_ServerName_Title": "Stationeers Server UI", - "UIText_ServerName_HeaderTitle": "Servernamninställning", + "UIText_ServerName_HeaderTitle": "Servernamn", "UIText_ServerName_StepMessage": "Ge din server ett namn, t.ex. 'Rymdstation 13'", "UIText_ServerName_PrimaryPlaceholder": "Min Stationeers-server med UI", "UIText_ServerName_PrimaryLabel": "Servernamn", "UIText_ServerName_SubmitButton": "Spara & fortsätt", "UIText_ServerName_SkipButton": "Hoppa över", "UIText_SaveIdentifier_Title": "Stationeers Server UI", - "UIText_SaveIdentifier_HeaderTitle": "Sparidentifieringsinställning", + "UIText_SaveIdentifier_HeaderTitle": "Sparnamn", "UIText_SaveIdentifier_StepMessage": "Ange en sparidentifierare, t.ex. 'SpaceStation13 Vulcan'. Använd stor bokstav i början av varje ord. Världstyper finns på Stationeers Wiki -> Dedicated Server.", "UIText_SaveIdentifier_PrimaryPlaceholder": "Kräver ett sparnamn och världstyp vid första start!", "UIText_SaveIdentifier_PrimaryLabel": "Sparidentifierare", "UIText_SaveIdentifier_SubmitButton": "Spara & fortsätt", "UIText_SaveIdentifier_SkipButton": "Hoppa över", "UIText_MaxPlayers_Title": "Stationeers Server UI", - "UIText_MaxPlayers_HeaderTitle": "Spelargränsinställning", + "UIText_MaxPlayers_HeaderTitle": "Spelargräns", "UIText_MaxPlayers_StepMessage": "Välj maximalt antal spelare som kan ansluta till servern.", "UIText_MaxPlayers_PrimaryPlaceholder": "8", "UIText_MaxPlayers_PrimaryLabel": "Max spelare", "UIText_MaxPlayers_SubmitButton": "Spara & fortsätt", "UIText_MaxPlayers_SkipButton": "Hoppa över", "UIText_ServerPassword_Title": "Stationeers Server UI", - "UIText_ServerPassword_HeaderTitle": "Serverlösenordsinställning", + "UIText_ServerPassword_HeaderTitle": "Serverlösenord", "UIText_ServerPassword_StepMessage": "Ange ett serverlösenord eller hoppa över detta steg.", "UIText_ServerPassword_PrimaryPlaceholder": "Serverlösenord", "UIText_ServerPassword_PrimaryLabel": "Serverlösenord", "UIText_ServerPassword_SubmitButton": "Spara & fortsätt", "UIText_ServerPassword_SkipButton": "Hoppa över", "UIText_GameBranch_Title": "Stationeers Server UI", - "UIText_GameBranch_HeaderTitle": "Spelgreninställning", + "UIText_GameBranch_HeaderTitle": "Spel-branch", "UIText_GameBranch_StepMessage": "Ange en betagren eller hoppa över för att använda standardversionen. Vid byte av gren, starta om SSUI efter guiden.", "UIText_GameBranch_PrimaryPlaceholder": "beta", "UIText_GameBranch_PrimaryLabel": "Spelgren", "UIText_GameBranch_SubmitButton": "Spara & fortsätt", "UIText_GameBranch_SkipButton": "Använd standardversion", - "UIText_NewTerrainAndSaveSystem_Title": "VÄLJ TERRÄNGSYSTEM", - "UIText_NewTerrainAndSaveSystem_HeaderTitle": "Viktigt steg!", + "UIText_NewTerrainAndSaveSystem_Title": "Viktigt", + "UIText_NewTerrainAndSaveSystem_HeaderTitle": "Välj terrängsystem", "UIText_NewTerrainAndSaveSystem_StepMessage": "Bytt till beta? Aktivera terräng- och sparsystem för att stödja det! Ange 'ja' för att aktivera eller 'nej' för att inaktivera.", "UIText_NewTerrainAndSaveSystem_PrimaryPlaceholder": "yes/no", "UIText_NewTerrainAndSaveSystem_PrimaryLabel": "Aktivera nytt system", "UIText_NewTerrainAndSaveSystem_SubmitButton": "Spara & fortsätt", "UIText_NewTerrainAndSaveSystem_SkipButton": "Hoppa över", - "UIText_DiscordEnabled_Title": "Stationeers Server UI", - "UIText_DiscordEnabled_HeaderTitle": "Discord-integration", - "UIText_DiscordEnabled_StepMessage": "Vill du aktivera Discord-integration? Ange 'ja' för att aktivera eller hoppa över för att inaktivera.", - "UIText_DiscordEnabled_PrimaryPlaceholder": "yes/no", - "UIText_DiscordEnabled_PrimaryLabel": "Aktivera Discord", - "UIText_DiscordEnabled_SubmitButton": "Spara & fortsätt", - "UIText_DiscordEnabled_SkipButton": "Hoppa över (inaktivera Discord)", - "UIText_DiscordToken_Title": "Stationeers Server UI", - "UIText_DiscordToken_HeaderTitle": "Discord-bot-token", - "UIText_DiscordToken_StepMessage": "Ange din Discord-bot-token för serverintegration", - "UIText_DiscordToken_PrimaryPlaceholder": "Discord-bot-token", - "UIText_DiscordToken_PrimaryLabel": "Discord-token", - "UIText_DiscordToken_SubmitButton": "Spara & fortsätt", - "UIText_DiscordToken_SkipButton": "Hoppa över", - "UIText_ControlPanelChannel_Title": "Stationeers Server UI", - "UIText_ControlPanelChannel_HeaderTitle": "Discord-kanalinställning (1/6)", - "UIText_ControlPanelChannel_StepMessage": "Ange Discord-kontrollpanelkanalens ID", - "UIText_ControlPanelChannel_PrimaryPlaceholder": "Kanal-ID", - "UIText_ControlPanelChannel_PrimaryLabel": "Kontrollpanelkanal-ID", - "UIText_ControlPanelChannel_SubmitButton": "Spara & fortsätt", - "UIText_ControlPanelChannel_SkipButton": "Hoppa över", - "UIText_SaveChannel_Title": "Stationeers Server UI", - "UIText_SaveChannel_HeaderTitle": "Discord-kanalinställning (2/6)", - "UIText_SaveChannel_StepMessage": "Ange Discord-sparfilskanalens ID", - "UIText_SaveChannel_PrimaryPlaceholder": "Kanal-ID", - "UIText_SaveChannel_PrimaryLabel": "Sparkanal-ID", - "UIText_SaveChannel_SubmitButton": "Spara & fortsätt", - "UIText_SaveChannel_SkipButton": "Hoppa över", - "UIText_LogChannel_Title": "Stationeers Server UI", - "UIText_LogChannel_HeaderTitle": "Discord-kanalinställning (3/6)", - "UIText_LogChannel_StepMessage": "Ange Discord-loggkanalens ID", - "UIText_LogChannel_PrimaryPlaceholder": "Kanal-ID", - "UIText_LogChannel_PrimaryLabel": "Loggkanal-ID", - "UIText_LogChannel_SubmitButton": "Spara & fortsätt", - "UIText_LogChannel_SkipButton": "Hoppa över", - "UIText_ConnectionListChannel_Title": "Stationeers Server UI", - "UIText_ConnectionListChannel_HeaderTitle": "Discord-kanalinställning (4/6)", - "UIText_ConnectionListChannel_StepMessage": "Ange Discord-anslutningslistkanalens ID", - "UIText_ConnectionListChannel_PrimaryPlaceholder": "Kanal-ID", - "UIText_ConnectionListChannel_PrimaryLabel": "Anslutningslistkanal-ID", - "UIText_ConnectionListChannel_SubmitButton": "Spara & fortsätt", - "UIText_ConnectionListChannel_SkipButton": "Hoppa över", - "UIText_StatusChannel_Title": "Stationeers Server UI", - "UIText_StatusChannel_HeaderTitle": "Discord-kanalinställning (5/6)", - "UIText_StatusChannel_StepMessage": "Ange Discord-statuskanalens ID", - "UIText_StatusChannel_PrimaryPlaceholder": "Kanal-ID", - "UIText_StatusChannel_PrimaryLabel": "Statuskanal-ID", - "UIText_StatusChannel_SubmitButton": "Spara & fortsätt", - "UIText_StatusChannel_SkipButton": "Hoppa över", - "UIText_ControlChannel_Title": "Stationeers Server UI", - "UIText_ControlChannel_HeaderTitle": "Discord-kanalinställning (6/6)", - "UIText_ControlChannel_StepMessage": "Ange Discord-kontrollkanalens ID", - "UIText_ControlChannel_PrimaryPlaceholder": "Kanal-ID", - "UIText_ControlChannel_PrimaryLabel": "Kontrollkanal-ID", - "UIText_ControlChannel_SubmitButton": "Spara & fortsätt", - "UIText_ControlChannel_SkipButton": "Hoppa över", "UIText_NetworkConfigChoice_Title": "Stationeers Server UI", - "UIText_NetworkConfigChoice_HeaderTitle": "Nätverkskonfiguration", + "UIText_NetworkConfigChoice_HeaderTitle": "Nätverk", "UIText_NetworkConfigChoice_StepMessage": "Vill du konfigurera nätverksinställningar? Ange 'ja' för att konfigurera eller hoppa över för att använda standardvärden. Obs: Nätverkskonfiguration är särskilt viktigt på Linux-servrar.", "UIText_NetworkConfigChoice_PrimaryPlaceholder": "ja", "UIText_NetworkConfigChoice_PrimaryLabel": "Konfigurera nätverk", "UIText_NetworkConfigChoice_SubmitButton": "Fortsätt", "UIText_NetworkConfigChoice_SkipButton": "Hoppa över (använd standardvärden)", "UIText_GamePort_Title": "Stationeers Server UI", - "UIText_GamePort_HeaderTitle": "Nätverksinställning (1/4)", + "UIText_GamePort_HeaderTitle": "Nätverk (1/4)", "UIText_GamePort_StepMessage": "Ange portnummer för spelanslutningar", "UIText_GamePort_PrimaryPlaceholder": "27016", "UIText_GamePort_PrimaryLabel": "Spelport", "UIText_GamePort_SubmitButton": "Spara & fortsätt", "UIText_GamePort_SkipButton": "Hoppa över", "UIText_UpdatePort_Title": "Stationeers Server UI", - "UIText_UpdatePort_HeaderTitle": "Nätverksinställning (2/4)", + "UIText_UpdatePort_HeaderTitle": "Nätverk (2/4)", "UIText_UpdatePort_StepMessage": "Ange portnummer för uppdateringsanslutningar", "UIText_UpdatePort_PrimaryPlaceholder": "27015", "UIText_UpdatePort_PrimaryLabel": "Uppdateringsport", "UIText_UpdatePort_SubmitButton": "Spara & fortsätt", "UIText_UpdatePort_SkipButton": "Hoppa över", "UIText_UPnPEnabled_Title": "Stationeers Server UI", - "UIText_UPnPEnabled_HeaderTitle": "Nätverksinställning (3/4)", + "UIText_UPnPEnabled_HeaderTitle": "Nätverk (3/4)", "UIText_UPnPEnabled_StepMessage": "Aktivera UPnP? Ange 'ja' för att aktivera eller 'nej' för att inaktivera.", "UIText_UPnPEnabled_PrimaryPlaceholder": "yes/no", "UIText_UPnPEnabled_PrimaryLabel": "Aktivera UPnP", "UIText_UPnPEnabled_SubmitButton": "Spara & fortsätt", "UIText_UPnPEnabled_SkipButton": "Hoppa över", "UIText_LocalIPAddress_Title": "Stationeers Server UI", - "UIText_LocalIPAddress_HeaderTitle": "Nätverksinställning (4/4)", + "UIText_LocalIPAddress_HeaderTitle": "Nätverk (4/4)", "UIText_LocalIPAddress_StepMessage": "Ange serverns lokala IP-adress i formatet 0.0.0.0 (ingen CIDR-notation)", "UIText_LocalIPAddress_PrimaryPlaceholder": "0.0.0.0", "UIText_LocalIPAddress_PrimaryLabel": "Lokal IP-adress", "UIText_LocalIPAddress_SubmitButton": "Spara & fortsätt", "UIText_LocalIPAddress_SkipButton": "Hoppa över", "UIText_AdminAccount_Title": "Stationeers Server UI", - "UIText_AdminAccount_HeaderTitle": "Admin-kontoinställning", + "UIText_AdminAccount_HeaderTitle": "Admin-konto", "UIText_AdminAccount_StepMessage": "Konfigurera ditt admin-konto.", "UIText_AdminAccount_PrimaryPlaceholder": "Användarnamn", "UIText_AdminAccount_PrimaryLabel": "Användarnamn", @@ -283,7 +228,8 @@ "UIText_SSCM_PrimaryLabel": "Att välja bort rekommenderas INTE.", "UIText_SSCM_SubmitButton": "Fortsätt", "UIText_SSCM_SkipButton": "Behåll aktiverad", - "UIText_Finalize_Title": "Slutför konfiguration", + "UIText_Finalize_Title": "Det var allt", + "UIText_Finalize_HeaderTitle": "Slutför konfiguration", "UIText_Finalize_StepMessage": "Redo att slutföra? Din konfiguration har redan sparats under guiden. Om du vill ändra inställningar kan du klicka på Gå tillbaka till start och hoppa över det du vill behålla. De flesta inställningar kan också ändras i konfigurationsfliken i gränssnittet.", "UIText_Finalize_SubmitButton": "Gå tillbaka till start", "UIText_Finalize_SkipButton": "Hoppa över autentisering", diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.css b/UIMod/onboard_bundled/twoboxform/twoboxform.css index 1df32dca..3372918d 100644 --- a/UIMod/onboard_bundled/twoboxform/twoboxform.css +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.css @@ -482,4 +482,71 @@ footer { h1 { font-size: 1.2rem; } +} + +.progress-bar { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 10px; + margin-top: 20px; + padding-top: 10px; + margin-bottom: 20px; +} + +.progress-step { + display: flex; + flex-direction: column; + align-items: center; + width: 80px; + text-align: center; +} + +.progress-circle { + display: block; + width: 20px; + height: 20px; + border-radius: 50%; + opacity: 0.7; + border: 2px solid var(--primary); + transition: all 0.3s ease; +} + +.progress-circle.active { + background-color: var(--primary-dim); + scale: 1.2; +} + +.progress-step.active .progress-circle { + background-color: var(--primary); + opacity: 1; + box-shadow: 0 0 10px var(--primary-glow); +} + +.progress-circle:hover { + background-color: var(--primary); + transform: scale(1.2); +} + +.progress-label { + font-size: 0.7rem; + color: var(--primary-dim); + margin-top: 5px; + text-wrap: balance; + line-height: 1.2; +} + +.progress-step.active .progress-label { + color: var(--text-bright); +} + +#progress-bar-container { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + padding: 10px 0; + z-index: 100; + display: flex; + justify-content: center; } \ No newline at end of file diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.html b/UIMod/onboard_bundled/twoboxform/twoboxform.html index 5fa32691..804494ee 100644 --- a/UIMod/onboard_bundled/twoboxform/twoboxform.html +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.html @@ -111,6 +111,18 @@

{{.HeaderTitle}}

+ {{if and (eq .Mode "setup") (ne .Step "welcome")}} +
+
+ {{range .Steps}} +
+ + {{.HeaderTitle}} +
+ {{end}} +
+ {{end}} + \ No newline at end of file diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.js b/UIMod/onboard_bundled/twoboxform/twoboxform.js index d329e72b..46b61bc7 100644 --- a/UIMod/onboard_bundled/twoboxform/twoboxform.js +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.js @@ -67,9 +67,9 @@ document.addEventListener('DOMContentLoaded', () => { function booleanToConfig(value) { if (typeof value === 'string') { value = value.trim().toLowerCase(); - if (value === 'yes' || value === 'true' || value === '1') { + if (value === 'yes' || value === 'true' || value === '1' || value === 'ja') { return true; - } else if (value === 'no' || value === 'false' || value === '0') { + } else if (value === 'no' || value === 'false' || value === '0' || value === 'nej') { return false; } } diff --git a/src/web/TwoBoxForm.go b/src/web/TwoBoxForm.go index e509020f..77bdc157 100644 --- a/src/web/TwoBoxForm.go +++ b/src/web/TwoBoxForm.go @@ -46,6 +46,7 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { NextStep string PrimaryPlaceholderText string SecondaryPlaceholderText string + Steps []Step } twoboxformAssetsFS, err := fs.Sub(config.GetV1UIFS(), "UIMod/onboard_bundled/twoboxform") @@ -73,7 +74,7 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { "welcome": { ID: "welcome", Title: localization.GetString("UIText_Welcome_Title"), - HeaderTitle: "", + HeaderTitle: localization.GetString("UIText_Welcome_HeaderTitle"), StepMessage: "", PrimaryLabel: "", SecondaryLabel: "", @@ -178,119 +179,6 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { ConfigField: "IsNewTerrainAndSaveSystem", NextStep: "network_config_choice", }, - "discord_enabled": { - ID: "discord_enabled", - Title: localization.GetString("UIText_DiscordEnabled_Title"), - HeaderTitle: localization.GetString("UIText_DiscordEnabled_HeaderTitle"), - StepMessage: localization.GetString("UIText_DiscordEnabled_StepMessage"), - PrimaryPlaceholderText: localization.GetString("UIText_DiscordEnabled_PrimaryPlaceholder"), - PrimaryLabel: localization.GetString("UIText_DiscordEnabled_PrimaryLabel"), - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: localization.GetString("UIText_DiscordEnabled_SubmitButton"), - SkipButtonText: localization.GetString("UIText_DiscordEnabled_SkipButton"), - ConfigField: "isDiscordEnabled", // We'll handle the boolean conversion in JS - NextStep: "discord_token", // Default next step if enabled - // The actual next step will be determined by JS based on the answer - }, - "discord_token": { - ID: "discord_token", - Title: localization.GetString("UIText_DiscordToken_Title"), - HeaderTitle: localization.GetString("UIText_DiscordToken_HeaderTitle"), - StepMessage: localization.GetString("UIText_DiscordToken_StepMessage"), - PrimaryPlaceholderText: localization.GetString("UIText_DiscordToken_PrimaryPlaceholder"), - PrimaryLabel: localization.GetString("UIText_DiscordToken_PrimaryLabel"), - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: localization.GetString("UIText_DiscordToken_SubmitButton"), - SkipButtonText: localization.GetString("UIText_DiscordToken_SkipButton"), - ConfigField: "discordToken", - NextStep: "control_panel_channel", - }, - "control_panel_channel": { - ID: "control_panel_channel", - Title: localization.GetString("UIText_ControlPanelChannel_Title"), - HeaderTitle: localization.GetString("UIText_ControlPanelChannel_HeaderTitle"), - StepMessage: localization.GetString("UIText_ControlPanelChannel_StepMessage"), - PrimaryPlaceholderText: localization.GetString("UIText_ControlPanelChannel_PrimaryPlaceholder"), - PrimaryLabel: localization.GetString("UIText_ControlPanelChannel_PrimaryLabel"), - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: localization.GetString("UIText_ControlPanelChannel_SubmitButton"), - SkipButtonText: localization.GetString("UIText_ControlPanelChannel_SkipButton"), - ConfigField: "controlPanelChannelID", - NextStep: "save_channel", - }, - "save_channel": { - ID: "save_channel", - Title: localization.GetString("UIText_SaveChannel_Title"), - HeaderTitle: localization.GetString("UIText_SaveChannel_HeaderTitle"), - StepMessage: localization.GetString("UIText_SaveChannel_StepMessage"), - PrimaryPlaceholderText: localization.GetString("UIText_SaveChannel_PrimaryPlaceholder"), - PrimaryLabel: localization.GetString("UIText_SaveChannel_PrimaryLabel"), - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: localization.GetString("UIText_SaveChannel_SubmitButton"), - SkipButtonText: localization.GetString("UIText_SaveChannel_SkipButton"), - ConfigField: "saveChannelID", - NextStep: "log_channel", - }, - "log_channel": { - ID: "log_channel", - Title: localization.GetString("UIText_LogChannel_Title"), - HeaderTitle: localization.GetString("UIText_LogChannel_HeaderTitle"), - StepMessage: localization.GetString("UIText_LogChannel_StepMessage"), - PrimaryPlaceholderText: localization.GetString("UIText_LogChannel_PrimaryPlaceholder"), - PrimaryLabel: localization.GetString("UIText_LogChannel_PrimaryLabel"), - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: localization.GetString("UIText_LogChannel_SubmitButton"), - SkipButtonText: localization.GetString("UIText_LogChannel_SkipButton"), - ConfigField: "logChannelID", - NextStep: "connection_list_channel", - }, - "connection_list_channel": { - ID: "connection_list_channel", - Title: localization.GetString("UIText_ConnectionListChannel_Title"), - HeaderTitle: localization.GetString("UIText_ConnectionListChannel_HeaderTitle"), - StepMessage: localization.GetString("UIText_ConnectionListChannel_StepMessage"), - PrimaryPlaceholderText: localization.GetString("UIText_ConnectionListChannel_PrimaryPlaceholder"), - PrimaryLabel: localization.GetString("UIText_ConnectionListChannel_PrimaryLabel"), - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: localization.GetString("UIText_ConnectionListChannel_SubmitButton"), - SkipButtonText: localization.GetString("UIText_ConnectionListChannel_SkipButton"), - ConfigField: "connectionListChannelID", - NextStep: "status_channel", - }, - "status_channel": { - ID: "status_channel", - Title: localization.GetString("UIText_StatusChannel_Title"), - HeaderTitle: localization.GetString("UIText_StatusChannel_HeaderTitle"), - StepMessage: localization.GetString("UIText_StatusChannel_StepMessage"), - PrimaryPlaceholderText: localization.GetString("UIText_StatusChannel_PrimaryPlaceholder"), - PrimaryLabel: localization.GetString("UIText_StatusChannel_PrimaryLabel"), - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: localization.GetString("UIText_StatusChannel_SubmitButton"), - SkipButtonText: localization.GetString("UIText_StatusChannel_SkipButton"), - ConfigField: "statusChannelID", - NextStep: "control_channel", - }, - "control_channel": { - ID: "control_channel", - Title: localization.GetString("UIText_ControlChannel_Title"), - HeaderTitle: localization.GetString("UIText_ControlChannel_HeaderTitle"), - StepMessage: localization.GetString("UIText_ControlChannel_StepMessage"), - PrimaryPlaceholderText: localization.GetString("UIText_ControlChannel_PrimaryPlaceholder"), - PrimaryLabel: localization.GetString("UIText_ControlChannel_PrimaryLabel"), - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: localization.GetString("UIText_ControlChannel_SubmitButton"), - SkipButtonText: localization.GetString("UIText_ControlChannel_SkipButton"), - ConfigField: "controlChannelID", - NextStep: "network_config_choice", - }, "network_config_choice": { ID: "network_config_choice", Title: localization.GetString("UIText_NetworkConfigChoice_Title"), @@ -378,24 +266,10 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { ConfigField: "", NextStep: "finalize", }, - "sscm": { - ID: "sscm", - Title: localization.GetString("UIText_SSCM_Title"), - HeaderTitle: localization.GetString("UIText_SSCM_HeaderTitle"), - StepMessage: localization.GetString("UIText_SSCM_StepMessage"), - PrimaryPlaceholderText: localization.GetString("UIText_SSCM_PrimaryPlaceholder"), - PrimaryLabel: localization.GetString("UIText_SSCM_PrimaryLabel"), - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: localization.GetString("UIText_SSCM_SubmitButton"), - SkipButtonText: localization.GetString("UIText_SSCM_SkipButton"), - ConfigField: "IsSSCMEnabled", - NextStep: "finalize", - }, "finalize": { ID: "finalize", Title: localization.GetString("UIText_Finalize_Title"), - HeaderTitle: "", + HeaderTitle: localization.GetString("UIText_Finalize_HeaderTitle"), StepMessage: localization.GetString("UIText_Finalize_StepMessage"), PrimaryLabel: "", SecondaryLabel: "", @@ -454,6 +328,21 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { data.NextStep = welcomeStep.NextStep data.Step = "welcome" } + stepOrder := []string{ + "welcome", "pls_read", "server_name", "save_identifier", "max_players", + "server_password", "game_branch", "newterrain_and_savesystem", + "discord_enabled", "discord_token", "control_panel_channel", "save_channel", + "log_channel", "connection_list_channel", "status_channel", "control_channel", + "network_config_choice", "game_port", "update_port", "upnp_enabled", + "local_ip_address", "admin_account", "sscm", "finalize", + } + var stepSlice []Step + for _, id := range stepOrder { + if step, exists := steps[id]; exists { + stepSlice = append(stepSlice, step) + } + } + data.Steps = stepSlice case path == "/changeuser": data.Title = localization.GetString("UIText_ChangeUser_Title") From 5fe2f17f2a25994018cf43540b1957634bf14204 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 15:26:24 +0200 Subject: [PATCH 23/37] TwoBoxForm Setup now simplifies SaveInfo setup (usual error source) --- UIMod/onboard_bundled/localization/de-DE.json | 2 -- UIMod/onboard_bundled/localization/en-US.json | 3 +-- UIMod/onboard_bundled/localization/sv-SE.json | 2 -- .../onboard_bundled/twoboxform/twoboxform.js | 9 +++++++ src/web/TwoBoxForm.go | 25 ++++++++++--------- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/UIMod/onboard_bundled/localization/de-DE.json index 88b7e365..c82efe09 100644 --- a/UIMod/onboard_bundled/localization/de-DE.json +++ b/UIMod/onboard_bundled/localization/de-DE.json @@ -145,9 +145,7 @@ "UIText_ServerName_SkipButton": "Überspringen", "UIText_SaveIdentifier_Title": "Stationeers Server UI", "UIText_SaveIdentifier_HeaderTitle": "Speicher-Identifikator", - "UIText_SaveIdentifier_StepMessage": "Setze einen Speicher-Identifikator wie 'Weltraumstation13 Vulcan'. Ersten Buchstaben jedes Wortes groß schreiben. Welttypen im Stationeers Wiki -> Dedicated Server", "UIText_SaveIdentifier_PrimaryPlaceholder": "Benötigt SaveName und WorldType für ersten Start!", - "UIText_SaveIdentifier_PrimaryLabel": "Speicher-Identifikator", "UIText_SaveIdentifier_SubmitButton": "Speichern & Weiter", "UIText_SaveIdentifier_SkipButton": "Überspringen", "UIText_MaxPlayers_Title": "Stationeers Server UI", diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index 342f581b..6ca79be1 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -144,9 +144,8 @@ "UIText_ServerName_SkipButton": "Skip", "UIText_SaveIdentifier_Title": "Stationeers Server UI", "UIText_SaveIdentifier_HeaderTitle": "Save Identifier", - "UIText_SaveIdentifier_StepMessage": "Name of save folder, like 'MySave Lunar'. Capitalize the first letter of each word. To create a new world, provide the World type to generate. (MyLunarMap Lunar) Possible World types: Moon, Mars, Europa, Mimas, Vulcan, Space, Venus -- BETA BRANCH: Mars2, Europa3, MimasHerschel, Vulcan, Venus, Lunar", + "UIText_SaveIdentifier_StepMessage": "World types: Moon, Mars, Europa, Mimas, Vulcan, Space, Venus / Beta: Mars2, Europa3, MimasHerschel, Vulcan, Venus, Lunar", "UIText_SaveIdentifier_PrimaryPlaceholder": "Requires a SaveName and WorldType for first start!", - "UIText_SaveIdentifier_PrimaryLabel": "Save Identifier", "UIText_SaveIdentifier_SubmitButton": "Save & Continue", "UIText_SaveIdentifier_SkipButton": "Skip", "UIText_MaxPlayers_Title": "Stationeers Server UI", diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/UIMod/onboard_bundled/localization/sv-SE.json index 388835ec..b617e1ed 100644 --- a/UIMod/onboard_bundled/localization/sv-SE.json +++ b/UIMod/onboard_bundled/localization/sv-SE.json @@ -144,9 +144,7 @@ "UIText_ServerName_SkipButton": "Hoppa över", "UIText_SaveIdentifier_Title": "Stationeers Server UI", "UIText_SaveIdentifier_HeaderTitle": "Sparnamn", - "UIText_SaveIdentifier_StepMessage": "Ange en sparidentifierare, t.ex. 'SpaceStation13 Vulcan'. Använd stor bokstav i början av varje ord. Världstyper finns på Stationeers Wiki -> Dedicated Server.", "UIText_SaveIdentifier_PrimaryPlaceholder": "Kräver ett sparnamn och världstyp vid första start!", - "UIText_SaveIdentifier_PrimaryLabel": "Sparidentifierare", "UIText_SaveIdentifier_SubmitButton": "Spara & fortsätt", "UIText_SaveIdentifier_SkipButton": "Hoppa över", "UIText_MaxPlayers_Title": "Stationeers Server UI", diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.js b/UIMod/onboard_bundled/twoboxform/twoboxform.js index 46b61bc7..a0928335 100644 --- a/UIMod/onboard_bundled/twoboxform/twoboxform.js +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.js @@ -135,6 +135,15 @@ document.addEventListener('DOMContentLoaded', () => { body = JSON.stringify({ [configField]: booleanToConfig(document.getElementById('primary-field').value) }); + + } else if (configField === "SaveInfo") { + const primaryValue = document.getElementById('primary-field').value.trim(); + const secondaryValue = document.getElementById('secondary-field').value.trim(); + const joinedValue = `${primaryValue} ${secondaryValue}`; + console.log(joinedValue); + body = JSON.stringify({ + [configField]: joinedValue + }); } else { body = JSON.stringify({ [configField]: document.getElementById('primary-field').value diff --git a/src/web/TwoBoxForm.go b/src/web/TwoBoxForm.go index 77bdc157..081f2988 100644 --- a/src/web/TwoBoxForm.go +++ b/src/web/TwoBoxForm.go @@ -110,18 +110,19 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { NextStep: "save_identifier", }, "save_identifier": { - ID: "save_identifier", - Title: localization.GetString("UIText_SaveIdentifier_Title"), - HeaderTitle: localization.GetString("UIText_SaveIdentifier_HeaderTitle"), - StepMessage: localization.GetString("UIText_SaveIdentifier_StepMessage"), - PrimaryPlaceholderText: localization.GetString("UIText_SaveIdentifier_PrimaryPlaceholder"), - PrimaryLabel: localization.GetString("UIText_SaveIdentifier_PrimaryLabel"), - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: localization.GetString("UIText_SaveIdentifier_SubmitButton"), - SkipButtonText: localization.GetString("UIText_SaveIdentifier_SkipButton"), - ConfigField: "SaveInfo", - NextStep: "max_players", + ID: "save_identifier", + Title: localization.GetString("UIText_SaveIdentifier_Title"), + HeaderTitle: localization.GetString("UIText_SaveIdentifier_HeaderTitle"), + StepMessage: localization.GetString("UIText_SaveIdentifier_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_SaveIdentifier_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_SaveIdentifier_PrimaryLabel"), + SecondaryLabel: localization.GetString("UIText_SaveIdentifier_SecondaryLabel"), + SecondaryLabelType: "text", + SecondaryPlaceholderText: localization.GetString("UIText_SaveIdentifier_SecondaryPlaceholder"), + SubmitButtonText: localization.GetString("UIText_SaveIdentifier_SubmitButton"), + SkipButtonText: localization.GetString("UIText_SaveIdentifier_SkipButton"), + ConfigField: "SaveInfo", + NextStep: "max_players", }, "max_players": { ID: "max_players", From 2fa16fcd78504a947f66a590ce5e9d3f2793df84 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 15:47:41 +0200 Subject: [PATCH 24/37] added firstpass on worldID dropdown for SaveInfo --- .../twoboxform/twoboxform.html | 21 +++++++++++----- .../onboard_bundled/twoboxform/twoboxform.js | 5 ++++ src/web/TwoBoxForm.go | 24 ++++++++++++------- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.html b/UIMod/onboard_bundled/twoboxform/twoboxform.html index 804494ee..755413cc 100644 --- a/UIMod/onboard_bundled/twoboxform/twoboxform.html +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.html @@ -67,13 +67,22 @@

{{.HeaderTitle}}

{{if ne .SecondaryLabel ""}}
+ {{if eq .SecondaryLabelType "dropdown"}} + + {{else}} + type="{{.SecondaryLabelType}}" + id="secondary-field" + name="secondary-field" + placeholder="{{.SecondaryPlaceholderText}}" + required + > + {{end}}
{{end}} diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.js b/UIMod/onboard_bundled/twoboxform/twoboxform.js index a0928335..de781dda 100644 --- a/UIMod/onboard_bundled/twoboxform/twoboxform.js +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.js @@ -139,6 +139,11 @@ document.addEventListener('DOMContentLoaded', () => { } else if (configField === "SaveInfo") { const primaryValue = document.getElementById('primary-field').value.trim(); const secondaryValue = document.getElementById('secondary-field').value.trim(); + if (secondaryValue === '' || secondaryValue === document.getElementById('secondary-field').placeholder) { + showNotification('Please select a world type!', 'error'); + hidePreloader(); + return; // Prevent submission + } const joinedValue = `${primaryValue} ${secondaryValue}`; console.log(joinedValue); body = JSON.stringify({ diff --git a/src/web/TwoBoxForm.go b/src/web/TwoBoxForm.go index 081f2988..f688a243 100644 --- a/src/web/TwoBoxForm.go +++ b/src/web/TwoBoxForm.go @@ -19,6 +19,7 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { PrimaryLabel string SecondaryLabel string SecondaryLabelType string + SecondaryOptions []string SubmitButtonText string SkipButtonText string PrimaryPlaceholderText string @@ -36,6 +37,7 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { PrimaryLabel string SecondaryLabel string SecondaryLabelType string + SecondaryOptions []string SubmitButtonText string SkipButtonText string Mode string @@ -117,12 +119,20 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { PrimaryPlaceholderText: localization.GetString("UIText_SaveIdentifier_PrimaryPlaceholder"), PrimaryLabel: localization.GetString("UIText_SaveIdentifier_PrimaryLabel"), SecondaryLabel: localization.GetString("UIText_SaveIdentifier_SecondaryLabel"), - SecondaryLabelType: "text", + SecondaryLabelType: "dropdown", SecondaryPlaceholderText: localization.GetString("UIText_SaveIdentifier_SecondaryPlaceholder"), - SubmitButtonText: localization.GetString("UIText_SaveIdentifier_SubmitButton"), - SkipButtonText: localization.GetString("UIText_SaveIdentifier_SkipButton"), - ConfigField: "SaveInfo", - NextStep: "max_players", + SecondaryOptions: []string{ // NEW: Predefined world types (customize as needed) + "Mars", + "Europa", + "Titan", + "Venus", + "Asteroid", + "Moon", + }, + SubmitButtonText: localization.GetString("UIText_SaveIdentifier_SubmitButton"), + SkipButtonText: localization.GetString("UIText_SaveIdentifier_SkipButton"), + ConfigField: "SaveInfo", + NextStep: "max_players", }, "max_players": { ID: "max_players", @@ -311,9 +321,7 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { data.NextStep = step.NextStep data.PrimaryPlaceholderText = step.PrimaryPlaceholderText data.SecondaryPlaceholderText = step.SecondaryPlaceholderText - if stepID == "sscm" { - data.FooterText = localization.GetString("UIText_SSCM_FooterText") - } + data.SecondaryOptions = step.SecondaryOptions } else { // Default to welcome page if step is invalid welcomeStep := steps["welcome"] From f60b1672b178e8c43a43653106bcd1847a401a51 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 16:00:01 +0200 Subject: [PATCH 25/37] Dropdown now shows (eg) "Old Terrain Mars" and uses "Mars2" --- .../twoboxform/twoboxform.html | 2 +- src/web/TwoBoxForm.go | 58 +++++++++++-------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.html b/UIMod/onboard_bundled/twoboxform/twoboxform.html index 755413cc..f328c9cc 100644 --- a/UIMod/onboard_bundled/twoboxform/twoboxform.html +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.html @@ -71,7 +71,7 @@

{{.HeaderTitle}}

{{else}} diff --git a/src/web/TwoBoxForm.go b/src/web/TwoBoxForm.go index f688a243..60059e8b 100644 --- a/src/web/TwoBoxForm.go +++ b/src/web/TwoBoxForm.go @@ -12,14 +12,17 @@ import ( func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { type Step struct { - ID string - Title string - HeaderTitle string - StepMessage string - PrimaryLabel string - SecondaryLabel string - SecondaryLabelType string - SecondaryOptions []string + ID string + Title string + HeaderTitle string + StepMessage string + PrimaryLabel string + SecondaryLabel string + SecondaryLabelType string + SecondaryOptions []struct { + Display string // Text shown in dropdown + Value string // Value sent on submission + } SubmitButtonText string SkipButtonText string PrimaryPlaceholderText string @@ -29,15 +32,18 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { } type TemplateData struct { - IsFirstTimeSetup bool - Path string - Title string - HeaderTitle string - StepMessage string - PrimaryLabel string - SecondaryLabel string - SecondaryLabelType string - SecondaryOptions []string + IsFirstTimeSetup bool + Path string + Title string + HeaderTitle string + StepMessage string + PrimaryLabel string + SecondaryLabel string + SecondaryLabelType string + SecondaryOptions []struct { + Display string + Value string + } SubmitButtonText string SkipButtonText string Mode string @@ -121,13 +127,17 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { SecondaryLabel: localization.GetString("UIText_SaveIdentifier_SecondaryLabel"), SecondaryLabelType: "dropdown", SecondaryPlaceholderText: localization.GetString("UIText_SaveIdentifier_SecondaryPlaceholder"), - SecondaryOptions: []string{ // NEW: Predefined world types (customize as needed) - "Mars", - "Europa", - "Titan", - "Venus", - "Asteroid", - "Moon", + SecondaryOptions: []struct{ Display, Value string }{ + {Display: "Moon", Value: "Moon"}, + {Display: "Vulcan", Value: "Vulcan"}, + {Display: "Venus", Value: "Venus"}, + {Display: "New Terrain Mars", Value: "Mars2"}, + {Display: "New Terrain Europa", Value: "Europa3"}, + {Display: "New Terrain Mimas", Value: "MimasHerschel"}, + {Display: "New Terrain Moon", Value: "Lunar"}, + {Display: "Old Terrain Mars", Value: "Mars"}, + {Display: "Old Terrain Europa", Value: "Europa"}, + {Display: "Old Terrain Mimas", Value: "Mimas"}, }, SubmitButtonText: localization.GetString("UIText_SaveIdentifier_SubmitButton"), SkipButtonText: localization.GetString("UIText_SaveIdentifier_SkipButton"), From 6805256df36042822381c3ba35822b5b841251e9 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 16:17:07 +0200 Subject: [PATCH 26/37] 2bxform now auto-selects available world types based on GetIsNewTerrainAndSaveSystem. Changed step order accordingly. --- UIMod/onboard_bundled/localization/de-DE.json | 9 +- UIMod/onboard_bundled/localization/en-US.json | 2 +- UIMod/onboard_bundled/localization/sv-SE.json | 11 +- src/web/TwoBoxForm.go | 108 ++++++++++-------- 4 files changed, 64 insertions(+), 66 deletions(-) diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/UIMod/onboard_bundled/localization/de-DE.json index c82efe09..8420f6af 100644 --- a/UIMod/onboard_bundled/localization/de-DE.json +++ b/UIMod/onboard_bundled/localization/de-DE.json @@ -172,7 +172,7 @@ "UIText_NewTerrainAndSaveSystem_Title": "Wichtig", "UIText_NewTerrainAndSaveSystem_HeaderTitle": "Terrainsystem wählen", "UIText_NewTerrainAndSaveSystem_StepMessage": "Gerade zu Beta gewechselt? Terrain- und Speichersystem umschalten! 'ja' eingeben zum Aktivieren oder 'nein' zum Deaktivieren.", - "UIText_NewTerrainAndSaveSystem_PrimaryPlaceholder": "ja/nein", + "UIText_NewTerrainAndSaveSystem_PrimaryPlaceholder": "nein", "UIText_NewTerrainAndSaveSystem_PrimaryLabel": "Neues System aktivieren", "UIText_NewTerrainAndSaveSystem_SubmitButton": "Speichern & Weiter", "UIText_NewTerrainAndSaveSystem_SkipButton": "Überspringen", @@ -220,13 +220,6 @@ "UIText_AdminAccount_SecondaryPlaceholder": "Passwort", "UIText_AdminAccount_SubmitButton": "Speichern & Weiter", "UIText_AdminAccount_SkipButton": "Authentifizierung Überspringen", - "UIText_SSCM_Title": "Stationeers Server Command Manager", - "UIText_SSCM_HeaderTitle": "Einzigartige Funktion", - "UIText_SSCM_StepMessage": "SSCM ist ein maßgeschneidertes Server-Plugin für direkte Serverbefehl-Ausführung aus SSUI. Es ermöglicht Befehle aus der Web-Konsole ohne Störung des Vanilla-Verhaltens, keine Client-seitigen Mods nötig.", - "UIText_SSCM_PrimaryPlaceholder": "'nein' eingeben zum Deaktivieren", - "UIText_SSCM_PrimaryLabel": "Deaktivierung NICHT empfohlen.", - "UIText_SSCM_SubmitButton": "Weiter", - "UIText_SSCM_SkipButton": "Aktiviert lassen", "UIText_Finalize_Title": "Das wars!", "UIText_Finalize_HeaderTitle": "Setup Abschließen", "UIText_Finalize_StepMessage": "Bereit zum Abschließen? Deine Konfiguration wurde bereits während des Setups gespeichert. Für Änderungen klicke 'Zurück zum Start' und überspringe was behalten werden soll. Meiste Optionen auch im Config Tab änderbar.", diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index 6ca79be1..34b8f5c9 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -172,7 +172,7 @@ "UIText_NewTerrainAndSaveSystem_Title": "IMPORTANT", "UIText_NewTerrainAndSaveSystem_HeaderTitle": "Select Terrain System", "UIText_NewTerrainAndSaveSystem_StepMessage": "Just switched to Beta? If yes, enable handling of the new terrain and save system here. Enter 'yes' to enable or 'no' to use the old system.", - "UIText_NewTerrainAndSaveSystem_PrimaryPlaceholder": "yes/no", + "UIText_NewTerrainAndSaveSystem_PrimaryPlaceholder": "no", "UIText_NewTerrainAndSaveSystem_PrimaryLabel": "Enable new System", "UIText_NewTerrainAndSaveSystem_SubmitButton": "Save & Continue", "UIText_NewTerrainAndSaveSystem_SkipButton": "Skip", diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/UIMod/onboard_bundled/localization/sv-SE.json index b617e1ed..6ecf8c94 100644 --- a/UIMod/onboard_bundled/localization/sv-SE.json +++ b/UIMod/onboard_bundled/localization/sv-SE.json @@ -171,7 +171,7 @@ "UIText_NewTerrainAndSaveSystem_Title": "Viktigt", "UIText_NewTerrainAndSaveSystem_HeaderTitle": "Välj terrängsystem", "UIText_NewTerrainAndSaveSystem_StepMessage": "Bytt till beta? Aktivera terräng- och sparsystem för att stödja det! Ange 'ja' för att aktivera eller 'nej' för att inaktivera.", - "UIText_NewTerrainAndSaveSystem_PrimaryPlaceholder": "yes/no", + "UIText_NewTerrainAndSaveSystem_PrimaryPlaceholder": "nej", "UIText_NewTerrainAndSaveSystem_PrimaryLabel": "Aktivera nytt system", "UIText_NewTerrainAndSaveSystem_SubmitButton": "Spara & fortsätt", "UIText_NewTerrainAndSaveSystem_SkipButton": "Hoppa över", @@ -199,7 +199,7 @@ "UIText_UPnPEnabled_Title": "Stationeers Server UI", "UIText_UPnPEnabled_HeaderTitle": "Nätverk (3/4)", "UIText_UPnPEnabled_StepMessage": "Aktivera UPnP? Ange 'ja' för att aktivera eller 'nej' för att inaktivera.", - "UIText_UPnPEnabled_PrimaryPlaceholder": "yes/no", + "UIText_UPnPEnabled_PrimaryPlaceholder": "ja/nej", "UIText_UPnPEnabled_PrimaryLabel": "Aktivera UPnP", "UIText_UPnPEnabled_SubmitButton": "Spara & fortsätt", "UIText_UPnPEnabled_SkipButton": "Hoppa över", @@ -219,13 +219,6 @@ "UIText_AdminAccount_SecondaryPlaceholder": "Lösenord", "UIText_AdminAccount_SubmitButton": "Spara & fortsätt", "UIText_AdminAccount_SkipButton": "Hoppa över autentisering", - "UIText_SSCM_Title": "Stationeers Server Command Manager", - "UIText_SSCM_HeaderTitle": "Unik funktion", - "UIText_SSCM_StepMessage": "SSCM är ett anpassat serverplugin som låter dig köra serverkommandon direkt från SSUI. Det ger dig möjlighet att köra kommandon från webbkonsolen utan att störa vanliga funktioner, inga klientsidemoddar behövs.", - "UIText_SSCM_PrimaryPlaceholder": "skriv 'nej' för att inaktivera", - "UIText_SSCM_PrimaryLabel": "Att välja bort rekommenderas INTE.", - "UIText_SSCM_SubmitButton": "Fortsätt", - "UIText_SSCM_SkipButton": "Behåll aktiverad", "UIText_Finalize_Title": "Det var allt", "UIText_Finalize_HeaderTitle": "Slutför konfiguration", "UIText_Finalize_StepMessage": "Redo att slutföra? Din konfiguration har redan sparats under guiden. Om du vill ändra inställningar kan du klicka på Gå tillbaka till start och hoppa över det du vill behålla. De flesta inställningar kan också ändras i konfigurationsfliken i gränssnittet.", diff --git a/src/web/TwoBoxForm.go b/src/web/TwoBoxForm.go index 60059e8b..403daade 100644 --- a/src/web/TwoBoxForm.go +++ b/src/web/TwoBoxForm.go @@ -77,6 +77,29 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { stepID = "welcome" // Start with welcome page for first-time setup } + var worldOptions = []struct{ Display, Value string }{ + {Display: "Moon", Value: "Moon"}, + {Display: "Vulcan", Value: "Vulcan"}, + {Display: "Venus", Value: "Venus"}, + {Display: "Mars", Value: "Mars"}, + {Display: "Europa", Value: "Europa"}, + {Display: "Mimas", Value: "Mimas"}, + } + + if config.GetIsNewTerrainAndSaveSystem() { + worldOptions = []struct{ Display, Value string }{ + {Display: "Lunar", Value: "Lunar"}, + {Display: "Vulcan", Value: "Vulcan"}, + {Display: "Venus", Value: "Venus"}, + {Display: "Mars", Value: "Mars2"}, + {Display: "Europa", Value: "Europa3"}, + {Display: "Mimas Herschel", Value: "MimasHerschel"}, + {Display: "Mars", Value: "Mars"}, + {Display: "Europa", Value: "Europa"}, + {Display: "Mimas", Value: "Mimas"}, + } + } + // Define all steps in a map for easy access and modification steps := map[string]Step{ "welcome": { @@ -101,7 +124,35 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { SecondaryLabelType: "hidden", SubmitButtonText: localization.GetString("UIText_PlsRead_SubmitButton"), SkipButtonText: localization.GetString("UIText_PlsRead_SkipButton"), - NextStep: "server_name", + NextStep: "game_branch", + }, + "game_branch": { + ID: "game_branch", + Title: localization.GetString("UIText_GameBranch_Title"), + HeaderTitle: localization.GetString("UIText_GameBranch_HeaderTitle"), + StepMessage: localization.GetString("UIText_GameBranch_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_GameBranch_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_GameBranch_PrimaryLabel"), + SecondaryLabel: "", + SecondaryLabelType: "hidden", + SubmitButtonText: localization.GetString("UIText_GameBranch_SubmitButton"), + SkipButtonText: localization.GetString("UIText_GameBranch_SkipButton"), + ConfigField: "gameBranch", + NextStep: "newterrain_and_savesystem", + }, + "newterrain_and_savesystem": { + ID: "newterrain_and_savesystem", + Title: localization.GetString("UIText_NewTerrainAndSaveSystem_Title"), + HeaderTitle: localization.GetString("UIText_NewTerrainAndSaveSystem_HeaderTitle"), + StepMessage: localization.GetString("UIText_NewTerrainAndSaveSystem_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_NewTerrainAndSaveSystem_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_NewTerrainAndSaveSystem_PrimaryLabel"), + SecondaryLabel: "", + SecondaryLabelType: "hidden", + SubmitButtonText: localization.GetString("UIText_NewTerrainAndSaveSystem_SubmitButton"), + SkipButtonText: localization.GetString("UIText_NewTerrainAndSaveSystem_SkipButton"), + ConfigField: "IsNewTerrainAndSaveSystem", + NextStep: "server_name", }, "server_name": { ID: "server_name", @@ -127,22 +178,11 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { SecondaryLabel: localization.GetString("UIText_SaveIdentifier_SecondaryLabel"), SecondaryLabelType: "dropdown", SecondaryPlaceholderText: localization.GetString("UIText_SaveIdentifier_SecondaryPlaceholder"), - SecondaryOptions: []struct{ Display, Value string }{ - {Display: "Moon", Value: "Moon"}, - {Display: "Vulcan", Value: "Vulcan"}, - {Display: "Venus", Value: "Venus"}, - {Display: "New Terrain Mars", Value: "Mars2"}, - {Display: "New Terrain Europa", Value: "Europa3"}, - {Display: "New Terrain Mimas", Value: "MimasHerschel"}, - {Display: "New Terrain Moon", Value: "Lunar"}, - {Display: "Old Terrain Mars", Value: "Mars"}, - {Display: "Old Terrain Europa", Value: "Europa"}, - {Display: "Old Terrain Mimas", Value: "Mimas"}, - }, - SubmitButtonText: localization.GetString("UIText_SaveIdentifier_SubmitButton"), - SkipButtonText: localization.GetString("UIText_SaveIdentifier_SkipButton"), - ConfigField: "SaveInfo", - NextStep: "max_players", + SecondaryOptions: worldOptions, + SubmitButtonText: localization.GetString("UIText_SaveIdentifier_SubmitButton"), + SkipButtonText: localization.GetString("UIText_SaveIdentifier_SkipButton"), + ConfigField: "SaveInfo", + NextStep: "max_players", }, "max_players": { ID: "max_players", @@ -170,34 +210,6 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { SubmitButtonText: localization.GetString("UIText_ServerPassword_SubmitButton"), SkipButtonText: localization.GetString("UIText_ServerPassword_SkipButton"), ConfigField: "ServerPassword", - NextStep: "game_branch", - }, - "game_branch": { - ID: "game_branch", - Title: localization.GetString("UIText_GameBranch_Title"), - HeaderTitle: localization.GetString("UIText_GameBranch_HeaderTitle"), - StepMessage: localization.GetString("UIText_GameBranch_StepMessage"), - PrimaryPlaceholderText: localization.GetString("UIText_GameBranch_PrimaryPlaceholder"), - PrimaryLabel: localization.GetString("UIText_GameBranch_PrimaryLabel"), - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: localization.GetString("UIText_GameBranch_SubmitButton"), - SkipButtonText: localization.GetString("UIText_GameBranch_SkipButton"), - ConfigField: "gameBranch", - NextStep: "newterrain_and_savesystem", - }, - "newterrain_and_savesystem": { - ID: "newterrain_and_savesystem", - Title: localization.GetString("UIText_NewTerrainAndSaveSystem_Title"), - HeaderTitle: localization.GetString("UIText_NewTerrainAndSaveSystem_HeaderTitle"), - StepMessage: localization.GetString("UIText_NewTerrainAndSaveSystem_StepMessage"), - PrimaryPlaceholderText: localization.GetString("UIText_NewTerrainAndSaveSystem_PrimaryPlaceholder"), - PrimaryLabel: localization.GetString("UIText_NewTerrainAndSaveSystem_PrimaryLabel"), - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: localization.GetString("UIText_NewTerrainAndSaveSystem_SubmitButton"), - SkipButtonText: localization.GetString("UIText_NewTerrainAndSaveSystem_SkipButton"), - ConfigField: "IsNewTerrainAndSaveSystem", NextStep: "network_config_choice", }, "network_config_choice": { @@ -348,12 +360,12 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { data.Step = "welcome" } stepOrder := []string{ - "welcome", "pls_read", "server_name", "save_identifier", "max_players", - "server_password", "game_branch", "newterrain_and_savesystem", + "welcome", "pls_read", "game_branch", "newterrain_and_savesystem", "server_name", "save_identifier", "max_players", + "server_password", "discord_enabled", "discord_token", "control_panel_channel", "save_channel", "log_channel", "connection_list_channel", "status_channel", "control_channel", "network_config_choice", "game_port", "update_port", "upnp_enabled", - "local_ip_address", "admin_account", "sscm", "finalize", + "local_ip_address", "admin_account", "finalize", } var stepSlice []Step for _, id := range stepOrder { From fc99da490c8bfe5fa02d4988c3cd47cc23491010 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 16:27:37 +0200 Subject: [PATCH 27/37] added some css for 2bxform, dropdown and mobile style (none) for progress bar --- .../onboard_bundled/twoboxform/twoboxform.css | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.css b/UIMod/onboard_bundled/twoboxform/twoboxform.css index 3372918d..6d50ca7a 100644 --- a/UIMod/onboard_bundled/twoboxform/twoboxform.css +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.css @@ -482,6 +482,17 @@ footer { h1 { font-size: 1.2rem; } + +} +@media (max-width: 800px) { + .progress-bar { + display: none !important; + } +} +@media (max-height: 1050px) { + .progress-bar { + display: none !important; + } } .progress-bar { @@ -549,4 +560,41 @@ footer { z-index: 100; display: flex; justify-content: center; +} + +select { + width: 100%; + padding: 12px; + background-color: rgba(0, 0, 0, 0.6); + color: var(--primary); + border: 2px solid var(--primary-dim); + border-radius: 4px; + font-family: 'Share Tech Mono', monospace; + transition: all 0.3s ease; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position: right 12px top 50%; +} + +select:focus { + border-color: var(--primary); + box-shadow: 0 0 20px var(--primary-glow); + outline: none; + transform: scale(1.02); +} + +option { + background-color: var(--bg-panel); + color: var(--primary); + padding: 10px; + font-family: 'Share Tech Mono', monospace; + font-size: 1rem; +} + +option:hover { + background-color: var(--primary-dim); + color: var(--text-bright); } \ No newline at end of file From dc8527292256e88362f08743ebca1d606137a602 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 16:53:25 +0200 Subject: [PATCH 28/37] updated / added localization for reworked save_identifier step --- UIMod/onboard_bundled/localization/de-DE.json | 8 ++++++-- UIMod/onboard_bundled/localization/en-US.json | 7 +++++-- UIMod/onboard_bundled/localization/sv-SE.json | 8 ++++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/UIMod/onboard_bundled/localization/de-DE.json index 8420f6af..04f264d6 100644 --- a/UIMod/onboard_bundled/localization/de-DE.json +++ b/UIMod/onboard_bundled/localization/de-DE.json @@ -144,8 +144,12 @@ "UIText_ServerName_SubmitButton": "Speichern & Weiter", "UIText_ServerName_SkipButton": "Überspringen", "UIText_SaveIdentifier_Title": "Stationeers Server UI", - "UIText_SaveIdentifier_HeaderTitle": "Speicher-Identifikator", - "UIText_SaveIdentifier_PrimaryPlaceholder": "Benötigt SaveName und WorldType für ersten Start!", + "UIText_SaveIdentifier_HeaderTitle": "Speicherstand", + "UIText_SaveIdentifier_StepMessage": "Konfiguriere unten den Namen deines Speicherstands und den Welttyp.", + "UIText_SaveIdentifier_PrimaryLabel": "Name deiner Karte", + "UIText_SaveIdentifier_PrimaryPlaceholder": "MeineStationeersKarte", + "UIText_SaveIdentifier_SecondaryLabel": "Stationeers-Welttyp", + "UIText_SaveIdentifier_SecondaryPlaceholder": "Klicke, um einen Welttyp auszuwählen", "UIText_SaveIdentifier_SubmitButton": "Speichern & Weiter", "UIText_SaveIdentifier_SkipButton": "Überspringen", "UIText_MaxPlayers_Title": "Stationeers Server UI", diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index 34b8f5c9..78f3e2a9 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -144,8 +144,11 @@ "UIText_ServerName_SkipButton": "Skip", "UIText_SaveIdentifier_Title": "Stationeers Server UI", "UIText_SaveIdentifier_HeaderTitle": "Save Identifier", - "UIText_SaveIdentifier_StepMessage": "World types: Moon, Mars, Europa, Mimas, Vulcan, Space, Venus / Beta: Mars2, Europa3, MimasHerschel, Vulcan, Venus, Lunar", - "UIText_SaveIdentifier_PrimaryPlaceholder": "Requires a SaveName and WorldType for first start!", + "UIText_SaveIdentifier_StepMessage": "Configue your savegame name and world type from the options below.", + "UIText_SaveIdentifier_PrimaryLabel": "Your Map Name", + "UIText_SaveIdentifier_PrimaryPlaceholder": "MyStationeersMap", + "UIText_SaveIdentifier_SecondaryLabel": "Stationeers World Type", + "UIText_SaveIdentifier_SecondaryPlaceholder": "Click to select a world type", "UIText_SaveIdentifier_SubmitButton": "Save & Continue", "UIText_SaveIdentifier_SkipButton": "Skip", "UIText_MaxPlayers_Title": "Stationeers Server UI", diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/UIMod/onboard_bundled/localization/sv-SE.json index 6ecf8c94..ab4d30b3 100644 --- a/UIMod/onboard_bundled/localization/sv-SE.json +++ b/UIMod/onboard_bundled/localization/sv-SE.json @@ -143,8 +143,12 @@ "UIText_ServerName_SubmitButton": "Spara & fortsätt", "UIText_ServerName_SkipButton": "Hoppa över", "UIText_SaveIdentifier_Title": "Stationeers Server UI", - "UIText_SaveIdentifier_HeaderTitle": "Sparnamn", - "UIText_SaveIdentifier_PrimaryPlaceholder": "Kräver ett sparnamn och världstyp vid första start!", + "UIText_SaveIdentifier_HeaderTitle": "Sparfil", + "UIText_SaveIdentifier_StepMessage": "Ange ett namn för ditt spel och världstyp från alternativen nedan.", + "UIText_SaveIdentifier_PrimaryLabel": "Namn på din karta", + "UIText_SaveIdentifier_PrimaryPlaceholder": "MinStationeersKarta", + "UIText_SaveIdentifier_SecondaryLabel": "Stationeers världstyp", + "UIText_SaveIdentifier_SecondaryPlaceholder": "Klicka för att välja en världstyp", "UIText_SaveIdentifier_SubmitButton": "Spara & fortsätt", "UIText_SaveIdentifier_SkipButton": "Hoppa över", "UIText_MaxPlayers_Title": "Stationeers Server UI", From f9f8d9b60f8a3b455975ab7bec1d1244c080e852 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 16:53:53 +0200 Subject: [PATCH 29/37] removed scale transformation on 2bxform select focus --- UIMod/onboard_bundled/twoboxform/twoboxform.css | 1 - 1 file changed, 1 deletion(-) diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.css b/UIMod/onboard_bundled/twoboxform/twoboxform.css index 6d50ca7a..0e85c2d3 100644 --- a/UIMod/onboard_bundled/twoboxform/twoboxform.css +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.css @@ -583,7 +583,6 @@ select:focus { border-color: var(--primary); box-shadow: 0 0 20px var(--primary-glow); outline: none; - transform: scale(1.02); } option { From b1c6ce89a9a5b7479c7a1d854ca6205da52054da Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 17:21:51 +0200 Subject: [PATCH 30/37] updated game branch selection to also use new dropdown style --- UIMod/onboard_bundled/localization/de-DE.json | 6 ++-- UIMod/onboard_bundled/localization/en-US.json | 4 +-- UIMod/onboard_bundled/localization/sv-SE.json | 6 ++-- .../onboard_bundled/twoboxform/twoboxform.js | 11 ++++++- src/web/TwoBoxForm.go | 32 ++++++++++++------- 5 files changed, 38 insertions(+), 21 deletions(-) diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/UIMod/onboard_bundled/localization/de-DE.json index 04f264d6..d8286cab 100644 --- a/UIMod/onboard_bundled/localization/de-DE.json +++ b/UIMod/onboard_bundled/localization/de-DE.json @@ -168,9 +168,9 @@ "UIText_ServerPassword_SkipButton": "Überspringen", "UIText_GameBranch_Title": "Stationeers Server UI", "UIText_GameBranch_HeaderTitle": "Spiel Branch", - "UIText_GameBranch_StepMessage": "Gib einen Beta-Branch ein oder überspringe für Release-Version. Bei Branch-Wechsel SSUI nach Assistenten n e u s t a r t e n.", - "UIText_GameBranch_PrimaryPlaceholder": "beta", - "UIText_GameBranch_PrimaryLabel": "Spiel Branch", + "UIText_GameBranch_StepMessage": "Gib einen Beta-Branch ein oder überspringe für Release-Version. Wenn Sie den Zweig wechseln, klicken Sie nach Abschluss dieses Assistenten unbedingt auf „Server aktualisieren“ im Haupt-Dashboard.", + "UIText_GameBranch_SecondaryPlaceholder": "Wähle einen Zweig", + "UIText_GameBranch_SecondaryLabel": "Spielzweig", "UIText_GameBranch_SubmitButton": "Speichern & Weiter", "UIText_GameBranch_SkipButton": "Release Version nutzen", "UIText_NewTerrainAndSaveSystem_Title": "Wichtig", diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index 78f3e2a9..372d0978 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -168,8 +168,8 @@ "UIText_GameBranch_Title": "Stationeers Server UI", "UIText_GameBranch_HeaderTitle": "Game Branch", "UIText_GameBranch_StepMessage": "Enter a beta branch or skip this to use the release version. If switching branches, make sure to click Update Server on the Main dashboard after completing this wizzard.", - "UIText_GameBranch_PrimaryPlaceholder": "beta", - "UIText_GameBranch_PrimaryLabel": "Game Branch", + "UIText_GameBranch_SecondaryPlaceholder": "Select a branch", + "UIText_GameBranch_SecondaryLabel": "Game Branch", "UIText_GameBranch_SubmitButton": "Save & Continue", "UIText_GameBranch_SkipButton": "Use Normal Version", "UIText_NewTerrainAndSaveSystem_Title": "IMPORTANT", diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/UIMod/onboard_bundled/localization/sv-SE.json index ab4d30b3..de3346d2 100644 --- a/UIMod/onboard_bundled/localization/sv-SE.json +++ b/UIMod/onboard_bundled/localization/sv-SE.json @@ -167,9 +167,9 @@ "UIText_ServerPassword_SkipButton": "Hoppa över", "UIText_GameBranch_Title": "Stationeers Server UI", "UIText_GameBranch_HeaderTitle": "Spel-branch", - "UIText_GameBranch_StepMessage": "Ange en betagren eller hoppa över för att använda standardversionen. Vid byte av gren, starta om SSUI efter guiden.", - "UIText_GameBranch_PrimaryPlaceholder": "beta", - "UIText_GameBranch_PrimaryLabel": "Spelgren", + "UIText_GameBranch_StepMessage": "Ange en betagren eller hoppa över för att använda standardversionen. Om du byter grenar, se till att klicka på Uppdatera server på huvudpanelen efter att du har slutfört den här guiden.", + "UIText_GameBranch_SecondaryPlaceholder": "Välj en gren", + "UIText_GameBranch_SecondaryLabel": "Spelgren", "UIText_GameBranch_SubmitButton": "Spara & fortsätt", "UIText_GameBranch_SkipButton": "Använd standardversion", "UIText_NewTerrainAndSaveSystem_Title": "Viktigt", diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.js b/UIMod/onboard_bundled/twoboxform/twoboxform.js index de781dda..4add1e9a 100644 --- a/UIMod/onboard_bundled/twoboxform/twoboxform.js +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.js @@ -145,10 +145,19 @@ document.addEventListener('DOMContentLoaded', () => { return; // Prevent submission } const joinedValue = `${primaryValue} ${secondaryValue}`; - console.log(joinedValue); body = JSON.stringify({ [configField]: joinedValue }); + } else if (configField === "gameBranch") { + const secondaryValue = document.getElementById('secondary-field').value.trim(); + if (secondaryValue === '' || secondaryValue === document.getElementById('secondary-field').placeholder) { + showNotification('Please select a world type!', 'error'); + hidePreloader(); + return; // Prevent submission + } + body = JSON.stringify({ + [configField]: secondaryValue + }); } else { body = JSON.stringify({ [configField]: document.getElementById('primary-field').value diff --git a/src/web/TwoBoxForm.go b/src/web/TwoBoxForm.go index 403daade..59ec24a7 100644 --- a/src/web/TwoBoxForm.go +++ b/src/web/TwoBoxForm.go @@ -77,6 +77,14 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { stepID = "welcome" // Start with welcome page for first-time setup } + var gameBranchOptions = []struct{ Display, Value string }{ + {Display: "Stable branch (default)", Value: "public"}, + {Display: "Beta branch", Value: "beta"}, + {Display: "Pre-terrain rework update", Value: "preterrain"}, + {Display: "Pre-rocket refactor update", Value: "prerocket"}, + {Display: "Version before the latest update", Value: "previous"}, + } + var worldOptions = []struct{ Display, Value string }{ {Display: "Moon", Value: "Moon"}, {Display: "Vulcan", Value: "Vulcan"}, @@ -127,18 +135,18 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { NextStep: "game_branch", }, "game_branch": { - ID: "game_branch", - Title: localization.GetString("UIText_GameBranch_Title"), - HeaderTitle: localization.GetString("UIText_GameBranch_HeaderTitle"), - StepMessage: localization.GetString("UIText_GameBranch_StepMessage"), - PrimaryPlaceholderText: localization.GetString("UIText_GameBranch_PrimaryPlaceholder"), - PrimaryLabel: localization.GetString("UIText_GameBranch_PrimaryLabel"), - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: localization.GetString("UIText_GameBranch_SubmitButton"), - SkipButtonText: localization.GetString("UIText_GameBranch_SkipButton"), - ConfigField: "gameBranch", - NextStep: "newterrain_and_savesystem", + ID: "game_branch", + Title: localization.GetString("UIText_GameBranch_Title"), + HeaderTitle: localization.GetString("UIText_GameBranch_HeaderTitle"), + StepMessage: localization.GetString("UIText_GameBranch_StepMessage"), + SecondaryPlaceholderText: localization.GetString("UIText_GameBranch_SecondaryPlaceholder"), + SecondaryLabel: localization.GetString("UIText_GameBranch_SecondaryLabel"), + SecondaryLabelType: "dropdown", + SecondaryOptions: gameBranchOptions, + SubmitButtonText: localization.GetString("UIText_GameBranch_SubmitButton"), + SkipButtonText: localization.GetString("UIText_GameBranch_SkipButton"), + ConfigField: "gameBranch", + NextStep: "newterrain_and_savesystem", }, "newterrain_and_savesystem": { ID: "newterrain_and_savesystem", From 9e40650ebaeaa682d747a2791de3d64b705c4e98 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 17:44:50 +0200 Subject: [PATCH 31/37] enhance localization for save file configuration and add wizard button --- UIMod/onboard_bundled/localization/en-US.json | 3 +- UIMod/onboard_bundled/ui/config.html | 1 + src/web/configpage.go | 115 +++++++++--------- src/web/templatevars.go | 115 +++++++++--------- 4 files changed, 119 insertions(+), 115 deletions(-) diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index 372d0978..e282a291 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -32,7 +32,8 @@ "UIText_ServerName": "Server Name", "UIText_ServerNameInfo": "Name displayed in server list", "UIText_SaveFileName": "Save File Name", - "UIText_SaveFileNameInfo": "Name of save folder. Must be capitalized. To create a new world, provide the World type to generate. (MyVulcanMap Vulcan) Possible World types: Moon, Mars, Europa, Mimas, Vulcan, Space, Venus -- BETA BRANCH: Mars2, Europa3, MimasHerschel, Vulcan, Venus, Lunar", + "UIText_SaveFileNameInfo": "Name of save folder. Must be capitalized. To create a new world, provide the World type to generate. It is recommended to use the Wizard to configure this value correctly. ", + "UIText_SaveFileNameUseWizzardButtonText": "Open Wizard", "UIText_MaxPlayers": "Max Players", "UIText_MaxPlayersInfo": "Maximum number of players allowed", "UIText_ServerPassword": "Server Password", diff --git a/UIMod/onboard_bundled/ui/config.html b/UIMod/onboard_bundled/ui/config.html index 0b27d486..3ceef7a3 100644 --- a/UIMod/onboard_bundled/ui/config.html +++ b/UIMod/onboard_bundled/ui/config.html @@ -80,6 +80,7 @@

{{.UIText_BasicServerSettings}}

{{.UIText_SaveFileNameInfo}}
+
{{.UIText_SaveFileNameUseWizzardButtonText}}
diff --git a/src/web/configpage.go b/src/web/configpage.go index 152f6a91..225ab365 100644 --- a/src/web/configpage.go +++ b/src/web/configpage.go @@ -180,63 +180,64 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { UIText_BetaSettings: localization.GetString("UIText_BetaSettings"), UIText_BasicServerSettings: localization.GetString("UIText_BasicServerSettings"), - UIText_ServerName: localization.GetString("UIText_ServerName"), - UIText_ServerNameInfo: localization.GetString("UIText_ServerNameInfo"), - UIText_SaveFileName: localization.GetString("UIText_SaveFileName"), - UIText_SaveFileNameInfo: localization.GetString("UIText_SaveFileNameInfo"), - UIText_MaxPlayers: localization.GetString("UIText_MaxPlayers"), - UIText_MaxPlayersInfo: localization.GetString("UIText_MaxPlayersInfo"), - UIText_ServerPassword: localization.GetString("UIText_ServerPassword"), - UIText_ServerPasswordInfo: localization.GetString("UIText_ServerPasswordInfo"), - UIText_AdminPassword: localization.GetString("UIText_AdminPassword"), - UIText_AdminPasswordInfo: localization.GetString("UIText_AdminPasswordInfo"), - UIText_AutoSave: localization.GetString("UIText_AutoSave"), - UIText_AutoSaveInfo: localization.GetString("UIText_AutoSaveInfo"), - UIText_SaveInterval: localization.GetString("UIText_SaveInterval"), - UIText_SaveIntervalInfo: localization.GetString("UIText_SaveIntervalInfo"), - UIText_AutoPauseServer: localization.GetString("UIText_AutoPauseServer"), - UIText_AutoPauseServerInfo: localization.GetString("UIText_AutoPauseServerInfo"), - UIText_NetworkConfiguration: localization.GetString("UIText_NetworkConfiguration"), - UIText_GamePort: localization.GetString("UIText_GamePort"), - UIText_GamePortInfo: localization.GetString("UIText_GamePortInfo"), - UIText_UpdatePort: localization.GetString("UIText_UpdatePort"), - UIText_UpdatePortInfo: localization.GetString("UIText_UpdatePortInfo"), - UIText_UPNPEnabled: localization.GetString("UIText_UPNPEnabled"), - UIText_UPNPEnabledInfo: localization.GetString("UIText_UPNPEnabledInfo"), - UIText_LocalIpAddress: localization.GetString("UIText_LocalIpAddress"), - UIText_LocalIpAddressInfo: localization.GetString("UIText_LocalIpAddressInfo"), - UIText_StartLocalHost: localization.GetString("UIText_StartLocalHost"), - UIText_StartLocalHostInfo: localization.GetString("UIText_StartLocalHostInfo"), - UIText_ServerVisible: localization.GetString("UIText_ServerVisible"), - UIText_ServerVisibleInfo: localization.GetString("UIText_ServerVisibleInfo"), - UIText_UseSteamP2P: localization.GetString("UIText_UseSteamP2P"), - UIText_UseSteamP2PInfo: localization.GetString("UIText_UseSteamP2PInfo"), - UIText_AdvancedConfiguration: localization.GetString("UIText_AdvancedConfiguration"), - UIText_ServerAuthSecret: localization.GetString("UIText_ServerAuthSecret"), - UIText_ServerAuthSecretInfo: localization.GetString("UIText_ServerAuthSecretInfo"), - UIText_ServerExePath: localization.GetString("UIText_ServerExePath"), - UIText_ServerExePathInfo: localization.GetString("UIText_ServerExePathInfo"), - UIText_ServerExePathInfo2: localization.GetString("UIText_ServerExePathInfo2"), - UIText_AdditionalParams: localization.GetString("UIText_AdditionalParams"), - UIText_AdditionalParamsInfo: localization.GetString("UIText_AdditionalParamsInfo"), - UIText_AutoRestartServerTimer: localization.GetString("UIText_AutoRestartServerTimer"), - UIText_AutoRestartServerTimerInfo: localization.GetString("UIText_AutoRestartServerTimerInfo"), - UIText_GameBranch: localization.GetString("UIText_GameBranch"), - UIText_GameBranchInfo: localization.GetString("UIText_GameBranchInfo"), - UIText_BetaOnlySettings: localization.GetString("UIText_BetaOnlySettings"), - UIText_BetaWarning: localization.GetString("UIText_BetaWarning"), - UIText_UseNewTerrainAndSave: localization.GetString("UIText_UseNewTerrainAndSave"), - UIText_UseNewTerrainAndSaveInfo: localization.GetString("UIText_UseNewTerrainAndSaveInfo"), - UIText_Difficulty: localization.GetString("UIText_Difficulty"), - UIText_DifficultyInfo: localization.GetString("UIText_DifficultyInfo"), - UIText_StartCondition: localization.GetString("UIText_StartCondition"), - UIText_StartConditionInfo: localization.GetString("UIText_StartConditionInfo"), - UIText_StartLocation: localization.GetString("UIText_StartLocation"), - UIText_StartLocationInfo: localization.GetString("UIText_StartLocationInfo"), - UIText_AutoStartServerOnStartup: localization.GetString("UIText_AutoStartServerOnStartup"), - UIText_AutoStartServerOnStartupInfo: localization.GetString("UIText_AutoStartServerOnStartupInfo"), - UIText_AllowAutoGameServerUpdates: localization.GetString("UIText_AllowAutoGameServerUpdates"), - UIText_AllowAutoGameServerUpdatesInfo: localization.GetString("UIText_AllowAutoGameServerUpdatesInfo"), + UIText_ServerName: localization.GetString("UIText_ServerName"), + UIText_ServerNameInfo: localization.GetString("UIText_ServerNameInfo"), + UIText_SaveFileName: localization.GetString("UIText_SaveFileName"), + UIText_SaveFileNameInfo: localization.GetString("UIText_SaveFileNameInfo"), + UIText_SaveFileNameUseWizzardButtonText: localization.GetString("UIText_SaveFileNameUseWizzardButtonText"), + UIText_MaxPlayers: localization.GetString("UIText_MaxPlayers"), + UIText_MaxPlayersInfo: localization.GetString("UIText_MaxPlayersInfo"), + UIText_ServerPassword: localization.GetString("UIText_ServerPassword"), + UIText_ServerPasswordInfo: localization.GetString("UIText_ServerPasswordInfo"), + UIText_AdminPassword: localization.GetString("UIText_AdminPassword"), + UIText_AdminPasswordInfo: localization.GetString("UIText_AdminPasswordInfo"), + UIText_AutoSave: localization.GetString("UIText_AutoSave"), + UIText_AutoSaveInfo: localization.GetString("UIText_AutoSaveInfo"), + UIText_SaveInterval: localization.GetString("UIText_SaveInterval"), + UIText_SaveIntervalInfo: localization.GetString("UIText_SaveIntervalInfo"), + UIText_AutoPauseServer: localization.GetString("UIText_AutoPauseServer"), + UIText_AutoPauseServerInfo: localization.GetString("UIText_AutoPauseServerInfo"), + UIText_NetworkConfiguration: localization.GetString("UIText_NetworkConfiguration"), + UIText_GamePort: localization.GetString("UIText_GamePort"), + UIText_GamePortInfo: localization.GetString("UIText_GamePortInfo"), + UIText_UpdatePort: localization.GetString("UIText_UpdatePort"), + UIText_UpdatePortInfo: localization.GetString("UIText_UpdatePortInfo"), + UIText_UPNPEnabled: localization.GetString("UIText_UPNPEnabled"), + UIText_UPNPEnabledInfo: localization.GetString("UIText_UPNPEnabledInfo"), + UIText_LocalIpAddress: localization.GetString("UIText_LocalIpAddress"), + UIText_LocalIpAddressInfo: localization.GetString("UIText_LocalIpAddressInfo"), + UIText_StartLocalHost: localization.GetString("UIText_StartLocalHost"), + UIText_StartLocalHostInfo: localization.GetString("UIText_StartLocalHostInfo"), + UIText_ServerVisible: localization.GetString("UIText_ServerVisible"), + UIText_ServerVisibleInfo: localization.GetString("UIText_ServerVisibleInfo"), + UIText_UseSteamP2P: localization.GetString("UIText_UseSteamP2P"), + UIText_UseSteamP2PInfo: localization.GetString("UIText_UseSteamP2PInfo"), + UIText_AdvancedConfiguration: localization.GetString("UIText_AdvancedConfiguration"), + UIText_ServerAuthSecret: localization.GetString("UIText_ServerAuthSecret"), + UIText_ServerAuthSecretInfo: localization.GetString("UIText_ServerAuthSecretInfo"), + UIText_ServerExePath: localization.GetString("UIText_ServerExePath"), + UIText_ServerExePathInfo: localization.GetString("UIText_ServerExePathInfo"), + UIText_ServerExePathInfo2: localization.GetString("UIText_ServerExePathInfo2"), + UIText_AdditionalParams: localization.GetString("UIText_AdditionalParams"), + UIText_AdditionalParamsInfo: localization.GetString("UIText_AdditionalParamsInfo"), + UIText_AutoRestartServerTimer: localization.GetString("UIText_AutoRestartServerTimer"), + UIText_AutoRestartServerTimerInfo: localization.GetString("UIText_AutoRestartServerTimerInfo"), + UIText_GameBranch: localization.GetString("UIText_GameBranch"), + UIText_GameBranchInfo: localization.GetString("UIText_GameBranchInfo"), + UIText_BetaOnlySettings: localization.GetString("UIText_BetaOnlySettings"), + UIText_BetaWarning: localization.GetString("UIText_BetaWarning"), + UIText_UseNewTerrainAndSave: localization.GetString("UIText_UseNewTerrainAndSave"), + UIText_UseNewTerrainAndSaveInfo: localization.GetString("UIText_UseNewTerrainAndSaveInfo"), + UIText_Difficulty: localization.GetString("UIText_Difficulty"), + UIText_DifficultyInfo: localization.GetString("UIText_DifficultyInfo"), + UIText_StartCondition: localization.GetString("UIText_StartCondition"), + UIText_StartConditionInfo: localization.GetString("UIText_StartConditionInfo"), + UIText_StartLocation: localization.GetString("UIText_StartLocation"), + UIText_StartLocationInfo: localization.GetString("UIText_StartLocationInfo"), + UIText_AutoStartServerOnStartup: localization.GetString("UIText_AutoStartServerOnStartup"), + UIText_AutoStartServerOnStartupInfo: localization.GetString("UIText_AutoStartServerOnStartupInfo"), + UIText_AllowAutoGameServerUpdates: localization.GetString("UIText_AllowAutoGameServerUpdates"), + UIText_AllowAutoGameServerUpdatesInfo: localization.GetString("UIText_AllowAutoGameServerUpdatesInfo"), UIText_DiscordIntegrationTitle: localization.GetString("UIText_DiscordIntegrationTitle"), UIText_DiscordBotToken: localization.GetString("UIText_DiscordBotToken"), diff --git a/src/web/templatevars.go b/src/web/templatevars.go index 08d5bcad..13cd36c2 100644 --- a/src/web/templatevars.go +++ b/src/web/templatevars.go @@ -92,63 +92,64 @@ type ConfigTemplateData struct { UIText_BetaSettings string UIText_BasicServerSettings string - UIText_ServerName string - UIText_ServerNameInfo string - UIText_SaveFileName string - UIText_SaveFileNameInfo string - UIText_MaxPlayers string - UIText_MaxPlayersInfo string - UIText_ServerPassword string - UIText_ServerPasswordInfo string - UIText_AdminPassword string - UIText_AdminPasswordInfo string - UIText_AutoSave string - UIText_AutoSaveInfo string - UIText_SaveInterval string - UIText_SaveIntervalInfo string - UIText_AutoPauseServer string - UIText_AutoPauseServerInfo string - UIText_NetworkConfiguration string - UIText_GamePort string - UIText_GamePortInfo string - UIText_UpdatePort string - UIText_UpdatePortInfo string - UIText_UPNPEnabled string - UIText_UPNPEnabledInfo string - UIText_LocalIpAddress string - UIText_LocalIpAddressInfo string - UIText_StartLocalHost string - UIText_StartLocalHostInfo string - UIText_ServerVisible string - UIText_ServerVisibleInfo string - UIText_UseSteamP2P string - UIText_UseSteamP2PInfo string - UIText_AdvancedConfiguration string - UIText_ServerAuthSecret string - UIText_ServerAuthSecretInfo string - UIText_ServerExePath string - UIText_ServerExePathInfo string - UIText_ServerExePathInfo2 string - UIText_AdditionalParams string - UIText_AdditionalParamsInfo string - UIText_AutoRestartServerTimer string - UIText_AutoRestartServerTimerInfo string - UIText_GameBranch string - UIText_GameBranchInfo string - UIText_BetaOnlySettings string - UIText_BetaWarning string - UIText_UseNewTerrainAndSave string - UIText_UseNewTerrainAndSaveInfo string - UIText_Difficulty string - UIText_DifficultyInfo string - UIText_StartCondition string - UIText_StartConditionInfo string - UIText_StartLocation string - UIText_StartLocationInfo string - UIText_AutoStartServerOnStartup string - UIText_AutoStartServerOnStartupInfo string - UIText_AllowAutoGameServerUpdates string - UIText_AllowAutoGameServerUpdatesInfo string + UIText_ServerName string + UIText_ServerNameInfo string + UIText_SaveFileName string + UIText_SaveFileNameInfo string + UIText_SaveFileNameUseWizzardButtonText string + UIText_MaxPlayers string + UIText_MaxPlayersInfo string + UIText_ServerPassword string + UIText_ServerPasswordInfo string + UIText_AdminPassword string + UIText_AdminPasswordInfo string + UIText_AutoSave string + UIText_AutoSaveInfo string + UIText_SaveInterval string + UIText_SaveIntervalInfo string + UIText_AutoPauseServer string + UIText_AutoPauseServerInfo string + UIText_NetworkConfiguration string + UIText_GamePort string + UIText_GamePortInfo string + UIText_UpdatePort string + UIText_UpdatePortInfo string + UIText_UPNPEnabled string + UIText_UPNPEnabledInfo string + UIText_LocalIpAddress string + UIText_LocalIpAddressInfo string + UIText_StartLocalHost string + UIText_StartLocalHostInfo string + UIText_ServerVisible string + UIText_ServerVisibleInfo string + UIText_UseSteamP2P string + UIText_UseSteamP2PInfo string + UIText_AdvancedConfiguration string + UIText_ServerAuthSecret string + UIText_ServerAuthSecretInfo string + UIText_ServerExePath string + UIText_ServerExePathInfo string + UIText_ServerExePathInfo2 string + UIText_AdditionalParams string + UIText_AdditionalParamsInfo string + UIText_AutoRestartServerTimer string + UIText_AutoRestartServerTimerInfo string + UIText_GameBranch string + UIText_GameBranchInfo string + UIText_BetaOnlySettings string + UIText_BetaWarning string + UIText_UseNewTerrainAndSave string + UIText_UseNewTerrainAndSaveInfo string + UIText_Difficulty string + UIText_DifficultyInfo string + UIText_StartCondition string + UIText_StartConditionInfo string + UIText_StartLocation string + UIText_StartLocationInfo string + UIText_AutoStartServerOnStartup string + UIText_AutoStartServerOnStartupInfo string + UIText_AllowAutoGameServerUpdates string + UIText_AllowAutoGameServerUpdatesInfo string UIText_DiscordIntegrationTitle string UIText_DiscordBotToken string From c2444da5b4dec4947d9373cb0aebe06986238516 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:05:53 +0200 Subject: [PATCH 32/37] some more localization polishes --- UIMod/onboard_bundled/localization/de-DE.json | 12 +++++++----- UIMod/onboard_bundled/localization/en-US.json | 1 + UIMod/onboard_bundled/localization/sv-SE.json | 4 +++- UIMod/onboard_bundled/twoboxform/twoboxform.html | 2 ++ src/web/TwoBoxForm.go | 2 ++ 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/UIMod/onboard_bundled/localization/de-DE.json index d8286cab..5ec5abd6 100644 --- a/UIMod/onboard_bundled/localization/de-DE.json +++ b/UIMod/onboard_bundled/localization/de-DE.json @@ -23,16 +23,17 @@ "UIText_ConfigurationWizard": "Konfigurations-Assistent", "UIText_PleaseSelectSection": "Bitte wähle oben eine Konfigurationssektion aus", "UIText_UseWizardAlternative": "Alternativ nutze den Konfigurations-Assistenten zur Serverkonfiguration.", - "UIText_BasicSettings": "Grundeinstellungen", - "UIText_NetworkSettings": "Netzwerk-Einstellungen", - "UIText_AdvancedSettings": "Erweiterte Einstellungen", - "UIText_BetaSettings": "Beta-Einstellungen", + "UIText_BasicSettings": "Basis", + "UIText_NetworkSettings": "Netzwerk", + "UIText_AdvancedSettings": "Erweitert", + "UIText_BetaSettings": "Beta", "basic": { "UIText_BasicServerSettings": "Grundlegende Servereinstellungen", "UIText_ServerName": "Servername", "UIText_ServerNameInfo": "Name in der Serverliste angezeigt", "UIText_SaveFileName": "Speicherdatei Name", - "UIText_SaveFileNameInfo": "Name des Speicherordners. Muss großgeschrieben sein. Für neue Welt, Welttyp angeben. (MeineVulkanKarte Vulcan) Welttypen im Stationeers Wiki -> Dedicated Server.", + "UIText_SaveFileNameInfo": "Name des Speicherordners. Muss großgeschrieben sein. Für neue Welt, Welttyp angeben. Es wird empfohlen, diesen Wert über den Setup Assistent zu konfigurieren um ihn korrekt zu setzen. ", + "UIText_SaveFileNameUseWizzardButtonText": "Öffne Assistent", "UIText_MaxPlayers": "Max Spieler", "UIText_MaxPlayersInfo": "Maximale Anzahl erlaubter Spieler", "UIText_ServerPassword": "Server Passwort", @@ -126,6 +127,7 @@ }, "setup": { "UIText_FooterText": "Hilfe benötigt? Schaue ins Stationeers Server UI Github Wiki.", + "UIText_FooterTextInfo": "Du kannst das Setup jederzeit beenden, jeder Schritt wird einzeln gespeichert.", "UIText_SSCM_FooterText": "Nutze SSCM für das mächtigste Stationeers Server Management! Du kannst Befehle von der Web-Konsole ausführen ohne Vanilla-Verhalten zu stören!", "UIText_Welcome_Title": "Stationeers Server UI", "UIText_Welcome_HeaderTitle": "Willkommen!", diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index e282a291..4e0b9806 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -126,6 +126,7 @@ }, "setup": { "UIText_FooterText": "Need help? Check the Stationeers Server UI Github Wiki.", + "UIText_FooterTextInfo": "You may exit the wizard at any time, each step is saved individually.", "UIText_SSCM_FooterText": "Use SSCM for the most powerful Stationeers server management! You can run commands from the Web console without disrupting vanilla behaviour!", "UIText_Welcome_Title": "Stationeers Server UI", "UIText_Welcome_HeaderTitle": "Welcome!", diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/UIMod/onboard_bundled/localization/sv-SE.json index de3346d2..ac731882 100644 --- a/UIMod/onboard_bundled/localization/sv-SE.json +++ b/UIMod/onboard_bundled/localization/sv-SE.json @@ -32,7 +32,8 @@ "UIText_ServerName": "Servernamn", "UIText_ServerNameInfo": "Namn som visas i serverlistan", "UIText_SaveFileName": "Sparfilsnamn", - "UIText_SaveFileNameInfo": "Namn på sparmappen. Måste börja med stor bokstav. För att skapa en ny värld, ange världstypen att generera. (MyVulcanMap Vulcan) Världstyper finns på Stationeers Wiki -> Dedicated Server-sidan.", + "UIText_SaveFileNameInfo": "Namn på sparmappen. Måste börja med stor bokstav. För att skapa en ny värld, ange världstypen att generera. Vi rekommenderar att du använder guiden för att konfigurera detta korrekt.", + "UIText_SaveFileNameUseWizzardButtonText": "Öppna guiden", "UIText_MaxPlayers": "Max spelare", "UIText_MaxPlayersInfo": "Maximalt antal tillåtna spelare", "UIText_ServerPassword": "Serverlösenord", @@ -125,6 +126,7 @@ }, "setup": { "UIText_FooterText": "Behöver du hjälp? Kolla Stationeers Server UI Github Wiki.", + "UIText_FooterTextInfo": "Du kan avsluta guiden när som helst, varje steg sparas individuellt.", "UIText_SSCM_FooterText": "Använd SSCM för den mest kraftfulla hanteringen av Stationeers-servrar! Du kan köra kommandon från webbkonsolen utan att störa vanliga funktioner!", "UIText_Welcome_Title": "Stationeers Server UI", "UIText_Welcome_HeaderTitle": "Välkommen!", diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.html b/UIMod/onboard_bundled/twoboxform/twoboxform.html index f328c9cc..6ba43350 100644 --- a/UIMod/onboard_bundled/twoboxform/twoboxform.html +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.html @@ -116,6 +116,8 @@

{{.HeaderTitle}}

+

{{.FooterTextInfo}}

+

{{.FooterText}}

diff --git a/src/web/TwoBoxForm.go b/src/web/TwoBoxForm.go index 59ec24a7..1b4e32bf 100644 --- a/src/web/TwoBoxForm.go +++ b/src/web/TwoBoxForm.go @@ -49,6 +49,7 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { Mode string ShowExtraButtons bool FooterText string + FooterTextInfo string Step string ConfigField string NextStep string @@ -325,6 +326,7 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { Path: path, Step: stepID, FooterText: localization.GetString("UIText_FooterText"), + FooterTextInfo: localization.GetString("UIText_FooterTextInfo"), } switch { From bc6a114af4a52ead062816e314f8505d3309354d Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:08:55 +0200 Subject: [PATCH 33/37] fixed ssui discod icon (again) --- UIMod/onboard_bundled/ui/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UIMod/onboard_bundled/ui/index.html b/UIMod/onboard_bundled/ui/index.html index 0a753410..3ffbd622 100644 --- a/UIMod/onboard_bundled/ui/index.html +++ b/UIMod/onboard_bundled/ui/index.html @@ -152,7 +152,7 @@

- SSUI Discord From d991393fbe49ecef3240f3bdee0b8ef14c8ed51a Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:22:07 +0200 Subject: [PATCH 34/37] increased steam update check interval to 10 minutes --- src/steamcmd/getappinfo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/steamcmd/getappinfo.go b/src/steamcmd/getappinfo.go index bde13ecc..d693693d 100644 --- a/src/steamcmd/getappinfo.go +++ b/src/steamcmd/getappinfo.go @@ -53,7 +53,7 @@ func AppInfoPoller() { case <-stopPoller: logger.Install.Debug("🛑 App info poller stopped") return - case <-time.After(5 * time.Minute): + case <-time.After(10 * time.Minute): // Continue to next iteration } } From b6609f3c2046c434ecbb25039f64808cd4cb97fa Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:36:36 +0200 Subject: [PATCH 35/37] added exit setup button --- .../onboard_bundled/twoboxform/twoboxform.css | 31 ++++++++++++++++++- .../twoboxform/twoboxform.html | 3 ++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.css b/UIMod/onboard_bundled/twoboxform/twoboxform.css index 0e85c2d3..5a5d7795 100644 --- a/UIMod/onboard_bundled/twoboxform/twoboxform.css +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.css @@ -596,4 +596,33 @@ option { option:hover { background-color: var(--primary-dim); color: var(--text-bright); -} \ No newline at end of file +} + + +#exit-button-container { + position: absolute; + top: 15px; + right: 15px; + display: flex; + gap: 10px; + cursor: pointer; + font-variant-emoji: text; + color: #00ffab; +} + +#exit-button-container img{ + width: 30px; + height: 20px; + cursor: pointer; + transition: transform 0.5s ease; +} + +#exit-button-container img { + opacity: 0.7; +} + +#exit-button-container img:hover { + scale: 1.1; + animation: wave 3s infinite; + opacity: 1; +} \ No newline at end of file diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.html b/UIMod/onboard_bundled/twoboxform/twoboxform.html index 6ba43350..984c9c0f 100644 --- a/UIMod/onboard_bundled/twoboxform/twoboxform.html +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.html @@ -28,6 +28,9 @@

Preparing...

{{end}}
+ {{if and (eq .Mode "setup") (ne .Step "welcome")}} +
+ {{end}}

{{.Title}}


From f3397949bf2fb723cc9168654b3673ce17fdf00e Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:58:04 +0200 Subject: [PATCH 36/37] fixed bug with 2bxform where /changeuser would show something went wrong but everything worked fine fixed bug where sucess message on /setup would not show --- UIMod/onboard_bundled/twoboxform/twoboxform.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.js b/UIMod/onboard_bundled/twoboxform/twoboxform.js index 4add1e9a..3423361b 100644 --- a/UIMod/onboard_bundled/twoboxform/twoboxform.js +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.js @@ -193,13 +193,15 @@ document.addEventListener('DOMContentLoaded', () => { }); const data = await response.json(); - if (response.ok) { + if (response.ok || response.status === 201) { if (configField || step === "admin_account") { hidePreloader(); showNotification(step === "admin_account" ? 'Admin account saved!' : 'Config saved!', 'success'); // Wait for backend response to complete before redirecting try { await response.json(); } catch (e) {} // Ensure backend response is fully processed - window.location.href = `/setup?step=${nextStep}`; + setTimeout(() => { + window.location.href = `/setup?step=${nextStep}`; + }, 1000); } else if (mode === 'login') { showNotification('Login Successful!', 'success'); await preloadNextPage(); @@ -207,7 +209,6 @@ document.addEventListener('DOMContentLoaded', () => { window.location.href = '/'; } else { // changeuser hidePreloader(); - const data = await response.json(); // Ensure backend response is processed showNotification(data.message || 'User updated!', 'success'); form.reset(); } From 4c7585e0cb2baa67400ee50f9f5e955935f918eb Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:27:39 +0200 Subject: [PATCH 37/37] update desc texts for recovery cmd arg --- src/core/loader/cmdargs.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/loader/cmdargs.go b/src/core/loader/cmdargs.go index 69784e7e..787d63e9 100644 --- a/src/core/loader/cmdargs.go +++ b/src/core/loader/cmdargs.go @@ -26,9 +26,9 @@ func LoadCmdArgs() { flag.StringVar(&backendEndpointPort, "p", "", "(Alias) Override the backend endpoint port (e.g., 8080)") flag.StringVar(&gameBranch, "GameBranch", "", "Override the game branch (e.g., beta)") flag.StringVar(&gameBranch, "b", "", "(Alias) Override the game branch (e.g., beta)") - flag.StringVar(&recoveryPassword, "RecoveryPassword", "", "Enable recovery user and OVERWRITES all existing users (expects password as argument)") - flag.StringVar(&recoveryPassword, "r", "", "(Alias) Enable recovery user and OVERWRITES all existing users (expects password as argument)") - flag.BoolVar(&devMode, "dev", false, "Enable dev mode: Auth, OVERWRITES all existing users with admin:admin->superadmin, and enables cli-console. For development only.") + flag.StringVar(&recoveryPassword, "RecoveryPassword", "", "Adds a 'recovery' user (expects password as argument)") + flag.StringVar(&recoveryPassword, "r", "", "(Alias) Adds a 'recovery' user (expects password as argument)") + flag.BoolVar(&devMode, "dev", false, "Enable dev mode: Auth, and enables cli-console. For development only.") flag.IntVar(&logLevel, "LogLevel", 0, "Override the log level (e.g., 10)") flag.IntVar(&logLevel, "ll", 0, "(Alias) Override the log level (e.g., 10)") flag.BoolVar(&isDebugMode, "IsDebugMode", false, "Enable debug mode")