From 238a2019411f06e3aa8e1f7e7d2d349e724099f5 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 13 Feb 2026 15:04:36 +0100 Subject: [PATCH 01/20] fixes [StationeersSUI] Improve "Save Name" input label on the config page Fixes #144 --- UIMod/onboard_bundled/localization/de-DE.json | 2 +- UIMod/onboard_bundled/localization/en-US.json | 4 ++-- UIMod/onboard_bundled/localization/sv-SE.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/UIMod/onboard_bundled/localization/de-DE.json index 90f922d9..6fe6090f 100644 --- a/UIMod/onboard_bundled/localization/de-DE.json +++ b/UIMod/onboard_bundled/localization/de-DE.json @@ -55,7 +55,7 @@ "UIText_AutoPauseServer": "Server Auto Pausieren", "UIText_AutoPauseServerInfo": "Server automatisch pausieren wenn kein Spieler verbunden ist", "UIText_SaveName": "Name der Speicherdatei", - "UIText_SaveNameInfo": "Name des Speicherordners, z. B. 'MySave' oder 'Europa Brutal'", + "UIText_SaveNameInfo": "Name des Speicherordners, z. B. 'MySave' oder 'Europa Brutal'. Dies ist der Name des Ordners, der im 'saves'-Ordner erstellt wird. Das Ändern dieses Werts auf einen nicht existierenden Ordnernamen erstellt einen neuen Speicherstand.", "UIText_WorldID": "Welt-ID", "UIText_WorldIDInfo": "Die Welt-ID, die beim Erstellen einer neuen Welt benutzt wird. Eine Liste der Welt-IDs findest du im Dedicated Server Wiki oder kannst du ganz einfach über den Einrichtungsassistenten konfigurieren." }, diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index 771215f7..faf5ccb9 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -52,7 +52,7 @@ "UIText_ServerPassword": "Server Password", "UIText_ServerPasswordInfo": "Password needed to connect to the server. Leave empty for no password", "UIText_AdminPassword": "Admin Password", - "UIText_AdminPasswordInfo": "Server Admin Password. VERY Legacy, unused in current Stationeers versions (as far as we know) - Leavy empty unless you know what this parameter does (and let us know if you do!)", + "UIText_AdminPasswordInfo": "Server Admin Password. VERY Legacy, unused in current Stationeers versions (as far as we know) - Leave empty unless you know what this parameter does (and let us know if you do!)", "UIText_AutoSave": "Auto Save", "UIText_AutoSaveInfo": "Set to TRUE to enable automatic saving", "UIText_SaveInterval": "Save Interval", @@ -60,7 +60,7 @@ "UIText_AutoPauseServer": "Auto Pause Server", "UIText_AutoPauseServerInfo": "Automatically pause server when no players are connected", "UIText_SaveName": "Save Name", - "UIText_SaveNameInfo": "Name of the save folder, like 'MySave' or 'Europa Brutal'", + "UIText_SaveNameInfo": "Name of the save folder, like 'MySave' or 'Europa Brutal'. This is the name of the folder that will be created in the 'saves' folder. Changing this value to a non-existent folder name will create a new save.", "UIText_WorldID": "World ID", "UIText_WorldIDInfo": "World ID used when creating a new world. For a list of world IDs, see the Dedicated Server Wiki or configure them easily from the setup wizard. For more options, see the World generation tab." }, diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/UIMod/onboard_bundled/localization/sv-SE.json index 57e4d339..58be15d5 100644 --- a/UIMod/onboard_bundled/localization/sv-SE.json +++ b/UIMod/onboard_bundled/localization/sv-SE.json @@ -55,7 +55,7 @@ "UIText_AutoPauseServer": "Autopausa server", "UIText_AutoPauseServerInfo": "Pausa servern automatiskt när inga spelare är anslutna", "UIText_SaveName": "Sparfil namn", - "UIText_SaveNameInfo": "Namnet på den sparade mappen, till exempel 'MySave' eller 'Europa Brutal'", + "UIText_SaveNameInfo": "Namnet på den sparade mappen, till exempel 'MySave' eller 'Europa Brutal'. Detta är namnet på mappen som kommer att skapas i 'saves'-mappen. Att ändra detta värde till ett icke-existerande mappnamn kommer att skapa en ny sparfil.", "UIText_WorldID": "Världs-ID", "UIText_WorldIDInfo": "Världs-ID som används när du skapar en ny värld. För en lista över världs-ID:n, se Dedicated Server Wiki eller konfigurera det enkelt från installationsguiden." }, From 8d1984d28aafd4782b1417d58f8078da0420d635 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 13 Feb 2026 15:08:02 +0100 Subject: [PATCH 02/20] fixes #151 [StationeersSUI] Discord Button Interaction Issue (Race) -> Split the ListenToSlashCommands condition into two parts --- src/discordbot/handleSlashcommands.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/discordbot/handleSlashcommands.go b/src/discordbot/handleSlashcommands.go index 88647a02..c4006353 100644 --- a/src/discordbot/handleSlashcommands.go +++ b/src/discordbot/handleSlashcommands.go @@ -37,7 +37,11 @@ var handlers = map[string]commandHandler{ // Check channel and handle initial validation func listenToSlashCommands(s *discordgo.Session, i *discordgo.InteractionCreate) { - if i.Type != discordgo.InteractionApplicationCommand || i.ChannelID != config.GetControlChannelID() { + if i.Type != discordgo.InteractionApplicationCommand { + return + } + + if i.ChannelID != config.GetControlChannelID() { respond(s, i, EmbedData{ Title: "Wrong Channel", Description: "Commands must be sent to the configured control channel", Color: 0xFF0000, Fields: []EmbedField{{Name: "Accepted Channel", Value: fmt.Sprintf("<#%s>", config.GetControlChannelID()), Inline: true}}, From e72e21741b75069f1506c07c034946d1826f45e4 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 13 Feb 2026 17:27:31 +0100 Subject: [PATCH 03/20] fix: enhance backup file handling with logging for corrupt or missing files --- src/managers/backupmgr/getbackups.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/managers/backupmgr/getbackups.go b/src/managers/backupmgr/getbackups.go index 057af9d3..a5c6e3e3 100644 --- a/src/managers/backupmgr/getbackups.go +++ b/src/managers/backupmgr/getbackups.go @@ -9,6 +9,8 @@ import ( "sort" "strings" "time" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" ) const filetimeEpochOffset = 116444736000000000 // difference between 1601 and 1970 in 100-ns units @@ -37,12 +39,14 @@ func (m *BackupManager) getBackupSaveFiles() ([]BackupSaveFile, error) { // Unzip the save file and open the world_meta.xml file inside r, err := zip.OpenReader(fullPath) if err != nil { - return err + logger.Backup.Warnf("Skipping corrupt/unreadable backup file %s: %s", fullPath, err.Error()) + return nil } defer r.Close() worldMetadata, err := r.Open("world_meta.xml") if err != nil { - return err + logger.Backup.Warnf("Skipping backup file %s (missing world_meta.xml): %s", fullPath, err.Error()) + return nil } defer worldMetadata.Close() // Read the world_meta.xml file content using the XML library @@ -52,7 +56,8 @@ func (m *BackupManager) getBackupSaveFiles() ([]BackupSaveFile, error) { var meta WorldMeta decoder := xml.NewDecoder(worldMetadata) if err := decoder.Decode(&meta); err != nil { - return err + logger.Backup.Warnf("Skipping backup file %s (invalid world_meta.xml): %s", fullPath, err.Error()) + return nil } // Convert FILETIME (100-ns intervals) → Unix time (seconds + nanoseconds) From 70d094d2e937017f3ee6be56c0a8a725c0895857 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 13 Feb 2026 17:27:43 +0100 Subject: [PATCH 04/20] fix: improve backup retention logic by adding daily, weekly, and monthly tracking --- src/managers/backupmgr/cleanup.go | 50 ++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/managers/backupmgr/cleanup.go b/src/managers/backupmgr/cleanup.go index f65e1db7..bfbfc361 100644 --- a/src/managers/backupmgr/cleanup.go +++ b/src/managers/backupmgr/cleanup.go @@ -59,6 +59,29 @@ func (m *BackupManager) cleanBackupDir() error { return nil } +// sameCalendarDay returns true if two times fall on the same calendar day (year + day-of-year). +func sameCalendarDay(a, b time.Time) bool { + return a.Year() == b.Year() && a.YearDay() == b.YearDay() +} + +// updateRetentionTrackers updates the daily/weekly/monthly tracker timestamps for a kept backup. +func updateRetentionTrackers(saveTime time.Time, lastKeptDaily, lastKeptWeekly, lastKeptMonthly *time.Time) { + // Track daily + if lastKeptDaily.IsZero() || !sameCalendarDay(saveTime, *lastKeptDaily) { + *lastKeptDaily = saveTime + } + // Track weekly + y1, w1 := saveTime.ISOWeek() + y2, w2 := lastKeptWeekly.ISOWeek() + if lastKeptWeekly.IsZero() || y1 != y2 || w1 != w2 { + *lastKeptWeekly = saveTime + } + // Track monthly + if lastKeptMonthly.IsZero() || saveTime.Month() != lastKeptMonthly.Month() || saveTime.Year() != lastKeptMonthly.Year() { + *lastKeptMonthly = saveTime + } +} + // cleanSafeBackupDir cleans the safe backup directory with retention policy func (m *BackupManager) cleanSafeBackupDir() error { saves, err := m.getBackupSaveFiles() @@ -78,28 +101,33 @@ func (m *BackupManager) cleanSafeBackupDir() error { lastKeptMonthly time.Time ) - for i, group := range saves { - age := now.Sub(group.SaveTime) + for i, backup := range saves { + age := now.Sub(backup.SaveTime) - // Always keep the most recent N backups + // Always keep the most recent N backups, but also update the retention + // trackers so the daily/weekly/monthly logic doesn't redundantly keep + // backups for days already covered by KeepLastN. if i < m.config.RetentionPolicy.KeepLastN { + updateRetentionTrackers(backup.SaveTime, &lastKeptDaily, &lastKeptWeekly, &lastKeptMonthly) continue } // Keep daily backups for specified duration + // Compare full calendar day (year + day-of-year) instead of just day-of-month + // to avoid incorrectly treating e.g. Jan 15 and Feb 15 as the "same day". if age < m.config.RetentionPolicy.KeepDailyFor { - if lastKeptDaily.IsZero() || group.SaveTime.Day() != lastKeptDaily.Day() { - lastKeptDaily = group.SaveTime + if lastKeptDaily.IsZero() || !sameCalendarDay(backup.SaveTime, lastKeptDaily) { + lastKeptDaily = backup.SaveTime continue } } // Keep weekly backups for specified duration if age < m.config.RetentionPolicy.KeepWeeklyFor { - year1, week1 := group.SaveTime.ISOWeek() + year1, week1 := backup.SaveTime.ISOWeek() year2, week2 := lastKeptWeekly.ISOWeek() if lastKeptWeekly.IsZero() || year1 != year2 || week1 != week2 { - lastKeptWeekly = group.SaveTime + lastKeptWeekly = backup.SaveTime continue } } @@ -107,15 +135,15 @@ func (m *BackupManager) cleanSafeBackupDir() error { // Keep monthly backups for specified duration if age < m.config.RetentionPolicy.KeepMonthlyFor { if lastKeptMonthly.IsZero() || - group.SaveTime.Month() != lastKeptMonthly.Month() || - group.SaveTime.Year() != lastKeptMonthly.Year() { - lastKeptMonthly = group.SaveTime + backup.SaveTime.Month() != lastKeptMonthly.Month() || + backup.SaveTime.Year() != lastKeptMonthly.Year() { + lastKeptMonthly = backup.SaveTime continue } } // If we get here, the backup should be deleted - m.deleteBackupGroup(group) + m.deleteBackupGroup(backup) } return nil From 6ec50e6ba7c589be9f395e993be60436137ac201 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 13 Feb 2026 17:46:21 +0100 Subject: [PATCH 05/20] feat: Add uptime to game server status http response --- src/web/http.go | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/web/http.go b/src/web/http.go index 1b214fe6..6e6e7666 100644 --- a/src/web/http.go +++ b/src/web/http.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "strings" + "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" @@ -49,8 +50,9 @@ func StopServer(w http.ResponseWriter, r *http.Request) { func GetGameServerRunState(w http.ResponseWriter, r *http.Request) { runState := config.GetIsGameServerRunning() - response := map[string]interface{}{ + response := map[string]any{ "isRunning": runState, + "uptime": prettyUptime(gamemgr.GetServerUptime()), "uuid": gamemgr.GameServerUUID.String(), } w.Header().Set("Content-Type", "application/json") @@ -60,6 +62,40 @@ func GetGameServerRunState(w http.ResponseWriter, r *http.Request) { } } +// helper to format the uptime in a more human readable way, e.g. "1d2h3m4s" instead of "26h3m4s" +func prettyUptime(d time.Duration) string { + if d <= 0 { + return "0s" + } + + d = d.Round(time.Second) // optional: round to nearest second + var parts []string + + days := int64(d / (24 * time.Hour)) + d -= time.Duration(days) * 24 * time.Hour + + hours := int64(d / time.Hour) + d -= time.Duration(hours) * time.Hour + + minutes := int64(d / time.Minute) + d -= time.Duration(minutes) * time.Minute + + seconds := int64(d / time.Second) + + if days > 0 { + parts = append(parts, fmt.Sprintf("%dd", days)) + } + if hours > 0 || days > 0 { + parts = append(parts, fmt.Sprintf("%dh", hours)) + } + if minutes > 0 || hours > 0 || days > 0 { + parts = append(parts, fmt.Sprintf("%dm", minutes)) + } + parts = append(parts, fmt.Sprintf("%ds", seconds)) + + return strings.Join(parts, "") +} + // CommandHandler handles POST requests to execute commands via commandmgr. // Expects a command in the request body. Returns 204 on success or error details. func CommandHandler(w http.ResponseWriter, r *http.Request) { From 2b64e90c42bcf848313c6931bbdb65ef45c5984c Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 13 Feb 2026 18:14:05 +0100 Subject: [PATCH 06/20] feat: Add uptime display to webUI --- UIMod/onboard_bundled/assets/css/home.css | 15 +++++++++++++++ UIMod/onboard_bundled/assets/js/server-api.js | 16 ++++++++++++++-- UIMod/onboard_bundled/ui/index.html | 1 + 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/UIMod/onboard_bundled/assets/css/home.css b/UIMod/onboard_bundled/assets/css/home.css index 2bc78208..1578a958 100644 --- a/UIMod/onboard_bundled/assets/css/home.css +++ b/UIMod/onboard_bundled/assets/css/home.css @@ -21,6 +21,21 @@ transition: opacity var(--transition-fast); } +.uptime-display { + position: absolute; + right: 50px; + top: 22px; + font-family: 'Share Tech Mono', monospace; + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.75); + background-color: rgba(0, 0, 0, 0.35); + padding: 2px 8px; + border-radius: 4px; + letter-spacing: 0.5px; + white-space: nowrap; + transition: opacity 0.3s ease; +} + .status-indicator { width: 16px; height: 16px; diff --git a/UIMod/onboard_bundled/assets/js/server-api.js b/UIMod/onboard_bundled/assets/js/server-api.js index 38d91e8d..5d1bb125 100644 --- a/UIMod/onboard_bundled/assets/js/server-api.js +++ b/UIMod/onboard_bundled/assets/js/server-api.js @@ -262,7 +262,7 @@ function pollRecurringTasks() { fetch('/api/v2/server/status') .then(response => response.json()) .then(data => { - updateStatusIndicator(data.isRunning); + updateStatusIndicator(data.isRunning, false, data.uptime); if (data.uuid) { localStorage.setItem('gameserverrunID', data.uuid); } @@ -290,13 +290,15 @@ function pollRecurringTasks() { }, 30000); } -function updateStatusIndicator(isRunning, isError = false) { +function updateStatusIndicator(isRunning, isError = false, uptime = '') { const indicator = document.getElementById('status-indicator'); + const uptimeDisplay = document.getElementById('uptime-display'); if (isError) { indicator.className = 'status-indicator error'; indicator.title = 'Error fetching server status'; window.gamserverstate = false; + if (uptimeDisplay) uptimeDisplay.style.display = 'none'; return; } @@ -309,4 +311,14 @@ function updateStatusIndicator(isRunning, isError = false) { indicator.title = 'Server is offline'; window.gamserverstate = false; } + + // Show uptime only when server is running and uptime is not "0s" + if (uptimeDisplay) { + if (isRunning && uptime && uptime !== '0s') { + uptimeDisplay.textContent = uptime; + uptimeDisplay.style.display = 'inline-block'; + } else { + uptimeDisplay.style.display = 'none'; + } + } } \ No newline at end of file diff --git a/UIMod/onboard_bundled/ui/index.html b/UIMod/onboard_bundled/ui/index.html index 19b65dd0..aa7b51dc 100644 --- a/UIMod/onboard_bundled/ui/index.html +++ b/UIMod/onboard_bundled/ui/index.html @@ -63,6 +63,7 @@

+

Stationeers Server UI v{{.Version}}{{.SSUIIdentifier}}

From 0805c30a1fba2955724e33586f76aafbcf64b7cf Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 13 Feb 2026 19:09:56 +0100 Subject: [PATCH 07/20] update go version to 1.26 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2967a033..e1571ee4 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/JacksonTheMaster/StationeersServerUI/v5 -go 1.25.0 +go 1.26 require ( github.com/bwmarrin/discordgo v0.28.1 From 5e9244600078b3f356b56bfd579d63c88f8f9a25 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 13 Feb 2026 16:09:17 +0100 Subject: [PATCH 08/20] feat: discord: redesign connected players panel with embeds and Steam Community links - Implement styled embed display for connected players - Add clickable Steam Community profile links for each player - Send initial panel on bot startup with proper message cleanup - Fix message editing to properly update embeds --- src/discordbot/connectedplayers.go | 117 ++++++++++++++++++++--------- src/discordbot/interface.go | 5 +- 2 files changed, 86 insertions(+), 36 deletions(-) diff --git a/src/discordbot/connectedplayers.go b/src/discordbot/connectedplayers.go index 6817a560..9e2bc511 100644 --- a/src/discordbot/connectedplayers.go +++ b/src/discordbot/connectedplayers.go @@ -8,6 +8,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/bwmarrin/discordgo" ) var ( @@ -15,13 +16,35 @@ var ( playersMutex sync.Mutex ) +// sendConnectedPlayersPanel sends the initial "no players" embed on startup +func sendConnectedPlayersPanel() { + if !config.GetIsDiscordEnabled() { + return + } + + channelID := config.GetConnectionListChannelID() + if channelID == "" { + logger.Discord.Debug("Connection list channel ID not configured, skipping panel") + return + } + + embed := buildConnectedPlayersEmbed(nil) + sendOrEditConnectedPlayersEmbed(channelID, embed) + clearMessagesAboveLastN(channelID, 1) + logger.Discord.Info("Connected players panel sent successfully") +} + func AddToConnectedPlayers(username, steamID string, connectionTime time.Time, players map[string]string) { if !config.GetIsDiscordEnabled() || config.DiscordSession == nil { logger.Discord.Debug("Discord not enabled or session not initialized") return } - content := formatConnectedPlayers(players) - sendAndEditMessageInConnectedPlayersChannel(config.GetConnectionListChannelID(), content) + channelID := config.GetConnectionListChannelID() + if channelID == "" { + return + } + embed := buildConnectedPlayersEmbed(players) + sendOrEditConnectedPlayersEmbed(channelID, embed) } func RemoveFromConnectedPlayers(steamID string, players map[string]string) { @@ -29,56 +52,82 @@ func RemoveFromConnectedPlayers(steamID string, players map[string]string) { logger.Discord.Debug("Discord not enabled or session not initialized") return } - content := formatConnectedPlayers(players) - sendAndEditMessageInConnectedPlayersChannel(config.GetConnectionListChannelID(), content) + channelID := config.GetConnectionListChannelID() + if channelID == "" { + return + } + embed := buildConnectedPlayersEmbed(players) + sendOrEditConnectedPlayersEmbed(channelID, embed) +} + +// buildConnectedPlayersEmbed creates a Discord embed for the connected players panel +func buildConnectedPlayersEmbed(players map[string]string) *discordgo.MessageEmbed { + embed := &discordgo.MessageEmbed{ + Title: "👥 Connected Players", + Timestamp: time.Now().Format(time.RFC3339), + Footer: &discordgo.MessageEmbedFooter{ + Text: "Last updated", + }, + } + + if len(players) == 0 { + embed.Description = "No players are currently connected." + embed.Color = 0x95A5A6 // Grey + return embed + } + + embed.Color = 0x2ECC71 // Green + + // Build a clean row-based player list in the description + var lines strings.Builder + fmt.Fprintf(&lines, "**%d** player(s) online, click opens Steam profile\n\n", len(players)) + for steamID, username := range players { + fmt.Fprintf(&lines, "👤 [%s](https://steamcommunity.com/profiles/%s/)\n", username, steamID) + } + embed.Description = lines.String() + + return embed } -func sendAndEditMessageInConnectedPlayersChannel(channelID, message string) { +// sendOrEditConnectedPlayersEmbed sends a new embed or edits the existing one +func sendOrEditConnectedPlayersEmbed(channelID string, embed *discordgo.MessageEmbed) { playersMutex.Lock() defer playersMutex.Unlock() if connectedPlayersMessageID == "" { - // Send a new message if there's no existing one - msg, err := config.DiscordSession.ChannelMessageSend(channelID, message) + // Send a new message with embed + msg, err := config.DiscordSession.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{ + Embeds: []*discordgo.MessageEmbed{embed}, + }) if err != nil { - logger.Discord.Error("Error sending message to channel " + channelID + ": " + err.Error()) + logger.Discord.Error("Error sending connected players embed to channel " + channelID + ": " + err.Error()) return } connectedPlayersMessageID = msg.ID - logger.Discord.Debug("Sent new message to channel " + channelID) + logger.Discord.Debug("Sent connected players embed to channel " + channelID) } else { - // Edit the existing message - _, err := config.DiscordSession.ChannelMessageEdit(channelID, connectedPlayersMessageID, message) + // Edit the existing message with the updated embed + embeds := []*discordgo.MessageEmbed{embed} + content := "" + _, err := config.DiscordSession.ChannelMessageEditComplex(&discordgo.MessageEdit{ + Channel: channelID, + ID: connectedPlayersMessageID, + Content: &content, + Embeds: &embeds, + }) if err != nil { - logger.Discord.Error("Error editing message in channel " + channelID + ": " + err.Error()) + logger.Discord.Error("Error editing connected players embed in channel " + channelID + ": " + err.Error()) // If editing fails (e.g., message deleted), reset and try sending a new one connectedPlayersMessageID = "" - msg, err := config.DiscordSession.ChannelMessageSend(channelID, message) + msg, err := config.DiscordSession.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{ + Embeds: []*discordgo.MessageEmbed{embed}, + }) if err != nil { - logger.Discord.Error("Error sending fallback message to channel " + channelID + ": " + err.Error()) + logger.Discord.Error("Error sending fallback connected players embed to channel " + channelID + ": " + err.Error()) } else { connectedPlayersMessageID = msg.ID - logger.Discord.Debug("Sent new message after edit failure to channel " + channelID) + logger.Discord.Debug("Sent new connected players embed after edit failure to channel " + channelID) } } } } - -func formatConnectedPlayers(players map[string]string) string { - if len(players) == 0 { - return "No players are currently connected." - } - - var sb strings.Builder - sb.WriteString("Connected Players:\n") - sb.WriteString("```\n") - sb.WriteString("Username | Steam ID\n") - sb.WriteString("----------------------|------------------------\n") - - for steamID, username := range players { - sb.WriteString(fmt.Sprintf("%-20s | %s\n", username, steamID)) - } - - sb.WriteString("```") - return sb.String() -} diff --git a/src/discordbot/interface.go b/src/discordbot/interface.go index 1c162efe..09d6a698 100644 --- a/src/discordbot/interface.go +++ b/src/discordbot/interface.go @@ -56,8 +56,9 @@ func InitializeDiscordBot() { logger.Discord.Info("Bot is now running.") SendMessageToStatusChannel("🤖 SSUI Version " + config.GetVersion() + " connected to Discord.") - sendControlPanel() // Send control panel message to Discord - sendServerInfoPanel() // Send server info panel with buttons to Discord + sendControlPanel() // Send control panel message to Discord + sendServerInfoPanel() // Send server info panel with buttons to Discord + sendConnectedPlayersPanel() // Send connected players panel to Discord UpdateBotStatusWithMessage("StationeersServerUI v" + config.GetVersion()) // Start buffer flush ticker BufferFlushTicker = time.NewTicker(5 * time.Second) From ca4ec17de7ebd4fd6ec7f79339c218c6cb4edb5d Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 13 Feb 2026 18:21:51 +0100 Subject: [PATCH 09/20] removed sendMessageToErrorChannel func that would add a restart button under the exeption message as this has been broken / unused since v4 anyway --- src/discordbot/handleReactions.go | 49 ----------------------------- src/discordbot/sendMessage.go | 51 ------------------------------- 2 files changed, 100 deletions(-) diff --git a/src/discordbot/handleReactions.go b/src/discordbot/handleReactions.go index 351c9d2b..68fc8701 100644 --- a/src/discordbot/handleReactions.go +++ b/src/discordbot/handleReactions.go @@ -1,13 +1,6 @@ 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/bwmarrin/discordgo" ) @@ -23,47 +16,5 @@ func listenToDiscordReactions(s *discordgo.Session, r *discordgo.MessageReaction handleControlReactions(s, r) return } - - // Check if the reaction was added to the last sent exception message for attaching restart buttons. Not used in v4.3 as nothing is sending tracked Exception messages to Discord anymore. - // Instead, we now only yoink the exception message to Discord without tracking it, thus there is no onfig.ExceptionMessageID set anymore. Removed as this was a rather unused feature. - if r.MessageID == config.ExceptionMessageID { - handleExceptionReactions(s, r) - return - } // Optionally, we could add more message-specific handlers here for other features } - -// v4 FIXED, Unused in v4.3 -func handleExceptionReactions(s *discordgo.Session, r *discordgo.MessageReactionAdd) { - var actionMessage string - - switch r.Emoji.Name { - case "♻️": // Stop server action due to exception - actionMessage = "🛑 Server is manually restarting due to critical exception." - gamemgr.InternalStopServer() - //sleep 5 sec - time.Sleep(5 * time.Second) - 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:\n" + err.Error()) - return - } - username := user.Username - - // Send the action message to the error channel - sendMessageToErrorChannel(fmt.Sprintf("%s triggered by %s.", actionMessage, username)) - - // Remove the reaction after processing - err = s.MessageReactionRemove(config.GetErrorChannelID(), r.MessageID, r.Emoji.APIName(), r.UserID) - if err != nil { - logger.Discord.Error("Error removing reaction: " + err.Error()) - } -} diff --git a/src/discordbot/sendMessage.go b/src/discordbot/sendMessage.go index 1654183f..30bd81ed 100644 --- a/src/discordbot/sendMessage.go +++ b/src/discordbot/sendMessage.go @@ -119,57 +119,6 @@ func SendUntrackedMessageToErrorChannel(message string) { } } -// unsused (replaced with SendUntrackedMessageToErrorChannel) in 4.3, needed for having a restart button on the last exception message like in v2. Might remve this in the future, but for now let's keep it. -func sendMessageToErrorChannel(message string) []*discordgo.Message { - if !config.GetIsDiscordEnabled() { - return nil - } - if config.DiscordSession == nil { - logger.Discord.Error("Discord Error: Discord is enabled but session is not initialized") - return nil - } - - maxMessageLength := 2000 // Discord's message character limit - var sentMessages []*discordgo.Message - - // Function to split the message into chunks and send each one - for len(message) > 0 { - if len(message) > maxMessageLength { - // Find a safe split point, for example, the last newline before the limit - splitIndex := strings.LastIndex(message[:maxMessageLength], "\n") - if splitIndex == -1 { - splitIndex = maxMessageLength // No newline found, force split at max length - } - - // Send the chunk - sentMessage, err := config.DiscordSession.ChannelMessageSend(config.GetErrorChannelID(), message[:splitIndex]) - if err != nil { - logger.Discord.Error("Error sending message to error channel: " + err.Error()) - return sentMessages // Return whatever was sent before the error - } - - // Add sent message to the list - sentMessages = append(sentMessages, sentMessage) - - // Remove the sent chunk from the message - message = message[splitIndex:] - } else { - // Send the remaining part of the message - sentMessage, err := config.DiscordSession.ChannelMessageSend(config.GetErrorChannelID(), message) - if err != nil { - logger.Discord.Error("Error sending message to error channel: " + err.Error()) - return sentMessages // Return whatever was sent before the error - } - - // Add the final sent message to the list - sentMessages = append(sentMessages, sentMessage) - break - } - } - - return sentMessages -} - // 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() { From 443defff943d8f5397bafe60d50bc83764dfc072 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 13 Feb 2026 18:23:20 +0100 Subject: [PATCH 10/20] rename SendUntrackedMessageToErrorChannel to SendMessageToErrorChannel for improved clarity since the old SendMessageToErrorChannel with error button (message tracking) was removed --- src/discordbot/sendMessage.go | 2 +- src/managers/detectionmgr/handlers.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/discordbot/sendMessage.go b/src/discordbot/sendMessage.go index 30bd81ed..34653ada 100644 --- a/src/discordbot/sendMessage.go +++ b/src/discordbot/sendMessage.go @@ -73,7 +73,7 @@ func SendMessageToSavesChannel(message string) { } } -func SendUntrackedMessageToErrorChannel(message string) { +func SendMessageToErrorChannel(message string) { if !config.GetIsDiscordEnabled() { return } diff --git a/src/managers/detectionmgr/handlers.go b/src/managers/detectionmgr/handlers.go index d2da296f..c426090e 100644 --- a/src/managers/detectionmgr/handlers.go +++ b/src/managers/detectionmgr/handlers.go @@ -131,7 +131,7 @@ func DefaultHandlers() map[EventType]Handler { alertMessage := "🎮 [Gameserver] 🚨 Exception detected!" logger.Detection.Info(alertMessage) ssestream.BroadcastDetectionEvent(alertMessage) - discordbot.SendUntrackedMessageToErrorChannel(alertMessage) + discordbot.SendMessageToErrorChannel(alertMessage) if event.ExceptionInfo != nil && len(event.ExceptionInfo.StackTrace) > 0 { // Format stack trace as a single-line string for SSE compatibility @@ -140,7 +140,7 @@ func DefaultHandlers() map[EventType]Handler { logger.Detection.Info(message) ssestream.BroadcastDetectionEvent(message) - discordbot.SendUntrackedMessageToErrorChannel(message) + discordbot.SendMessageToErrorChannel(message) } }, } From 3d84532c119c6461cfd618b1d48c0db6da699350 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 13 Feb 2026 20:03:01 +0100 Subject: [PATCH 11/20] added two new devcommands, testConnectedPlayersListDiscord and dumpHeapProfile. --- src/cli/devcommands.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/cli/devcommands.go b/src/cli/devcommands.go index f0aa5f7e..2120e193 100644 --- a/src/cli/devcommands.go +++ b/src/cli/devcommands.go @@ -2,8 +2,13 @@ package cli import ( "fmt" + "os" + "runtime" + "runtime/pprof" + "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discordbot" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/modding" @@ -54,3 +59,35 @@ func testLocalization() { s := localization.GetString("UIText_StartButton") logger.Core.Info("Start Server Button text (current language: " + currentLanguageSetting + "): " + s) } + +func dumpHeapProfile() { + runtime.GC() + if _, err := os.Stat("heap.pprof"); err == nil { + if err := os.Remove("heap.pprof"); err != nil { + logger.Main.Errorf("could not remove old heap profile", "err", err) + return + } + } + f, err := os.Create("heap.pprof") + if err != nil { + logger.Main.Errorf("could not create heap profile file", "err", err) + return + } + defer f.Close() + + if err := pprof.WriteHeapProfile(f); err != nil { + logger.Main.Errorf("could not write heap profile", "err", err) + return + } + + logger.Main.Info("Heap profile written to heap.pprof") +} + +func testConnectedPlayersListDiscord() { + players := map[string]string{ + "76561198334231312": "JacksonTheMaster", + "76561198012262058": "Sebastian - TheNovice", + "76561197995322389": "Non Action Man", + } + discordbot.AddToConnectedPlayers("ThisDataDoesntMatter", "ThisDataDoesntMatter", time.Now(), players) +} From 5661638412188c67c3d2f170f03b5219909a9eea Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 13 Feb 2026 20:03:09 +0100 Subject: [PATCH 12/20] feat: enhance SSUICLI help with pretty print, descriptions and dev-only flags that hide some commands when only using "help" or "h" --- src/cli/commands.go | 50 +++++++++++++++++++++++++------------------- src/cli/ssuicli.go | 51 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 74 insertions(+), 27 deletions(-) diff --git a/src/cli/commands.go b/src/cli/commands.go index 7c7b10ba..716b0afb 100644 --- a/src/cli/commands.go +++ b/src/cli/commands.go @@ -23,27 +23,35 @@ import ( // init registers default cli commands and their aliases. func init() { - RegisterCommand("help", helpCommand, "h") - RegisterCommand("dashboard", dashboardCommand, "dash", "d") - RegisterCommand("reloadbackend", WrapNoReturn(loader.ReloadBackend), "rlb", "rb", "r") - RegisterCommand("reloadconfig", WrapNoReturn(loader.ReloadConfig), "rlc", "rc") - RegisterCommand("restartbackend", WrapNoReturn(loader.RestartBackend), "rsb") - RegisterCommand("exit", WrapNoReturn(exitfromcli), "e") - RegisterCommand("deleteconfig", WrapNoReturn(deleteConfig), "delc", "dc") - RegisterCommand("startserver", WrapNoReturn(startServer), "start") - RegisterCommand("stopserver", WrapNoReturn(stopServer), "stop") - RegisterCommand("runsteamcmd", WrapNoReturn(runSteamCMD), "steamcmd", "stcmd") - RegisterCommand("testlocalization", WrapNoReturn(testLocalization), "tl") - RegisterCommand("supportmode", WrapNoReturn(supportMode), "sm") - RegisterCommand("supportpackage", WrapNoReturn(supportPackage), "sp") - RegisterCommand("getbuildid", WrapNoReturn(getBuildID), "gbid") - RegisterCommand("printconfig", WrapNoReturn(printConfig), "pc") - RegisterCommand("update", WrapNoReturn(triggerUpdateCheck), "u") - RegisterCommand("applyupdate", WrapNoReturn(applyUpdate), "au") - RegisterCommand("listmods", WrapNoReturn(listmods), "lm") - RegisterCommand("listworkshophandles", WrapNoReturn(listworkshophandles), "lwh") - RegisterCommand("downloadworkshopupdates", WrapNoReturn(downloadWorkshopUpdates), "dwu") - RegisterCommand("downloadworkshopitemtest", WrapNoReturn(downloadWorkshopItemTest), "dwmodcon") + + //Long command, Description, IsDevCommand, Aliases + + // User commands + RegisterCommand("help", helpCommand, "Show available commands", false, "h") + RegisterCommand("dashboard", dashboardCommand, "Launch interactive CLI dashboard", false, "dash", "d") + RegisterCommand("startserver", WrapNoReturn(startServer), "Start the game server", false, "start") + RegisterCommand("stopserver", WrapNoReturn(stopServer), "Stop the game server", false, "stop") + RegisterCommand("update", WrapNoReturn(triggerUpdateCheck), "Trigger an SSUI update check", false, "u") + RegisterCommand("applyupdate", WrapNoReturn(applyUpdate), "Apply available SSUI updates", false, "au") + RegisterCommand("reloadbackend", WrapNoReturn(loader.ReloadBackend), "Reload the SSUI backend", false, "rlb", "rb", "r") + RegisterCommand("reloadconfig", WrapNoReturn(loader.ReloadConfig), "Reload the SSUI configuration", false, "rlc", "rc") + RegisterCommand("restartbackend", WrapNoReturn(loader.RestartBackend), "Restart the SSUI backend", false, "rsb") + RegisterCommand("supportmode", WrapNoReturn(supportMode), "Enter support mode", false, "sm") + RegisterCommand("supportpackage", WrapNoReturn(supportPackage), "Create a support package", false, "sp") + RegisterCommand("exit", WrapNoReturn(exitfromcli), "Exit / Shutdown SSUI", false, "e") + RegisterCommand("runsteamcmd", WrapNoReturn(runSteamCMD), "Run SteamCMD to update the Gameserver", false, "steamcmd", "stcmd") + RegisterCommand("downloadworkshopupdates", WrapNoReturn(downloadWorkshopUpdates), "Download Steam workshop Mod updates", true, "dwu") + + // Dev commands + RegisterCommand("deleteconfig", WrapNoReturn(deleteConfig), "DELETE the SSUI configuration file", true, "delc", "dc") + RegisterCommand("testlocalization", WrapNoReturn(testLocalization), "Test localization strings", true, "tl") + RegisterCommand("getbuildid", WrapNoReturn(getBuildID), "Get the current game build ID (server must have started once, else empty)", true, "gbid") + RegisterCommand("printconfig", WrapNoReturn(printConfig), "Print the current SSUI configuration", true, "pc") + RegisterCommand("listmods", WrapNoReturn(listmods), "List installed SLP mods", true, "lm") + RegisterCommand("listworkshophandles", WrapNoReturn(listworkshophandles), "List workshop Mod handles", true, "lwh") + RegisterCommand("downloadworkshopitemtest", WrapNoReturn(downloadWorkshopItemTest), "Test downloading a workshop item (ModularConsolesMod)", true, "dwmodcon") + RegisterCommand("dumpheapprofile", WrapNoReturn(dumpHeapProfile), "Dump a pprof heap profile for debugging", true, "dhp") + RegisterCommand("testconnectedplayerslistdiscord", WrapNoReturn(testConnectedPlayersListDiscord), "Send a fake player list to the Discord package to test the connected players panel", true, "tcpd") } // dashboardCommand launches the interactive terminal dashboard diff --git a/src/cli/ssuicli.go b/src/cli/ssuicli.go index 625444b9..a5497da1 100644 --- a/src/cli/ssuicli.go +++ b/src/cli/ssuicli.go @@ -32,12 +32,16 @@ var commandRegistry = make(map[string]CommandFunc) var mu sync.Mutex var commandAliases = make(map[string][]string) +var commandDescriptions = make(map[string]string) +var commandIsDevOnly = make(map[string]bool) // RegisterCommand adds a new command and its handler to the registry. -func RegisterCommand(name string, handler CommandFunc, aliases ...string) { +func RegisterCommand(name string, handler CommandFunc, desc string, isDevCommand bool, aliases ...string) { mu.Lock() defer mu.Unlock() commandRegistry[name] = handler + commandDescriptions[name] = desc + commandIsDevOnly[name] = isDevCommand if len(aliases) > 0 { commandAliases[name] = append(commandAliases[name], aliases...) for _, alias := range aliases { @@ -127,24 +131,59 @@ func WrapNoReturn(fn func()) CommandFunc { } } -// helpCommand displays available commands along with their aliases. +// helpCommand displays available commands along with their aliases and descriptions. func helpCommand(args []string) error { mu.Lock() defer mu.Unlock() - logger.Core.Info("Available commands:") - // Collect primary commands (those in commandAliases keys) + showDev := len(args) > 0 && args[0] == "dev" + + // Collect primary commands primaryCommands := make([]string, 0, len(commandAliases)) for cmd := range commandAliases { primaryCommands = append(primaryCommands, cmd) } sort.Strings(primaryCommands) + + // Find the longest command name for alignment + maxLen := 0 for _, cmd := range primaryCommands { + if !showDev && commandIsDevOnly[cmd] { + continue + } + if len(cmd) > maxLen { + maxLen = len(cmd) + } + } + + logger.Core.Cleanf("") + if showDev { + logger.Core.Cleanf(" \033[33m═══ SSUICLI Commands (including dev) ═══\033[0m") + } else { + logger.Core.Cleanf(" \033[32m═══ SSUICLI Commands ═══\033[0m") + } + logger.Core.Cleanf("") + + for _, cmd := range primaryCommands { + isDev := commandIsDevOnly[cmd] + if !showDev && isDev { + continue + } + + desc := commandDescriptions[cmd] aliases := commandAliases[cmd] + + aliasStr := "" if len(aliases) > 0 { - logger.Core.Info("- " + cmd + " (aliases: " + strings.Join(aliases, ", ") + ")") + aliasStr = " \033[90m(" + strings.Join(aliases, ", ") + ")\033[0m" + } + + if isDev { + logger.Core.Cleanf(" \033[33m%-*s\033[0m %s%s", maxLen, cmd, desc, aliasStr) } else { - logger.Core.Info("- %s" + cmd) + logger.Core.Cleanf(" \033[36m%-*s\033[0m %s%s", maxLen, cmd, desc, aliasStr) } } + + logger.Core.Cleanf("") return nil } From 94bdcc09617ab05645712cc6873db43d89361ada Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 13 Feb 2026 20:20:35 +0100 Subject: [PATCH 13/20] bump version to 5.13.0 --- src/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.go b/src/config/config.go index 3fb6d76a..9d716b08 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -11,7 +11,7 @@ import ( var ( // All configuration variables can be found in vars.go - Version = "5.12.3" + Version = "5.13.0" Branch = "release" ) From 7bf44b2dd5837539a344c9ee94ab923550e5ae80 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sat, 14 Feb 2026 17:21:41 +0100 Subject: [PATCH 14/20] update Go version to 1.26.0 in Dockerfiles, workflows, and README --- .devcontainer/Dockerfile | 8 ++++---- .devcontainer/devcontainer.json | 2 +- .docker/Dockerfile | 2 +- .github/workflows/auto-release.yaml | 2 +- .github/workflows/test-build.yml | 2 +- readme.md | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c76b6c5b..3c8d5272 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -14,10 +14,10 @@ RUN apt-get update && apt-get install -y \ wget \ && rm -rf /var/lib/apt/lists/* -# Install Go 1.25.0 -RUN wget -q https://go.dev/dl/go1.25.0.linux-amd64.tar.gz && \ - tar -C /usr/local -xzf go1.25.0.linux-amd64.tar.gz && \ - rm go1.25.0.linux-amd64.tar.gz +# Install Go 1.26.0 +RUN wget -q https://go.dev/dl/go1.26.0.linux-amd64.tar.gz && \ + tar -C /usr/local -xzf go1.26.0.linux-amd64.tar.gz && \ + rm go1.26.0.linux-amd64.tar.gz ENV PATH="/usr/local/go/bin:${PATH}" ENV GOPATH=/go RUN mkdir -p /go/bin && chown -R vscode:vscode /go diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c1439a5d..44166e0b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,7 @@ "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/project,type=bind,consistency=cached", "features": { "ghcr.io/devcontainers/features/go:1": { - "version": "1.25.0" + "version": "1.26.0" } }, "forwardPorts": [8443], diff --git a/.docker/Dockerfile b/.docker/Dockerfile index 9bd45c49..5e1639d8 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -16,7 +16,7 @@ RUN npm run build ############ # Go build ############ -FROM golang:1.25-bookworm AS go-builder +FROM golang:1.26-bookworm AS go-builder WORKDIR /src diff --git a/.github/workflows/auto-release.yaml b/.github/workflows/auto-release.yaml index b36a7157..f8402625 100644 --- a/.github/workflows/auto-release.yaml +++ b/.github/workflows/auto-release.yaml @@ -25,7 +25,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.25.0' + go-version: '1.26.0' # Install dependencies - name: Install dependencies diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index e26bd219..18209ba4 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.25.0' + go-version: '1.26.0' # Install dependencies - name: Install dependencies diff --git a/readme.md b/readme.md index 6b6e0e90..e05c5f8a 100644 --- a/readme.md +++ b/readme.md @@ -4,7 +4,7 @@ # Stationeers Server UI -![Go](https://img.shields.io/badge/Go-1.25.0-blue?logo=go&logoColor=white) +![Go](https://img.shields.io/badge/Go-1.26.0-blue?logo=go&logoColor=white) ![Version](https://img.shields.io/github/v/release/jacksonthemaster/StationeersServerUI?logo=github&logoColor=white) ![Issues](https://img.shields.io/github/issues/jacksonthemaster/StationeersServerUI?logo=github&logoColor=white) ![Stars](https://img.shields.io/github/stars/jacksonthemaster/StationeersServerUI?style=social&logo=github) From 61c89687f6a630580f77aeb49aae5b1159dbfd0c Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sat, 14 Feb 2026 18:35:28 +0100 Subject: [PATCH 15/20] Refactor Discord Channel Configuration - Renamed and updated channel names: - `StatusChannel` to `EventLogChannel` - `ConnectionListChannel` to `GetStatusPanelChannel` - Corresponding functions were also renamed. - The config package migrates old keys to new keys for minimal disruption. - Deprecated dedicated saves channel in favor of evens channel and Consolidated server info and connected players panels into a single status panel, `sendServerStatusPanel`, which now handles player connection updates and server info / password gathering. - Updated all references in the codebase to use the new channel names and functions - Removed the `connectedplayers.go` and `serverinfopanel.go` files as their functionality has been integrated into the new status panel (serverstatuspanel.go) - Adjusted html templates and localization variables to reflect the new channel structure. - Updated localization keys for discord channels to be more informative --- UIMod/onboard_bundled/localization/de-DE.json | 24 +- UIMod/onboard_bundled/localization/en-US.json | 22 +- UIMod/onboard_bundled/localization/sv-SE.json | 22 +- UIMod/onboard_bundled/ui/config.html | 27 +- src/cli/commands.go | 2 +- src/cli/devcommands.go | 4 +- src/config/config.go | 62 +++-- src/config/getters.go | 20 +- src/config/setters.go | 21 +- src/config/vars.go | 30 +-- src/core/loader/helpers.go | 19 +- src/discordbot/connectedplayers.go | 133 ---------- src/discordbot/controlpanel.go | 6 +- src/discordbot/handleSlashcommands.go | 6 +- src/discordbot/interface.go | 16 +- src/discordbot/sendMessage.go | 30 +-- src/discordbot/serverinfopanel.go | 157 ----------- src/discordbot/serverstatuspanel.go | 251 ++++++++++++++++++ src/managers/detectionmgr/detector.go | 4 +- src/managers/detectionmgr/handlers.go | 26 +- src/managers/gamemgr/passwordrotation.go | 2 +- src/web/configpage.go | 18 +- src/web/templatevars.go | 18 +- 23 files changed, 409 insertions(+), 511 deletions(-) delete mode 100644 src/discordbot/connectedplayers.go delete mode 100644 src/discordbot/serverinfopanel.go create mode 100644 src/discordbot/serverstatuspanel.go diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/UIMod/onboard_bundled/localization/de-DE.json index 6fe6090f..9dbeeff4 100644 --- a/UIMod/onboard_bundled/localization/de-DE.json +++ b/UIMod/onboard_bundled/localization/de-DE.json @@ -119,23 +119,19 @@ "UIText_DiscordBotTokenInfo": "Authentifizierungstoken deines Discord Bots", "UIText_ChannelConfiguration": "Channel Konfiguration", "UIText_AdminCommandChannel": "Admin-Befehlskanal", - "UIText_AdminCommandChannelInfo": "Channel für Admin-Befehle", + "UIText_AdminCommandChannelInfo": "Channel für Server-Admin-Befehle wie Stoppen, Starten, Wiederherstellen, Server aktualisieren, Gameserver-Befehle ausführen, Spieler bannen & entbannen, Spielstände herunterladen, SSUI neu laden und den Serverstatus abrufen. Nutzung: Tippe / in diesem Discord-Channel und prüfe die Optionen.", "UIText_ControlPanelChannel": "Kontrollpanel Channel", - "UIText_ControlPanelChannelInfo": "Channel für Kontrollpanel", - "UIText_ServerInfoPanelChannel": "Server-Info-Panel Channel", - "UIText_ServerInfoPanelChannelInfo": "Optionaler Channel, der den Servernamen für Benutzer (nicht Admins) anzeigt und Benutzern die Möglichkeit gibt, das aktuelle Server-Passwort abzurufen.", + "UIText_ControlPanelChannelInfo": "Optionaler Channel, in dem ein Schnellzugriffs-Kontrollpanel angezeigt wird. Ermöglicht das Stoppen, Starten, Neustarten und Aktualisieren des Gameservers. Wenn leer, wird dieses Verhalten übersprungen.", "UIText_RotateServerPassword": "Server-Passwort rotieren", - "UIText_RotateServerPasswordInfo": "Rotiert das Server-Passwort automatisch bei jedem Serverstart. Spieler können das aktuelle Passwort über einen Button im \"Server-Info-Panel Channel\" abrufen.", - "UIText_StatusChannel": "Statuskanal", - "UIText_StatusChannelInfo": "Server Status Updates", - "UIText_ConnectionListChannel": "Verbindungslisten Channel", - "UIText_ConnectionListChannelInfo": "Spielerverbindungs-Tracking", + "UIText_RotateServerPasswordInfo": "Rotiert das Server-Passwort automatisch bei jedem Serverstart. Spieler können das aktuelle Passwort über einen Button im Server-Status-Panel Channel abrufen.", + "UIText_EventLogChannel": "Spielereignis-Protokollkanal", + "UIText_EventLogChannelInfo": "Optionaler Channel, in dem Ereignisbenachrichtigungen (Spieler beitreten, Speichern, Serverstatus) angezeigt werden. Wenn leer, wird dieses Verhalten übersprungen.", + "UIText_StatusPanelChannel": "Server-Status-Panel Channel:", + "UIText_StatusPanelChannelInfo": "Optionaler Channel für das Server-Status-Panel mit Serverinfo und verbundenen Spielern. Wenn ein Server-Passwort gesetzt ist, wird hier auch ein Button zum Abrufen des Passworts angezeigt. Wenn leer, wird dieses Verhalten übersprungen.", "UIText_LogChannel": "Protokollkanal", - "UIText_LogChannelInfo": "Server Log Ausgabe", - "UIText_SaveInfoChannel": "Speicherinfo Channel", - "UIText_SaveInfoChannelInfo": "Speicherdatei Informationen", - "UIText_ErrorChannel": "Fehler Channel", - "UIText_ErrorChannelInfo": "Server Fehlermeldungen", + "UIText_LogChannelInfo": "Optionaler Channel, der die vollständige Gameserver-Log-Ausgabe anzeigt. Wenn leer, wird dieses Verhalten übersprungen.", + "UIText_ErrorChannel": "Fehlerkanal", + "UIText_ErrorChannelInfo": "Optionaler Channel, der Serverfehler (Exceptions) anzeigt, wenn einer auftritt. Wenn leer, wird dieses Verhalten übersprungen.", "UIText_BannedPlayersListPath": "Gesperrte Spieler Liste Pfad", "UIText_BannedPlayersListPathInfo": "Dateipfad zur gesperrten Spieler Liste", "UIText_DiscordIntegrationBenefits": "Discord Integration Vorteile", diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index faf5ccb9..f314f88e 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -123,23 +123,19 @@ "UIText_DiscordBotTokenInfo": "Your Discord bot's authentication token", "UIText_ChannelConfiguration": "Channel Configuration", "UIText_AdminCommandChannel": "Admin Command Channel", - "UIText_AdminCommandChannelInfo": "Channel for admin commands", + "UIText_AdminCommandChannelInfo": "Channel for Server-Admin commands like stop, start, restore, update the server, run gameserver commands, ban & unban players, download saves, reload SSUI, and get the server status. Usage: Type / in this Discord channel and check the options.", "UIText_ControlPanelChannel": "Control Panel Channel", - "UIText_ControlPanelChannelInfo": "Channel for control panel", - "UIText_ServerInfoPanelChannel": "Server Info Panel Channel", - "UIText_ServerInfoPanelChannelInfo": "Optional channel which shows server name for users (not admins) and gives users the option to retrieve the current server password.", + "UIText_ControlPanelChannelInfo": "Optional Channel where a quick access control panel is displayed. Allows you to stop, start, restart and update the gameserver. If empty, this behaviour is skipped.", "UIText_RotateServerPassword": "Rotate Server Password", - "UIText_RotateServerPasswordInfo": "Automatically rotates the server password on each server start. Players can get the current password via a button in the \"Server Info Panel Channel\"", - "UIText_StatusChannel": "Status Channel", - "UIText_StatusChannelInfo": "Server status updates", - "UIText_ConnectionListChannel": "Connection List Channel", - "UIText_ConnectionListChannelInfo": "Player connection tracking", + "UIText_RotateServerPasswordInfo": "Automatically rotates the server password on each server start. Players can get the current password via a button in the Server Status Panel Channel.", + "UIText_EventLogChannel": "Game Event Log Channel", + "UIText_EventLogChannelInfo": "Optional channel where Event notifications (player joins, saves, server status) are displayed. If empty, this behaviour is skipped.", + "UIText_StatusPanelChannel": "Server Status Panel Channel", + "UIText_StatusPanelChannelInfo": "Optional Channel for the server status panel showing server info and connected players. If a server password is set, a button to get the password will also be displayed here. If empty, this behaviour is skipped.", "UIText_LogChannel": "Log Channel", - "UIText_LogChannelInfo": "Server log output", - "UIText_SaveInfoChannel": "Save Info Channel", - "UIText_SaveInfoChannelInfo": "Save file information", + "UIText_LogChannelInfo": "Optional channel that displays the full Gameserver log output. If empty, this behaviour is skipped.", "UIText_ErrorChannel": "Error Channel", - "UIText_ErrorChannelInfo": "Server error messages", + "UIText_ErrorChannelInfo": "Optional channel that displays Server Errors (Exceptions) when one occurs. If empty, this behaviour is skipped.", "UIText_BannedPlayersListPath": "Banned Players List Path", "UIText_BannedPlayersListPathInfo": "File path to banned players list", "UIText_DiscordIntegrationBenefits": "Discord Integration Benefits", diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/UIMod/onboard_bundled/localization/sv-SE.json index 58be15d5..defcb2cf 100644 --- a/UIMod/onboard_bundled/localization/sv-SE.json +++ b/UIMod/onboard_bundled/localization/sv-SE.json @@ -117,23 +117,19 @@ "UIText_DiscordBotTokenInfo": "Autentiseringstoken för din Discord-bot", "UIText_ChannelConfiguration": "Kanalkonfiguration", "UIText_AdminCommandChannel": "Admin-kommandokanal", - "UIText_AdminCommandChannelInfo": "Kanal för admin-kommandon", + "UIText_AdminCommandChannelInfo": "Kanal för serveradmin-kommandon som stoppa, starta, återställa, uppdatera servern, köra spelserverkommandon, banna och avbanna spelare, ladda ner sparfiler, ladda om SSUI och hämta serverstatus. Användning: Skriv / i denna Discord-kanal och se alternativen.", "UIText_ControlPanelChannel": "Kontrollpanelkanal", - "UIText_ControlPanelChannelInfo": "Kanal för kontrollpanel", - "UIText_ServerInfoPanelChannel": "Serverinfo-panelkanal", - "UIText_ServerInfoPanelChannelInfo": "Valfri kanal som visar servernamn för användare (inte administratörer) och ger användare möjligheten att hämta det aktuella serverlösenordet.", + "UIText_ControlPanelChannelInfo": "Valfri kanal där en snabbåtkomst-kontrollpanel visas. Gör det möjligt att stoppa, starta, starta om och uppdatera spelservern. Om tom, hoppas detta beteende över.", "UIText_RotateServerPassword": "Rotera serverlösenord", - "UIText_RotateServerPasswordInfo": "Roterar automatiskt serverlösenordet vid varje serverstart. Spelare kan hämta det aktuella lösenordet via en knapp i \"Serverinfo-panelkanalen\".", - "UIText_StatusChannel": "Statuskanal", - "UIText_StatusChannelInfo": "Uppdateringar om serverstatus", - "UIText_ConnectionListChannel": "Anslutningslistkanal", - "UIText_ConnectionListChannelInfo": "Spårning av spelaranslutningar", + "UIText_RotateServerPasswordInfo": "Roterar automatiskt serverlösenordet vid varje serverstart. Spelare kan hämta det aktuella lösenordet via en knapp i serverstatuspanelkanalen.", + "UIText_EventLogChannel": "Spelhändelseloggkanal", + "UIText_EventLogChannelInfo": "Valfri kanal där händelsenotiser (spelaranslutningar, sparningar, serverstatus) visas. Om tom, hoppas detta beteende över.", + "UIText_StatusPanelChannel": "Serverstatuspanelkanal:", + "UIText_StatusPanelChannelInfo": "Valfri kanal för serverstatuspanelen som visar serverinfo och anslutna spelare. Om ett serverlösenord är inställt visas även en knapp för att hämta lösenordet här. Om tom, hoppas detta beteende över.", "UIText_LogChannel": "Loggkanal", - "UIText_LogChannelInfo": "Utdata för serverloggar", - "UIText_SaveInfoChannel": "Sparinfokanal", - "UIText_SaveInfoChannelInfo": "Information om sparfiler", + "UIText_LogChannelInfo": "Valfri kanal som visar fullständig spelserverloggutdata. Om tom, hoppas detta beteende över.", "UIText_ErrorChannel": "Felkanal", - "UIText_ErrorChannelInfo": "Felmeddelanden från servern", + "UIText_ErrorChannelInfo": "Valfri kanal som visar serverfel (Exceptions) när ett uppstår. Om tom, hoppas detta beteende över.", "UIText_BannedPlayersListPath": "Sökväg till bannlysta spelare", "UIText_BannedPlayersListPathInfo": "Filsökväg till listan över bannlysta spelare", "UIText_DiscordIntegrationBenefits": "Fördelar med Discord-integration", diff --git a/UIMod/onboard_bundled/ui/config.html b/UIMod/onboard_bundled/ui/config.html index 700062a3..c46d9561 100644 --- a/UIMod/onboard_bundled/ui/config.html +++ b/UIMod/onboard_bundled/ui/config.html @@ -510,13 +510,6 @@

{{.UIText_ChannelConfiguration}}

{{.UIText_ControlPanelChannelInfo}}
-
- - -
{{.UIText_ServerInfoPanelChannelInfo}}
-
-
-
{{.UIText_StatusChannelInfo}}
+ + +
{{.UIText_EventLogChannelInfo}}
- - -
{{.UIText_ConnectionListChannelInfo}}
+ + +
{{.UIText_StatusPanelChannelInfo}}
@@ -545,12 +538,6 @@

{{.UIText_ChannelConfiguration}}

{{.UIText_LogChannelInfo}}
-
- - -
{{.UIText_SaveInfoChannelInfo}}
-
-
diff --git a/src/cli/commands.go b/src/cli/commands.go index 716b0afb..f9a7a57c 100644 --- a/src/cli/commands.go +++ b/src/cli/commands.go @@ -51,7 +51,7 @@ func init() { RegisterCommand("listworkshophandles", WrapNoReturn(listworkshophandles), "List workshop Mod handles", true, "lwh") RegisterCommand("downloadworkshopitemtest", WrapNoReturn(downloadWorkshopItemTest), "Test downloading a workshop item (ModularConsolesMod)", true, "dwmodcon") RegisterCommand("dumpheapprofile", WrapNoReturn(dumpHeapProfile), "Dump a pprof heap profile for debugging", true, "dhp") - RegisterCommand("testconnectedplayerslistdiscord", WrapNoReturn(testConnectedPlayersListDiscord), "Send a fake player list to the Discord package to test the connected players panel", true, "tcpd") + RegisterCommand("testserverstatuspaneldiscord", WrapNoReturn(testServerStatusPanelDiscord), "Send a fake player list to the Discord package to test the server status panel", true, "tsspd") } // dashboardCommand launches the interactive terminal dashboard diff --git a/src/cli/devcommands.go b/src/cli/devcommands.go index 2120e193..fca97acf 100644 --- a/src/cli/devcommands.go +++ b/src/cli/devcommands.go @@ -83,11 +83,11 @@ func dumpHeapProfile() { logger.Main.Info("Heap profile written to heap.pprof") } -func testConnectedPlayersListDiscord() { +func testServerStatusPanelDiscord() { players := map[string]string{ "76561198334231312": "JacksonTheMaster", "76561198012262058": "Sebastian - TheNovice", "76561197995322389": "Non Action Man", } - discordbot.AddToConnectedPlayers("ThisDataDoesntMatter", "ThisDataDoesntMatter", time.Now(), players) + discordbot.UpdateStatusPanelPlayerConnected("ThisDataDoesntMatter", "ThisDataDoesntMatter", time.Now(), players) } diff --git a/src/config/config.go b/src/config/config.go index 9d716b08..ae968fa1 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -89,19 +89,20 @@ type JsonConfig struct { IsStationeersLaunchPadAutoUpdatesEnabled *bool `json:"IsStationeersLaunchPadAutoUpdatesEnabled"` // 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"` - ServerInfoPanelChannelID string `json:"serverInfoPanelChannelID"` - DiscordCharBufferSize int `json:"DiscordCharBufferSize"` - BlackListFilePath string `json:"blackListFilePath"` - IsDiscordEnabled *bool `json:"isDiscordEnabled"` - RotateServerPassword *bool `json:"rotateServerPassword"` - ErrorChannelID string `json:"errorChannelID"` + DiscordToken string `json:"discordToken"` + ControlChannelID string `json:"controlChannelID"` + EventLogChannelID string `json:"eventLogChannelID"` + StatusChannelID string `json:"statusChannelID,omitempty"` // deprecated, migrated to EventLogChannelID + ConnectionListChannelID string `json:"connectionListChannelID,omitempty"` // deprecated, migrated to StatusPanelChannelID + StatusPanelChannelID string `json:"statusPanelChannelID"` // replaces ConnectionListChannelID and ServerInfoPanelChannelID + LogChannelID string `json:"logChannelID"` + SaveChannelID string `json:"saveChannelID,omitempty"` // deprecated, merged into EventLogChannelID + ControlPanelChannelID string `json:"controlPanelChannelID"` + DiscordCharBufferSize int `json:"DiscordCharBufferSize"` + BlackListFilePath string `json:"blackListFilePath"` + IsDiscordEnabled *bool `json:"isDiscordEnabled"` + RotateServerPassword *bool `json:"rotateServerPassword"` + ErrorChannelID string `json:"errorChannelID"` //Backup Settings BackupKeepLastN int `json:"backupKeepLastN"` // Number of most recent backups to keep (default: 2000) @@ -145,12 +146,10 @@ func applyConfig(cfg *JsonConfig) { // Apply values with hierarchy DiscordToken = getString(cfg.DiscordToken, "DISCORD_TOKEN", "") ControlChannelID = getString(cfg.ControlChannelID, "CONTROL_CHANNEL_ID", "") - StatusChannelID = getString(cfg.StatusChannelID, "STATUS_CHANNEL_ID", "") - ConnectionListChannelID = getString(cfg.ConnectionListChannelID, "CONNECTION_LIST_CHANNEL_ID", "") + EventLogChannelID = getString(cfg.EventLogChannelID, "GAME_EVENT_LOG_CHANNEL_ID", "") + StatusPanelChannelID = getString(cfg.StatusPanelChannelID, "STATUS_PANEL_CHANNEL_ID", "") LogChannelID = getString(cfg.LogChannelID, "LOG_CHANNEL_ID", "") - SaveChannelID = getString(cfg.SaveChannelID, "SAVE_CHANNEL_ID", "") ControlPanelChannelID = getString(cfg.ControlPanelChannelID, "CONTROL_PANEL_CHANNEL_ID", "") - ServerInfoPanelChannelID = getString(cfg.ServerInfoPanelChannelID, "SERVER_INFO_PANEL_CHANNEL_ID", "") DiscordCharBufferSize = getInt(cfg.DiscordCharBufferSize, "DISCORD_CHAR_BUFFER_SIZE", 1000) BlackListFilePath = getString(cfg.BlackListFilePath, "BLACKLIST_FILE_PATH", "./Blacklist.txt") @@ -300,6 +299,8 @@ func applyConfig(cfg *JsonConfig) { ShowExpertSettings = showExpertSettingsVal cfg.ShowExpertSettings = &showExpertSettingsVal + // START MIGRATIONS AND BACKWARDS COMPATIBILITY + // Process SaveInfo to maintain backwards compatibility with pre-5.6.6 SaveInfo field (deprecated) if SaveInfo != "" { parts := strings.Split(SaveInfo, " ") @@ -314,6 +315,27 @@ func applyConfig(cfg *JsonConfig) { cfg.SaveInfo = "" } + // Migrate ConnectionListChannelID -> StatusPanelChannelID (pre-5.13.0) + if cfg.ConnectionListChannelID != "" && StatusPanelChannelID == "" { + StatusPanelChannelID = cfg.ConnectionListChannelID + fmt.Println("Migrated ConnectionListChannelID to StatusPanelChannelID: " + StatusPanelChannelID) + cfg.ConnectionListChannelID = "" + } + + // Migrate StatusChannelID -> EventLogChannelID (pre-5.14.0) + if cfg.StatusChannelID != "" && EventLogChannelID == "" { + EventLogChannelID = cfg.StatusChannelID + fmt.Println("Migrated StatusChannelID to EventLogChannelID: " + EventLogChannelID) + cfg.StatusChannelID = "" + } + + // Migrate SaveChannelID -> EventLogChannelID (pre-5.14.0, save events now go to game event log) + if cfg.SaveChannelID != "" { + fmt.Println("SaveChannelID is deprecated and has been removed. Save events now go to the Game Event Log Channel. Dropping SaveChannelID value: " + cfg.SaveChannelID) + } + + // END MIGRATIONS AND BACKWARDS COMPATIBILITY + if GameBranch != "public" && GameBranch != "beta" && GameBranch != "multiplayersafe" { IsNewTerrainAndSaveSystem = false fmt.Println("The old terrain system and save format are no longer fully supported by SSUI. Please switch to the new terrain and save system if you wish to continue to use SSUI with all features. Please switch to the new Terrain system if you wish to continue to use new SSUI features. Alternatively, you can continue to use the old system by using an older version of SSUI, disabling auto-updates via the config.json file") @@ -357,12 +379,10 @@ func safeSaveConfig() error { cfg := JsonConfig{ DiscordToken: DiscordToken, ControlChannelID: ControlChannelID, - StatusChannelID: StatusChannelID, - ConnectionListChannelID: ConnectionListChannelID, + EventLogChannelID: EventLogChannelID, + StatusPanelChannelID: StatusPanelChannelID, LogChannelID: LogChannelID, - SaveChannelID: SaveChannelID, ControlPanelChannelID: ControlPanelChannelID, - ServerInfoPanelChannelID: ServerInfoPanelChannelID, DiscordCharBufferSize: DiscordCharBufferSize, BlackListFilePath: BlackListFilePath, IsDiscordEnabled: &IsDiscordEnabled, diff --git a/src/config/getters.go b/src/config/getters.go index 97a5b7c2..febfa4bd 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -16,16 +16,16 @@ func GetControlChannelID() string { return ControlChannelID } -func GetStatusChannelID() string { +func GetEventLogChannelID() string { ConfigMu.RLock() defer ConfigMu.RUnlock() - return StatusChannelID + return EventLogChannelID } -func GetConnectionListChannelID() string { +func GetStatusPanelChannelID() string { ConfigMu.RLock() defer ConfigMu.RUnlock() - return ConnectionListChannelID + return StatusPanelChannelID } func GetLogChannelID() string { @@ -34,24 +34,12 @@ func GetLogChannelID() string { return LogChannelID } -func GetSaveChannelID() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return SaveChannelID -} - func GetControlPanelChannelID() string { ConfigMu.RLock() defer ConfigMu.RUnlock() return ControlPanelChannelID } -func GetServerInfoPanelChannelID() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return ServerInfoPanelChannelID -} - func GetDiscordCharBufferSize() int { ConfigMu.RLock() defer ConfigMu.RUnlock() diff --git a/src/config/setters.go b/src/config/setters.go index d3767f7c..8744f1da 100644 --- a/src/config/setters.go +++ b/src/config/setters.go @@ -519,12 +519,12 @@ func SetControlChannelID(value string) error { return safeSaveConfig() } -// SetStatusChannelID sets the StatusChannelID -func SetStatusChannelID(value string) error { +// SetEventLogChannelID sets the EventLogChannelID +func SetEventLogChannelID(value string) error { ConfigMu.Lock() defer ConfigMu.Unlock() - StatusChannelID = value + EventLogChannelID = value return safeSaveConfig() } @@ -546,21 +546,12 @@ func SetErrorChannelID(value string) error { return safeSaveConfig() } -// SetConnectionListChannelID sets the ConnectionListChannelID -func SetConnectionListChannelID(value string) error { +// SetStatusPanelChannelID sets the StatusPanelChannelID +func SetStatusPanelChannelID(value string) error { ConfigMu.Lock() defer ConfigMu.Unlock() - ConnectionListChannelID = value - return safeSaveConfig() -} - -// SetSaveChannelID sets the SaveChannelID -func SetSaveChannelID(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - SaveChannelID = value + StatusPanelChannelID = value return safeSaveConfig() } diff --git a/src/config/vars.go b/src/config/vars.go index f4d02c07..86308c98 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -32,7 +32,7 @@ var ( AdditionalParams string UPNPEnabled bool StartLocalHost bool - SaveInfo string + SaveInfo string // deprecated, replaced by SaveName and WorldID SaveName string WorldID string SaveInterval string @@ -82,21 +82,19 @@ var ( // Discord integration var ( - DiscordToken string - DiscordSession *discordgo.Session - IsDiscordEnabled bool - RotateServerPassword bool - ControlChannelID string - StatusChannelID string - LogChannelID string - ErrorChannelID string - ConnectionListChannelID string - SaveChannelID string - ControlPanelChannelID string - ServerInfoPanelChannelID string - DiscordCharBufferSize int - ExceptionMessageID string - BlackListFilePath string + DiscordToken string + DiscordSession *discordgo.Session + IsDiscordEnabled bool + RotateServerPassword bool + ControlChannelID string + EventLogChannelID string + LogChannelID string + ErrorChannelID string + StatusPanelChannelID string + ControlPanelChannelID string + DiscordCharBufferSize int + ExceptionMessageID string + BlackListFilePath string ) // Backup and cleanup settings diff --git a/src/core/loader/helpers.go b/src/core/loader/helpers.go index a53265b2..51e07c7b 100644 --- a/src/core/loader/helpers.go +++ b/src/core/loader/helpers.go @@ -76,16 +76,15 @@ func PrintConfigDetails(logLevel ...string) { // Discord Configuration discord := map[string]string{ - "IsDiscordEnabled": fmt.Sprintf("%v", config.GetIsDiscordEnabled()), - "ControlChannelID": config.GetControlChannelID(), - "StatusChannelID": config.GetStatusChannelID(), - "ConnectionListChannelID": config.GetConnectionListChannelID(), - "LogChannelID": config.GetLogChannelID(), - "SaveChannelID": config.GetSaveChannelID(), - "ControlPanelChannelID": config.GetControlPanelChannelID(), - "ErrorChannelID": config.GetErrorChannelID(), - "DiscordCharBufferSize": fmt.Sprintf("%d", config.GetDiscordCharBufferSize()), - "BlackListFilePath": config.GetBlackListFilePath(), + "IsDiscordEnabled": fmt.Sprintf("%v", config.GetIsDiscordEnabled()), + "ControlChannelID": config.GetControlChannelID(), + "EventLogChannelID": config.GetEventLogChannelID(), + "StatusPanelChannelID": config.GetStatusPanelChannelID(), + "LogChannelID": config.GetLogChannelID(), + "ControlPanelChannelID": config.GetControlPanelChannelID(), + "ErrorChannelID": config.GetErrorChannelID(), + "DiscordCharBufferSize": fmt.Sprintf("%d", config.GetDiscordCharBufferSize()), + "BlackListFilePath": config.GetBlackListFilePath(), } printSection("Discord Configuration", discord) diff --git a/src/discordbot/connectedplayers.go b/src/discordbot/connectedplayers.go deleted file mode 100644 index 9e2bc511..00000000 --- a/src/discordbot/connectedplayers.go +++ /dev/null @@ -1,133 +0,0 @@ -package discordbot - -import ( - "fmt" - "strings" - "sync" - "time" - - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/bwmarrin/discordgo" -) - -var ( - connectedPlayersMessageID string // connectedPlayersMessageID tracks the message ID for editing the connected players message - playersMutex sync.Mutex -) - -// sendConnectedPlayersPanel sends the initial "no players" embed on startup -func sendConnectedPlayersPanel() { - if !config.GetIsDiscordEnabled() { - return - } - - channelID := config.GetConnectionListChannelID() - if channelID == "" { - logger.Discord.Debug("Connection list channel ID not configured, skipping panel") - return - } - - embed := buildConnectedPlayersEmbed(nil) - sendOrEditConnectedPlayersEmbed(channelID, embed) - clearMessagesAboveLastN(channelID, 1) - logger.Discord.Info("Connected players panel sent successfully") -} - -func AddToConnectedPlayers(username, steamID string, connectionTime time.Time, players map[string]string) { - if !config.GetIsDiscordEnabled() || config.DiscordSession == nil { - logger.Discord.Debug("Discord not enabled or session not initialized") - return - } - channelID := config.GetConnectionListChannelID() - if channelID == "" { - return - } - embed := buildConnectedPlayersEmbed(players) - sendOrEditConnectedPlayersEmbed(channelID, embed) -} - -func RemoveFromConnectedPlayers(steamID string, players map[string]string) { - if !config.GetIsDiscordEnabled() || config.DiscordSession == nil { - logger.Discord.Debug("Discord not enabled or session not initialized") - return - } - channelID := config.GetConnectionListChannelID() - if channelID == "" { - return - } - embed := buildConnectedPlayersEmbed(players) - sendOrEditConnectedPlayersEmbed(channelID, embed) -} - -// buildConnectedPlayersEmbed creates a Discord embed for the connected players panel -func buildConnectedPlayersEmbed(players map[string]string) *discordgo.MessageEmbed { - embed := &discordgo.MessageEmbed{ - Title: "👥 Connected Players", - Timestamp: time.Now().Format(time.RFC3339), - Footer: &discordgo.MessageEmbedFooter{ - Text: "Last updated", - }, - } - - if len(players) == 0 { - embed.Description = "No players are currently connected." - embed.Color = 0x95A5A6 // Grey - return embed - } - - embed.Color = 0x2ECC71 // Green - - // Build a clean row-based player list in the description - var lines strings.Builder - fmt.Fprintf(&lines, "**%d** player(s) online, click opens Steam profile\n\n", len(players)) - for steamID, username := range players { - fmt.Fprintf(&lines, "👤 [%s](https://steamcommunity.com/profiles/%s/)\n", username, steamID) - } - embed.Description = lines.String() - - return embed -} - -// sendOrEditConnectedPlayersEmbed sends a new embed or edits the existing one -func sendOrEditConnectedPlayersEmbed(channelID string, embed *discordgo.MessageEmbed) { - playersMutex.Lock() - defer playersMutex.Unlock() - - if connectedPlayersMessageID == "" { - // Send a new message with embed - msg, err := config.DiscordSession.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{ - Embeds: []*discordgo.MessageEmbed{embed}, - }) - if err != nil { - logger.Discord.Error("Error sending connected players embed to channel " + channelID + ": " + err.Error()) - return - } - connectedPlayersMessageID = msg.ID - logger.Discord.Debug("Sent connected players embed to channel " + channelID) - } else { - // Edit the existing message with the updated embed - embeds := []*discordgo.MessageEmbed{embed} - content := "" - _, err := config.DiscordSession.ChannelMessageEditComplex(&discordgo.MessageEdit{ - Channel: channelID, - ID: connectedPlayersMessageID, - Content: &content, - Embeds: &embeds, - }) - if err != nil { - logger.Discord.Error("Error editing connected players embed in channel " + channelID + ": " + err.Error()) - // If editing fails (e.g., message deleted), reset and try sending a new one - connectedPlayersMessageID = "" - msg, err := config.DiscordSession.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{ - Embeds: []*discordgo.MessageEmbed{embed}, - }) - if err != nil { - logger.Discord.Error("Error sending fallback connected players embed to channel " + channelID + ": " + err.Error()) - } else { - connectedPlayersMessageID = msg.ID - logger.Discord.Debug("Sent new connected players embed after edit failure to channel " + channelID) - } - } - } -} diff --git a/src/discordbot/controlpanel.go b/src/discordbot/controlpanel.go index 323e472d..5070694f 100644 --- a/src/discordbot/controlpanel.go +++ b/src/discordbot/controlpanel.go @@ -116,10 +116,10 @@ func handleControlReactions(s *discordgo.Session, r *discordgo.MessageReactionAd _, err := steamcmd.InstallAndRunSteamCMD() Value := map[bool]string{true: "🟢 Success", false: "🔴 Failed"}[err == nil] - SendMessageToStatusChannel(fmt.Sprintf("SteamCMD Update status: %s", Value)) + SendMessageToEventLogChannel(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())) + SendMessageToEventLogChannel(fmt.Sprintf("Update failed: %v", err.Error())) } }() @@ -140,7 +140,7 @@ func handleControlReactions(s *discordgo.Session, r *discordgo.MessageReactionAd 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)) + SendMessageToEventLogChannel(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) diff --git a/src/discordbot/handleSlashcommands.go b/src/discordbot/handleSlashcommands.go index c4006353..5eb13cad 100644 --- a/src/discordbot/handleSlashcommands.go +++ b/src/discordbot/handleSlashcommands.go @@ -75,7 +75,7 @@ func handleStart(s *discordgo.Session, i *discordgo.InteractionCreate, data Embe return err } gamemgr.InternalStartServer() - SendMessageToStatusChannel("🕛Start command received, Server is Starting...") + SendMessageToEventLogChannel("🕛Start command received, Server is Starting...") return nil } @@ -86,7 +86,7 @@ func handleStop(s *discordgo.Session, i *discordgo.InteractionCreate, data Embed return err } gamemgr.InternalStopServer() - SendMessageToStatusChannel("🕛Stop command received, flatlining Server in 5 Seconds...") + SendMessageToEventLogChannel("🕛Stop command received, flatlining Server in 5 Seconds...") return nil } @@ -172,7 +172,7 @@ func handleRestore(s *discordgo.Session, i *discordgo.InteractionCreate, data Em gamemgr.InternalStopServer() if err := backupmgr.GlobalBackupManager.RestoreBackup(index); err != nil { SendMessageToControlChannel(fmt.Sprintf("❌Failed to restore backup %d: %v", index, err)) - SendMessageToStatusChannel("⚠️Restore command failed") + SendMessageToEventLogChannel("⚠️Restore command failed") return nil } SendMessageToControlChannel(fmt.Sprintf("✅Backup %d restored, Starting Server...", index)) diff --git a/src/discordbot/interface.go b/src/discordbot/interface.go index 09d6a698..8d3745f2 100644 --- a/src/discordbot/interface.go +++ b/src/discordbot/interface.go @@ -35,10 +35,9 @@ func InitializeDiscordBot() { logger.Discord.Info("Starting Discord integration...") //logger.Discord.Debug("Discord token: " + config.GetDiscordToken()) logger.Discord.Debug("ControlChannelID: " + config.GetControlChannelID()) - logger.Discord.Debug("StatusChannelID: " + config.GetStatusChannelID()) - logger.Discord.Debug("ConnectionListChannelID: " + config.GetConnectionListChannelID()) + logger.Discord.Debug("EventLogChannelID: " + config.GetEventLogChannelID()) + logger.Discord.Debug("StatusPanelChannelID: " + config.GetStatusPanelChannelID()) logger.Discord.Debug("LogChannelID: " + config.GetLogChannelID()) - logger.Discord.Debug("SaveChannelID: " + config.GetSaveChannelID()) // Open session first err = config.DiscordSession.Open() @@ -50,15 +49,14 @@ func InitializeDiscordBot() { // Register handlers and commands after session is open config.DiscordSession.AddHandler(listenToDiscordReactions) config.DiscordSession.AddHandler(listenToSlashCommands) - config.DiscordSession.AddHandler(handleServerInfoButtonInteraction) // Handle button interactions - config.DiscordSession.AddHandler(handleDownloadButtonInteraction) // Handle download button interactions + config.DiscordSession.AddHandler(handlePanelButtonInteraction) // Handle button interactions (server info + players panel) + config.DiscordSession.AddHandler(handleDownloadButtonInteraction) // Handle download button interactions registerSlashCommands(config.DiscordSession) logger.Discord.Info("Bot is now running.") - SendMessageToStatusChannel("🤖 SSUI Version " + config.GetVersion() + " connected to Discord.") - sendControlPanel() // Send control panel message to Discord - sendServerInfoPanel() // Send server info panel with buttons to Discord - sendConnectedPlayersPanel() // Send connected players panel to Discord + SendMessageToEventLogChannel("🤖 SSUI Version " + config.GetVersion() + " connected to Discord.") + sendControlPanel() // Send control panel message to Discord + sendServerStatusPanel() // Send server status panel to Discord UpdateBotStatusWithMessage("StationeersServerUI v" + config.GetVersion()) // Start buffer flush ticker BufferFlushTicker = time.NewTicker(5 * time.Second) diff --git a/src/discordbot/sendMessage.go b/src/discordbot/sendMessage.go index 34653ada..55afd07c 100644 --- a/src/discordbot/sendMessage.go +++ b/src/discordbot/sendMessage.go @@ -33,12 +33,12 @@ func SendMessageToControlChannel(message string) { } } -func SendMessageToStatusChannel(message string) { +func SendMessageToEventLogChannel(message string) { if !config.GetIsDiscordEnabled() { return } - if config.GetStatusChannelID() == "" { + if config.GetEventLogChannelID() == "" { return } @@ -46,30 +46,10 @@ func SendMessageToStatusChannel(message string) { logger.Discord.Error("Discord Error: Discord is enabled but session is not initialized") return } - //clearMessagesAboveLastN(config.StatusChannelID, 10) - _, err := config.DiscordSession.ChannelMessageSend(config.GetStatusChannelID(), message) + //clearMessagesAboveLastN(config.EventLogChannelID, 10) + _, err := config.DiscordSession.ChannelMessageSend(config.GetEventLogChannelID(), message) if err != nil { - logger.Discord.Error("Error sending message to status channel: " + err.Error()) - } -} - -func SendMessageToSavesChannel(message string) { - if !config.GetIsDiscordEnabled() { - return - } - - if config.GetSaveChannelID() == "" { - return - } - - if config.DiscordSession == nil { - logger.Discord.Error("Discord Error: Discord is enabled but session is not initialized") - return - } - //clearMessagesAboveLastN(config.SaveChannelID, 300) - _, err := config.DiscordSession.ChannelMessageSend(config.GetSaveChannelID(), message) - if err != nil { - logger.Discord.Error("Error sending message to saves channel: " + err.Error()) + logger.Discord.Error("Error sending message to game event log channel: " + err.Error()) } } diff --git a/src/discordbot/serverinfopanel.go b/src/discordbot/serverinfopanel.go deleted file mode 100644 index a3b4beea..00000000 --- a/src/discordbot/serverinfopanel.go +++ /dev/null @@ -1,157 +0,0 @@ -package discordbot - -import ( - "time" - - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/bwmarrin/discordgo" -) - -// ServerInfoMessageID stores the ID of the server info panel message -var ServerInfoMessageID string - -// Custom IDs for button interactions -const ( - ButtonGetPassword = "ssui_get_password" - ButtonDownloadBackupPfx = "ssui_download_backup_" // Prefix for download backup button. Has NOTHING TO DO WITH SERVERINFOPANEL, but this file felt like the best place to put it for now -) - -// sendServerInfoPanel sends the server info panel with interactive buttons -func sendServerInfoPanel() { - if !config.GetIsDiscordEnabled() { - return - } - - channelID := config.GetServerInfoPanelChannelID() - if channelID == "" { - logger.Discord.Debug("Server info panel channel ID not configured, skipping panel") - return - } - - serverName := config.GetServerName() - - // Create an embed for the server info panel - embed := &discordgo.MessageEmbed{ - Title: "🎮 Server Information", - Description: "Your Stationeers server is running as **" + serverName + "**", - Color: 0x5865F2, // Discord blurple color - Fields: []*discordgo.MessageEmbedField{ - { - Name: "Server Name", - Value: serverName, - Inline: true, - }, - { - Name: "SSUI Version", - Value: config.GetVersion(), - Inline: true, - }, - }, - Timestamp: time.Now().Format(time.RFC3339), - } - - // Build message components (buttons in an action row) - var components []discordgo.MessageComponent - - components = append(components, discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.Button{ - Label: "🔑 Get Current Server Password", - Style: discordgo.PrimaryButton, - CustomID: ButtonGetPassword, - }, - }, - }) - - // Send the message with embed and buttons - msg, err := config.DiscordSession.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{ - Embeds: []*discordgo.MessageEmbed{embed}, - Components: components, - }) - if err != nil { - logger.Discord.Error("Error sending server info panel: " + err.Error()) - return - } - - clearMessagesAboveLastN(channelID, 1) // Clear all old server info panel messages - ServerInfoMessageID = msg.ID - logger.Discord.Info("Server info panel sent successfully") -} - -// handleServerInfoButtonInteraction handles button interactions from the server info panel -func handleServerInfoButtonInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { - // Only handle component interactions (buttons) - if i.Type != discordgo.InteractionMessageComponent { - return - } - - // Get the custom ID of the button that was clicked - customID := i.MessageComponentData().CustomID - - switch customID { - case ButtonGetPassword: - handleGetPasswordButton(s, i) - default: - // Not our button, ignore - return - } -} - -// handleGetPasswordButton sends the current server password as an ephemeral message (only visible to the user) -func handleGetPasswordButton(s *discordgo.Session, i *discordgo.InteractionCreate) { - - // Get the current server password from config - password := config.GetServerPassword() - - // Build the embed based on whether password is set - var embed *discordgo.MessageEmbed - if password == "" { - embed = &discordgo.MessageEmbed{ - Title: "🔓 No Password Set", - Description: "No password is currently configured for this server.", - Color: 0xFFA500, // Orange - Footer: &discordgo.MessageEmbedFooter{ - Text: "This message will disappear in 30 seconds", - }, - } - } else { - embed = &discordgo.MessageEmbed{ - Title: "🔑 Current Server Password", - Description: "Use this password to connect to the server.", - Color: 0x00FF00, // Green - Fields: []*discordgo.MessageEmbedField{ - { - Name: "Password", - Value: "```" + password + "```", - Inline: false, - }, - }, - Footer: &discordgo.MessageEmbedFooter{ - Text: "🔒 Only visible to you • Disappears in 30 seconds", - }, - Timestamp: time.Now().Format(time.RFC3339), - } - } - - // Respond with an ephemeral message (only visible to the user who clicked) - err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Embeds: []*discordgo.MessageEmbed{embed}, - Flags: discordgo.MessageFlagsEphemeral, - }, - }) - if err != nil { - logger.Discord.Error("Error responding to get password button: " + err.Error()) - return - } - - go func() { - time.Sleep(30 * time.Second) - err := s.InteractionResponseDelete(i.Interaction) - if err != nil { - logger.Discord.Debug("Could not delete ephemeral password message: " + err.Error()) - } - }() -} diff --git a/src/discordbot/serverstatuspanel.go b/src/discordbot/serverstatuspanel.go new file mode 100644 index 00000000..7b364299 --- /dev/null +++ b/src/discordbot/serverstatuspanel.go @@ -0,0 +1,251 @@ +package discordbot + +import ( + "fmt" + "strings" + "sync" + "time" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/bwmarrin/discordgo" +) + +// Button custom IDs for the server & players panel +const ( + ButtonGetPassword = "ssui_get_password" + ButtonDownloadBackupPfx = "ssui_download_backup_" // Prefix for download backup button +) + +var ( + statusPanelMessageID string // tracks the message ID for editing the server status panel + statusPanelMutex sync.Mutex +) + +// sendServerStatusPanel sends the initial now combined server info + players panel on startup +func sendServerStatusPanel() { + if !config.GetIsDiscordEnabled() { + return + } + + channelID := config.GetStatusPanelChannelID() + if channelID == "" { + logger.Discord.Debug("Status panel channel ID not configured, skipping panel") + return + } + + embed := buildStatusPanelEmbed(nil) + components := buildPanelComponents() + sendOrEditStatusPanel(channelID, embed, components) + clearMessagesAboveLastN(channelID, 1) + logger.Discord.Info("Server status panel sent successfully") +} + +// UpdateStatusPanelPlayerConnected updates the panel when a player connects +func UpdateStatusPanelPlayerConnected(username, steamID string, connectionTime time.Time, players map[string]string) { + if !config.GetIsDiscordEnabled() || config.DiscordSession == nil { + logger.Discord.Debug("Discord not enabled or session not initialized") + return + } + channelID := config.GetStatusPanelChannelID() + if channelID == "" { + return + } + embed := buildStatusPanelEmbed(players) + components := buildPanelComponents() + sendOrEditStatusPanel(channelID, embed, components) +} + +// UpdateStatusPanelPlayerDisconnected updates the panel when a player disconnects +func UpdateStatusPanelPlayerDisconnected(steamID string, players map[string]string) { + if !config.GetIsDiscordEnabled() || config.DiscordSession == nil { + logger.Discord.Debug("Discord not enabled or session not initialized") + return + } + channelID := config.GetStatusPanelChannelID() + if channelID == "" { + return + } + embed := buildStatusPanelEmbed(players) + components := buildPanelComponents() + sendOrEditStatusPanel(channelID, embed, components) +} + +// buildStatusPanelEmbed creates a combined server info + connected players embed +func buildStatusPanelEmbed(players map[string]string) *discordgo.MessageEmbed { + serverName := config.GetServerName() + + embed := &discordgo.MessageEmbed{ + Title: "🎮 Server Information", + Timestamp: time.Now().Format(time.RFC3339), + Color: 0x5865F2, // Discord blurple + } + + // Server info in description + inline fields (matches old style) + //embed.Description = "Your Stationeers server is running as **" + serverName + "**" + embed.Fields = []*discordgo.MessageEmbedField{ + { + Name: "Server Name", + Value: serverName, + Inline: true, + }, + { + Name: "SSUI Version", + Value: config.GetVersion(), + Inline: true, + }, + } + + // Connected players section + if len(players) == 0 { + embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{ + Name: "👥 Connected Players", + Value: "_No players are currently connected._", + }) + } else { + var lines strings.Builder + for steamID, username := range players { + fmt.Fprintf(&lines, "👤 [%s](https://steamcommunity.com/profiles/%s/)\n", username, steamID) + } + embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{ + Name: fmt.Sprintf("👥 Connected Players — %d online", len(players)), + Value: lines.String(), + }) + embed.Color = 0x57F287 // Green when players are online + } + + return embed +} + +// buildPanelComponents returns the action row with interactive buttons +func buildPanelComponents() []discordgo.MessageComponent { + + if config.GetServerPassword() == "" { + return nil + } + return []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "🔑 Get Server Password", + Style: discordgo.PrimaryButton, + CustomID: ButtonGetPassword, + }, + }, + }, + } +} + +// sendOrEditStatusPanel sends a new message or edits the existing one +func sendOrEditStatusPanel(channelID string, embed *discordgo.MessageEmbed, components []discordgo.MessageComponent) { + statusPanelMutex.Lock() + defer statusPanelMutex.Unlock() + + if statusPanelMessageID == "" { + msg, err := config.DiscordSession.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{ + Embeds: []*discordgo.MessageEmbed{embed}, + Components: components, + }) + if err != nil { + logger.Discord.Error("Error sending server status panel to channel " + channelID + ": " + err.Error()) + return + } + statusPanelMessageID = msg.ID + logger.Discord.Debug("Sent server status panel to channel " + channelID) + } else { + embeds := []*discordgo.MessageEmbed{embed} + content := "" + _, err := config.DiscordSession.ChannelMessageEditComplex(&discordgo.MessageEdit{ + Channel: channelID, + ID: statusPanelMessageID, + Content: &content, + Embeds: &embeds, + Components: &components, + }) + if err != nil { + logger.Discord.Error("Error editing server status panel in channel " + channelID + ": " + err.Error()) + // If editing fails (e.g., message deleted), reset and try sending a new one + statusPanelMessageID = "" + msg, err := config.DiscordSession.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{ + Embeds: []*discordgo.MessageEmbed{embed}, + Components: components, + }) + if err != nil { + logger.Discord.Error("Error sending fallback server status panel to channel " + channelID + ": " + err.Error()) + } else { + statusPanelMessageID = msg.ID + logger.Discord.Debug("Sent new server status panel after edit failure to channel " + channelID) + } + } + } +} + +// handlePanelButtonInteraction handles button interactions from the combined panel +func handlePanelButtonInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { + if i.Type != discordgo.InteractionMessageComponent { + return + } + + customID := i.MessageComponentData().CustomID + + switch customID { + case ButtonGetPassword: + handleGetPasswordButton(s, i) + default: + return + } +} + +// handleGetPasswordButton sends the current server password as an ephemeral message +func handleGetPasswordButton(s *discordgo.Session, i *discordgo.InteractionCreate) { + password := config.GetServerPassword() + + var embed *discordgo.MessageEmbed + if password == "" { + embed = &discordgo.MessageEmbed{ + Title: "🔓 No Password Set", + Description: "No password is currently configured for this server.", + Color: 0xFFA500, + Footer: &discordgo.MessageEmbedFooter{ + Text: "This message will disappear in 30 seconds", + }, + } + } else { + embed = &discordgo.MessageEmbed{ + Title: "🔑 Current Server Password", + Description: "Use this password to connect to the server.", + Color: 0x57F287, + Fields: []*discordgo.MessageEmbedField{ + { + Name: "Password", + Value: "```" + password + "```", + Inline: false, + }, + }, + Footer: &discordgo.MessageEmbedFooter{ + Text: "🔒 Only visible to you • Disappears in 30 seconds", + }, + Timestamp: time.Now().Format(time.RFC3339), + } + } + + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{embed}, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + if err != nil { + logger.Discord.Error("Error responding to get password button: " + err.Error()) + return + } + + go func() { + time.Sleep(30 * time.Second) + err := s.InteractionResponseDelete(i.Interaction) + if err != nil { + logger.Discord.Debug("Could not delete ephemeral password message: " + err.Error()) + } + }() +} diff --git a/src/managers/detectionmgr/detector.go b/src/managers/detectionmgr/detector.go index 544b4f72..010cdbb2 100644 --- a/src/managers/detectionmgr/detector.go +++ b/src/managers/detectionmgr/detector.go @@ -114,7 +114,7 @@ func (d *Detector) processRegexPatterns(logMessage string) { // Update connected players d.connectedPlayers[steamID] = username - discordbot.AddToConnectedPlayers(username, steamID, time.Now(), d.connectedPlayers) + discordbot.UpdateStatusPanelPlayerConnected(username, steamID, time.Now(), d.connectedPlayers) d.triggerEvent(Event{ Type: EventPlayerReady, @@ -156,7 +156,7 @@ func (d *Detector) processRegexPatterns(logMessage string) { // Remove from connected players delete(d.connectedPlayers, steamID) - discordbot.RemoveFromConnectedPlayers(steamID, d.connectedPlayers) + discordbot.UpdateStatusPanelPlayerDisconnected(steamID, d.connectedPlayers) d.triggerEvent(Event{ Type: EventPlayerDisconnect, diff --git a/src/managers/detectionmgr/handlers.go b/src/managers/detectionmgr/handlers.go index c426090e..12461870 100644 --- a/src/managers/detectionmgr/handlers.go +++ b/src/managers/detectionmgr/handlers.go @@ -29,56 +29,56 @@ func DefaultHandlers() map[EventType]Handler { message := fmt.Sprintf("🎮 [Custom Detection] %s", event.Message) logger.Detection.Info(message) ssestream.BroadcastDetectionEvent(message) - discordbot.SendMessageToStatusChannel(message) + discordbot.SendMessageToEventLogChannel(message) }, EventServerReady: func(event Event) { message := "🎮 [Gameserver] 🔔 Server is ready to connect!" logger.Detection.Info(message) ssestream.BroadcastDetectionEvent(message) - discordbot.SendMessageToStatusChannel(message) + discordbot.SendMessageToEventLogChannel(message) }, EventServerStarting: func(event Event) { message := "🎮 [Gameserver] 🕑 Server is starting up..." logger.Detection.Info(message) ssestream.BroadcastDetectionEvent(message) - discordbot.SendMessageToStatusChannel(message) + discordbot.SendMessageToEventLogChannel(message) }, EventServerError: func(event Event) { message := "🎮 [Gameserver] ⚠️ Server error detected" logger.Detection.Info(message) ssestream.BroadcastDetectionEvent(message) - discordbot.SendMessageToStatusChannel(message) + discordbot.SendMessageToEventLogChannel(message) }, EventSettingsChanged: func(event Event) { message := fmt.Sprintf("🎮 [Gameserver] ⚙️ %s", event.Message) logger.Detection.Info(message) ssestream.BroadcastDetectionEvent(message) - discordbot.SendMessageToStatusChannel(message) + discordbot.SendMessageToEventLogChannel(message) }, EventServerHosted: func(event Event) { message := fmt.Sprintf("🎮 [Gameserver] 🌐 %s", event.Message) logger.Detection.Info(message) ssestream.BroadcastDetectionEvent(message) - discordbot.SendMessageToStatusChannel(message) + discordbot.SendMessageToEventLogChannel(message) }, EventNewGameStarted: func(event Event) { message := fmt.Sprintf("🎮 [Gameserver] 🎲 %s", event.Message) logger.Detection.Info(message) ssestream.BroadcastDetectionEvent(message) - discordbot.SendMessageToStatusChannel(message) + discordbot.SendMessageToEventLogChannel(message) }, EventVersionExtracted: func(event Event) { message := fmt.Sprintf("🎮 [Gameserver] 📦 Version %s detected", event.Message) logger.Detection.Info(message) ssestream.BroadcastDetectionEvent(message) - discordbot.SendMessageToStatusChannel(message) + discordbot.SendMessageToEventLogChannel(message) }, EventServerRunning: func(event Event) { message := "🎮 [Gameserver] ✅ Server process has started!" logger.Detection.Info(message) ssestream.BroadcastDetectionEvent(message) - discordbot.SendMessageToStatusChannel(message) + discordbot.SendMessageToEventLogChannel(message) }, EventPlayerConnecting: func(event Event) { if event.PlayerInfo != nil { @@ -86,7 +86,7 @@ func DefaultHandlers() map[EventType]Handler { event.PlayerInfo.Username, event.PlayerInfo.SteamID) logger.Detection.Info(message) ssestream.BroadcastDetectionEvent(message) - discordbot.SendMessageToStatusChannel(message) + discordbot.SendMessageToEventLogChannel(message) } }, EventPlayerReady: func(event Event) { @@ -95,7 +95,7 @@ func DefaultHandlers() map[EventType]Handler { event.PlayerInfo.Username, event.PlayerInfo.SteamID) logger.Detection.Info(message) ssestream.BroadcastDetectionEvent(message) - discordbot.SendMessageToStatusChannel(message) + discordbot.SendMessageToEventLogChannel(message) } }, EventPlayerDisconnect: func(event Event) { @@ -104,7 +104,7 @@ func DefaultHandlers() map[EventType]Handler { event.PlayerInfo.Username) logger.Detection.Info(message) ssestream.BroadcastDetectionEvent(message) - discordbot.SendMessageToStatusChannel(message) + discordbot.SendMessageToEventLogChannel(message) } }, EventWorldSaved: func(event Event) { @@ -124,7 +124,7 @@ func DefaultHandlers() map[EventType]Handler { logger.Detection.Info(message) ssestream.BroadcastDetectionEvent(message) - discordbot.SendMessageToSavesChannel(message) + discordbot.SendMessageToEventLogChannel(message) }, EventException: func(event Event) { // Initial alert message diff --git a/src/managers/gamemgr/passwordrotation.go b/src/managers/gamemgr/passwordrotation.go index bfd2d2c7..c6f6fffb 100644 --- a/src/managers/gamemgr/passwordrotation.go +++ b/src/managers/gamemgr/passwordrotation.go @@ -13,7 +13,7 @@ import ( // rotatePasswordIfEnabled checks if password rotation is enabled and sets a new random password func rotatePasswordIfEnabled() { - if !config.GetIsDiscordEnabled() || config.GetServerInfoPanelChannelID() == "" || !config.GetRotateServerPassword() { + if !config.GetIsDiscordEnabled() || config.GetStatusPanelChannelID() == "" || !config.GetRotateServerPassword() { return } diff --git a/src/web/configpage.go b/src/web/configpage.go index 3e0af67b..c37c0c84 100644 --- a/src/web/configpage.go +++ b/src/web/configpage.go @@ -220,12 +220,10 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { // Config values DiscordToken: config.GetDiscordToken(), ControlChannelID: config.GetControlChannelID(), - StatusChannelID: config.GetStatusChannelID(), - ConnectionListChannelID: config.GetConnectionListChannelID(), + EventLogChannelID: config.GetEventLogChannelID(), + StatusPanelChannelID: config.GetStatusPanelChannelID(), LogChannelID: config.GetLogChannelID(), - SaveChannelID: config.GetSaveChannelID(), ControlPanelChannelID: config.GetControlPanelChannelID(), - ServerInfoPanelChannelID: config.GetServerInfoPanelChannelID(), BlackListFilePath: config.GetBlackListFilePath(), ErrorChannelID: config.GetErrorChannelID(), IsDiscordEnabled: fmt.Sprintf("%v", config.GetIsDiscordEnabled()), @@ -374,18 +372,14 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { UIText_AdminCommandChannelInfo: localization.GetString("UIText_AdminCommandChannelInfo"), UIText_ControlPanelChannel: localization.GetString("UIText_ControlPanelChannel"), UIText_ControlPanelChannelInfo: localization.GetString("UIText_ControlPanelChannelInfo"), - UIText_ServerInfoPanelChannel: localization.GetString("UIText_ServerInfoPanelChannel"), - UIText_ServerInfoPanelChannelInfo: localization.GetString("UIText_ServerInfoPanelChannelInfo"), UIText_RotateServerPassword: localization.GetString("UIText_RotateServerPassword"), UIText_RotateServerPasswordInfo: localization.GetString("UIText_RotateServerPasswordInfo"), - UIText_StatusChannel: localization.GetString("UIText_StatusChannel"), - UIText_StatusChannelInfo: localization.GetString("UIText_StatusChannelInfo"), - UIText_ConnectionListChannel: localization.GetString("UIText_ConnectionListChannel"), - UIText_ConnectionListChannelInfo: localization.GetString("UIText_ConnectionListChannelInfo"), + UIText_EventLogChannel: localization.GetString("UIText_EventLogChannel"), + UIText_EventLogChannelInfo: localization.GetString("UIText_EventLogChannelInfo"), + UIText_StatusPanelChannel: localization.GetString("UIText_StatusPanelChannel"), + UIText_StatusPanelChannelInfo: localization.GetString("UIText_StatusPanelChannelInfo"), UIText_LogChannel: localization.GetString("UIText_LogChannel"), UIText_LogChannelInfo: localization.GetString("UIText_LogChannelInfo"), - UIText_SaveInfoChannel: localization.GetString("UIText_SaveInfoChannel"), - UIText_SaveInfoChannelInfo: localization.GetString("UIText_SaveInfoChannelInfo"), UIText_ErrorChannel: localization.GetString("UIText_ErrorChannel"), UIText_ErrorChannelInfo: localization.GetString("UIText_ErrorChannelInfo"), UIText_BannedPlayersListPath: localization.GetString("UIText_BannedPlayersListPath"), diff --git a/src/web/templatevars.go b/src/web/templatevars.go index b274ea84..e38af1e9 100644 --- a/src/web/templatevars.go +++ b/src/web/templatevars.go @@ -30,12 +30,10 @@ type ConfigTemplateData struct { // Config values DiscordToken string ControlChannelID string - StatusChannelID string - ConnectionListChannelID string + EventLogChannelID string + StatusPanelChannelID string LogChannelID string - SaveChannelID string ControlPanelChannelID string - ServerInfoPanelChannelID string BlackListFilePath string ErrorChannelID string IsDiscordEnabled string @@ -186,18 +184,14 @@ type ConfigTemplateData struct { UIText_AdminCommandChannelInfo string UIText_ControlPanelChannel string UIText_ControlPanelChannelInfo string - UIText_ServerInfoPanelChannel string - UIText_ServerInfoPanelChannelInfo string UIText_RotateServerPassword string UIText_RotateServerPasswordInfo string - UIText_StatusChannel string - UIText_StatusChannelInfo string - UIText_ConnectionListChannel string - UIText_ConnectionListChannelInfo string + UIText_EventLogChannel string + UIText_EventLogChannelInfo string + UIText_StatusPanelChannel string + UIText_StatusPanelChannelInfo string UIText_LogChannel string UIText_LogChannelInfo string - UIText_SaveInfoChannel string - UIText_SaveInfoChannelInfo string UIText_ErrorChannel string UIText_ErrorChannelInfo string UIText_BannedPlayersListPath string From 1b2f1a3dfa23e6a3242fb323ced3d33afb186162 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sat, 14 Feb 2026 18:36:53 +0100 Subject: [PATCH 16/20] bunp version --- src/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.go b/src/config/config.go index ae968fa1..164d57ad 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -11,7 +11,7 @@ import ( var ( // All configuration variables can be found in vars.go - Version = "5.13.0" + Version = "5.13.1" Branch = "release" ) From fd78d742dc2e6c42d8e86ffb01e0e6fd0c26b977 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 15 Feb 2026 13:03:59 +0100 Subject: [PATCH 17/20] Remove error channel references and update exception detection message handling to use event log channel instead --- UIMod/onboard_bundled/localization/de-DE.json | 2 -- UIMod/onboard_bundled/localization/en-US.json | 2 -- UIMod/onboard_bundled/localization/sv-SE.json | 2 -- UIMod/onboard_bundled/ui/config.html | 6 ---- src/config/config.go | 3 -- src/config/getters.go | 6 ---- src/config/setters.go | 9 ----- src/config/vars.go | 1 - src/core/loader/helpers.go | 1 - src/discordbot/sendMessage.go | 35 +++++++------------ src/managers/detectionmgr/detector.go | 22 ++++++------ src/managers/detectionmgr/handlers.go | 6 ++-- src/web/configpage.go | 3 -- src/web/templatevars.go | 3 -- 14 files changed, 27 insertions(+), 74 deletions(-) diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/UIMod/onboard_bundled/localization/de-DE.json index 9dbeeff4..90dff033 100644 --- a/UIMod/onboard_bundled/localization/de-DE.json +++ b/UIMod/onboard_bundled/localization/de-DE.json @@ -130,8 +130,6 @@ "UIText_StatusPanelChannelInfo": "Optionaler Channel für das Server-Status-Panel mit Serverinfo und verbundenen Spielern. Wenn ein Server-Passwort gesetzt ist, wird hier auch ein Button zum Abrufen des Passworts angezeigt. Wenn leer, wird dieses Verhalten übersprungen.", "UIText_LogChannel": "Protokollkanal", "UIText_LogChannelInfo": "Optionaler Channel, der die vollständige Gameserver-Log-Ausgabe anzeigt. Wenn leer, wird dieses Verhalten übersprungen.", - "UIText_ErrorChannel": "Fehlerkanal", - "UIText_ErrorChannelInfo": "Optionaler Channel, der Serverfehler (Exceptions) anzeigt, wenn einer auftritt. Wenn leer, wird dieses Verhalten übersprungen.", "UIText_BannedPlayersListPath": "Gesperrte Spieler Liste Pfad", "UIText_BannedPlayersListPathInfo": "Dateipfad zur gesperrten Spieler Liste", "UIText_DiscordIntegrationBenefits": "Discord Integration Vorteile", diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index f314f88e..02cf7b23 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -134,8 +134,6 @@ "UIText_StatusPanelChannelInfo": "Optional Channel for the server status panel showing server info and connected players. If a server password is set, a button to get the password will also be displayed here. If empty, this behaviour is skipped.", "UIText_LogChannel": "Log Channel", "UIText_LogChannelInfo": "Optional channel that displays the full Gameserver log output. If empty, this behaviour is skipped.", - "UIText_ErrorChannel": "Error Channel", - "UIText_ErrorChannelInfo": "Optional channel that displays Server Errors (Exceptions) when one occurs. If empty, this behaviour is skipped.", "UIText_BannedPlayersListPath": "Banned Players List Path", "UIText_BannedPlayersListPathInfo": "File path to banned players list", "UIText_DiscordIntegrationBenefits": "Discord Integration Benefits", diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/UIMod/onboard_bundled/localization/sv-SE.json index defcb2cf..bea23d8d 100644 --- a/UIMod/onboard_bundled/localization/sv-SE.json +++ b/UIMod/onboard_bundled/localization/sv-SE.json @@ -128,8 +128,6 @@ "UIText_StatusPanelChannelInfo": "Valfri kanal för serverstatuspanelen som visar serverinfo och anslutna spelare. Om ett serverlösenord är inställt visas även en knapp för att hämta lösenordet här. Om tom, hoppas detta beteende över.", "UIText_LogChannel": "Loggkanal", "UIText_LogChannelInfo": "Valfri kanal som visar fullständig spelserverloggutdata. Om tom, hoppas detta beteende över.", - "UIText_ErrorChannel": "Felkanal", - "UIText_ErrorChannelInfo": "Valfri kanal som visar serverfel (Exceptions) när ett uppstår. Om tom, hoppas detta beteende över.", "UIText_BannedPlayersListPath": "Sökväg till bannlysta spelare", "UIText_BannedPlayersListPathInfo": "Filsökväg till listan över bannlysta spelare", "UIText_DiscordIntegrationBenefits": "Fördelar med Discord-integration", diff --git a/UIMod/onboard_bundled/ui/config.html b/UIMod/onboard_bundled/ui/config.html index c46d9561..95b3a538 100644 --- a/UIMod/onboard_bundled/ui/config.html +++ b/UIMod/onboard_bundled/ui/config.html @@ -537,12 +537,6 @@

{{.UIText_ChannelConfiguration}}

{{.UIText_LogChannelInfo}}
- -
- - -
{{.UIText_ErrorChannelInfo}}
-
diff --git a/src/config/config.go b/src/config/config.go index 164d57ad..8f448602 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -102,7 +102,6 @@ type JsonConfig struct { BlackListFilePath string `json:"blackListFilePath"` IsDiscordEnabled *bool `json:"isDiscordEnabled"` RotateServerPassword *bool `json:"rotateServerPassword"` - ErrorChannelID string `json:"errorChannelID"` //Backup Settings BackupKeepLastN int `json:"backupKeepLastN"` // Number of most recent backups to keep (default: 2000) @@ -161,7 +160,6 @@ func applyConfig(cfg *JsonConfig) { RotateServerPassword = rotateServerPasswordVal cfg.RotateServerPassword = &rotateServerPasswordVal - ErrorChannelID = getString(cfg.ErrorChannelID, "ERROR_CHANNEL_ID", "") BackupKeepLastN = getInt(cfg.BackupKeepLastN, "BACKUP_KEEP_LAST_N", 2000) isCleanupEnabledVal := getBool(cfg.IsCleanupEnabled, "IS_CLEANUP_ENABLED", false) @@ -387,7 +385,6 @@ func safeSaveConfig() error { BlackListFilePath: BlackListFilePath, IsDiscordEnabled: &IsDiscordEnabled, RotateServerPassword: &RotateServerPassword, - ErrorChannelID: ErrorChannelID, BackupKeepLastN: BackupKeepLastN, IsCleanupEnabled: &IsCleanupEnabled, BackupKeepDailyFor: int(BackupKeepDailyFor / time.Hour), // Convert to hours diff --git a/src/config/getters.go b/src/config/getters.go index febfa4bd..b7ace57f 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -64,12 +64,6 @@ func GetRotateServerPassword() bool { return RotateServerPassword } -func GetErrorChannelID() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return ErrorChannelID -} - func GetBackupKeepLastN() int { ConfigMu.RLock() defer ConfigMu.RUnlock() diff --git a/src/config/setters.go b/src/config/setters.go index 8744f1da..7839e082 100644 --- a/src/config/setters.go +++ b/src/config/setters.go @@ -537,15 +537,6 @@ func SetLogChannelID(value string) error { return safeSaveConfig() } -// SetErrorChannelID sets the ErrorChannelID -func SetErrorChannelID(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - ErrorChannelID = value - return safeSaveConfig() -} - // SetStatusPanelChannelID sets the StatusPanelChannelID func SetStatusPanelChannelID(value string) error { ConfigMu.Lock() diff --git a/src/config/vars.go b/src/config/vars.go index 86308c98..692fbaa6 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -89,7 +89,6 @@ var ( ControlChannelID string EventLogChannelID string LogChannelID string - ErrorChannelID string StatusPanelChannelID string ControlPanelChannelID string DiscordCharBufferSize int diff --git a/src/core/loader/helpers.go b/src/core/loader/helpers.go index 51e07c7b..765f41d0 100644 --- a/src/core/loader/helpers.go +++ b/src/core/loader/helpers.go @@ -82,7 +82,6 @@ func PrintConfigDetails(logLevel ...string) { "StatusPanelChannelID": config.GetStatusPanelChannelID(), "LogChannelID": config.GetLogChannelID(), "ControlPanelChannelID": config.GetControlPanelChannelID(), - "ErrorChannelID": config.GetErrorChannelID(), "DiscordCharBufferSize": fmt.Sprintf("%d", config.GetDiscordCharBufferSize()), "BlackListFilePath": config.GetBlackListFilePath(), } diff --git a/src/discordbot/sendMessage.go b/src/discordbot/sendMessage.go index 55afd07c..357321d9 100644 --- a/src/discordbot/sendMessage.go +++ b/src/discordbot/sendMessage.go @@ -46,30 +46,19 @@ func SendMessageToEventLogChannel(message string) { logger.Discord.Error("Discord Error: Discord is enabled but session is not initialized") return } - //clearMessagesAboveLastN(config.EventLogChannelID, 10) - _, err := config.DiscordSession.ChannelMessageSend(config.GetEventLogChannelID(), message) - if err != nil { - logger.Discord.Error("Error sending message to game event log channel: " + err.Error()) - } -} - -func SendMessageToErrorChannel(message string) { - if !config.GetIsDiscordEnabled() { - return - } - if config.GetErrorChannelID() == "" { - return - } - - if config.DiscordSession == nil { - logger.Discord.Error("Discord Error: Discord is enabled but session is not initialized") + if len(message) <= 2000 { + //clearMessagesAboveLastN(config.EventLogChannelID, 10) + _, err := config.DiscordSession.ChannelMessageSend(config.GetEventLogChannelID(), message) + if err != nil { + logger.Discord.Error("Error sending message to EventLog channel: " + err.Error()) + } return } maxMessageLength := 2000 // Discord's message character limit - // Function to split the message into chunks and send each one + // Function to split the message into chunks and send each one in case the message (primilariy exception stack traces) exceeds the Discord message limit for len(message) > 0 { if len(message) > maxMessageLength { // Find a safe split point, for example, the last newline before the limit @@ -79,9 +68,9 @@ func SendMessageToErrorChannel(message string) { } // Send the chunk - _, err := config.DiscordSession.ChannelMessageSend(config.GetErrorChannelID(), message[:splitIndex]) + _, err := config.DiscordSession.ChannelMessageSend(config.GetEventLogChannelID(), message[:splitIndex]) if err != nil { - logger.Discord.Error("Error sending message to error channel: " + err.Error()) + logger.Discord.Error("Error sending message to EventLog channel: " + err.Error()) return } @@ -89,10 +78,10 @@ func SendMessageToErrorChannel(message string) { message = message[splitIndex:] } else { // Send the remaining part of the message - _, err := config.DiscordSession.ChannelMessageSend(config.GetErrorChannelID(), message) + _, err := config.DiscordSession.ChannelMessageSend(config.GetEventLogChannelID(), message) if err != nil { - logger.Discord.Error("Error sending message to error channel: " + err.Error()) - return // Return whatever was sent before the error + logger.Discord.Error("Error sending message to EventLog channel: " + err.Error()) + return } break } diff --git a/src/managers/detectionmgr/detector.go b/src/managers/detectionmgr/detector.go index 010cdbb2..0db5a1fb 100644 --- a/src/managers/detectionmgr/detector.go +++ b/src/managers/detectionmgr/detector.go @@ -3,6 +3,7 @@ package detectionmgr import ( "fmt" + "maps" "regexp" "strings" "time" @@ -197,7 +198,7 @@ func (d *Detector) processRegexPatterns(logMessage string) { }) }, }, - { + { // Setting changed pattern pattern: regexp.MustCompile(`\d{2}:\d{2}:\d{2}: Changed setting '(.+?)' from '(.+?)' to '(.+?)'`), handler: func(matches []string, logMessage string) { settingName := matches[1] @@ -211,20 +212,21 @@ func (d *Detector) processRegexPatterns(logMessage string) { }) }, }, - { - pattern: regexp.MustCompile(`RocketNet Succesfully hosted with Address: (.+?) Port: (\d+)`), + { //RakNet hosted pattern + pattern: regexp.MustCompile(`(RocketNet|RakNet) Succesfully hosted with Address: (.+?) Port: (\d+)`), handler: func(matches []string, logMessage string) { - address := matches[1] - port := matches[2] + raknetType := matches[1] // purely cosmetic + address := matches[2] + port := matches[3] d.triggerEvent(Event{ Type: EventServerHosted, - Message: fmt.Sprintf("RocketNet Server hosted at %s:%s", address, port), + Message: fmt.Sprintf("%s Server hosted at %s:%s", raknetType, address, port), RawLog: logMessage, Timestamp: time.Now().Format(time.RFC3339), }) }, }, - { + { // New game started pattern pattern: regexp.MustCompile(`Started new game in world (.+)`), handler: func(matches []string, logMessage string) { worldName := matches[1] @@ -236,7 +238,7 @@ func (d *Detector) processRegexPatterns(logMessage string) { }) }, }, - { + { // Version extracted pattern pattern: regexp.MustCompile(`Version\s*:\s*(\d+\.\d+\.\d+\.\d+)`), handler: func(matches []string, logMessage string) { version := matches[1] @@ -270,9 +272,7 @@ func (d *Detector) triggerEvent(event Event) { // GetConnectedPlayers returns a copy of the connected players map func (d *Detector) GetConnectedPlayers() map[string]string { players := make(map[string]string) - for k, v := range d.connectedPlayers { - players[k] = v - } + maps.Copy(players, d.connectedPlayers) return players } diff --git a/src/managers/detectionmgr/handlers.go b/src/managers/detectionmgr/handlers.go index 12461870..cb63fc2c 100644 --- a/src/managers/detectionmgr/handlers.go +++ b/src/managers/detectionmgr/handlers.go @@ -126,12 +126,14 @@ func DefaultHandlers() map[EventType]Handler { ssestream.BroadcastDetectionEvent(message) discordbot.SendMessageToEventLogChannel(message) }, + + // not sure if this Detector still works, so I changed this to SendMessageToEventLogChannel instead of SendMessageToErrorChannel. EventException: func(event Event) { // Initial alert message alertMessage := "🎮 [Gameserver] 🚨 Exception detected!" logger.Detection.Info(alertMessage) ssestream.BroadcastDetectionEvent(alertMessage) - discordbot.SendMessageToErrorChannel(alertMessage) + discordbot.SendMessageToEventLogChannel(alertMessage) if event.ExceptionInfo != nil && len(event.ExceptionInfo.StackTrace) > 0 { // Format stack trace as a single-line string for SSE compatibility @@ -140,7 +142,7 @@ func DefaultHandlers() map[EventType]Handler { logger.Detection.Info(message) ssestream.BroadcastDetectionEvent(message) - discordbot.SendMessageToErrorChannel(message) + discordbot.SendMessageToEventLogChannel(message) } }, } diff --git a/src/web/configpage.go b/src/web/configpage.go index c37c0c84..73dcb588 100644 --- a/src/web/configpage.go +++ b/src/web/configpage.go @@ -225,7 +225,6 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { LogChannelID: config.GetLogChannelID(), ControlPanelChannelID: config.GetControlPanelChannelID(), BlackListFilePath: config.GetBlackListFilePath(), - ErrorChannelID: config.GetErrorChannelID(), IsDiscordEnabled: fmt.Sprintf("%v", config.GetIsDiscordEnabled()), IsDiscordEnabledTrueSelected: discordTrueSelected, IsDiscordEnabledFalseSelected: discordFalseSelected, @@ -380,8 +379,6 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { UIText_StatusPanelChannelInfo: localization.GetString("UIText_StatusPanelChannelInfo"), UIText_LogChannel: localization.GetString("UIText_LogChannel"), UIText_LogChannelInfo: localization.GetString("UIText_LogChannelInfo"), - UIText_ErrorChannel: localization.GetString("UIText_ErrorChannel"), - UIText_ErrorChannelInfo: localization.GetString("UIText_ErrorChannelInfo"), UIText_BannedPlayersListPath: localization.GetString("UIText_BannedPlayersListPath"), UIText_BannedPlayersListPathInfo: localization.GetString("UIText_BannedPlayersListPathInfo"), UIText_DiscordIntegrationBenefits: localization.GetString("UIText_DiscordIntegrationBenefits"), diff --git a/src/web/templatevars.go b/src/web/templatevars.go index e38af1e9..873659c3 100644 --- a/src/web/templatevars.go +++ b/src/web/templatevars.go @@ -35,7 +35,6 @@ type ConfigTemplateData struct { LogChannelID string ControlPanelChannelID string BlackListFilePath string - ErrorChannelID string IsDiscordEnabled string IsDiscordEnabledTrueSelected string IsDiscordEnabledFalseSelected string @@ -192,8 +191,6 @@ type ConfigTemplateData struct { UIText_StatusPanelChannelInfo string UIText_LogChannel string UIText_LogChannelInfo string - UIText_ErrorChannel string - UIText_ErrorChannelInfo string UIText_BannedPlayersListPath string UIText_BannedPlayersListPathInfo string UIText_DiscordIntegrationBenefits string From 5a4bea25e3367eef49440038a178c1eabc32f5a7 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 15 Feb 2026 14:23:08 +0100 Subject: [PATCH 18/20] feat: Implement theme editor and engine for user-customizable themes This builds upton the theming preperations already done with the xmas update. - Added a new theme editor UI for customizing theme colors and presets. - Introduced a theme engine to manage themes via CSS variables and localStorage. - Updated CSS variables for better organization and added derived variables. - Enhanced console messages to use theme colors dynamically. - Integrated theme editor into the configuration page - Added functionality for importing and exporting themes in JSON format. - Created preset themes for quick theme application. - Also fixes "Error sending log to Discord" bug --- UIMod/onboard_bundled/assets/css/apiinfo.css | 28 +- .../onboard_bundled/assets/css/background.css | 8 +- UIMod/onboard_bundled/assets/css/config.css | 116 ++-- .../assets/css/detectionmanager.css | 26 +- UIMod/onboard_bundled/assets/css/home.css | 158 +++--- .../assets/css/info-notice.css | 28 +- UIMod/onboard_bundled/assets/css/popup.css | 20 +- UIMod/onboard_bundled/assets/css/sscm.css | 14 +- .../assets/css/theme-editor.css | 242 +++++++++ .../onboard_bundled/assets/css/variables.css | 132 +++-- .../assets/js/console-manager.js | 13 +- UIMod/onboard_bundled/assets/js/slp.js | 2 +- .../onboard_bundled/assets/js/theme-editor.js | 278 ++++++++++ .../onboard_bundled/assets/js/theme-engine.js | 511 ++++++++++++++++++ UIMod/onboard_bundled/ui/config.html | 15 +- UIMod/onboard_bundled/ui/index.html | 1 + src/discordbot/logstream.go | 8 +- 17 files changed, 1339 insertions(+), 261 deletions(-) create mode 100644 UIMod/onboard_bundled/assets/css/theme-editor.css create mode 100644 UIMod/onboard_bundled/assets/js/theme-editor.js create mode 100644 UIMod/onboard_bundled/assets/js/theme-engine.js diff --git a/UIMod/onboard_bundled/assets/css/apiinfo.css b/UIMod/onboard_bundled/assets/css/apiinfo.css index 8d160e4f..73c59b55 100644 --- a/UIMod/onboard_bundled/assets/css/apiinfo.css +++ b/UIMod/onboard_bundled/assets/css/apiinfo.css @@ -16,17 +16,17 @@ .version-tag { background-color: var(--accent); - color: white; + color: var(--text-header); } .status-tag { background-color: var(--success); - color: white; + color: var(--text-header); } .api-overview { margin-bottom: 30px; - background-color: rgba(0, 0, 0, 0.2); + background-color: var(--form-bg); padding: 20px; border-radius: 8px; border-left: 4px solid var(--accent); @@ -46,16 +46,16 @@ } .endpoint { - background-color: rgba(0, 0, 0, 0.2); + background-color: var(--form-bg); padding: 15px; border-radius: 8px; - border: 1px solid rgba(0, 255, 171, 0.3); + border: 1px solid var(--primary-glow); transition: transform var(--transition-normal), box-shadow var(--transition-normal); } .endpoint:hover { transform: translateY(-3px); - box-shadow: 0 5px 15px rgba(0, 255, 171, 0.2); + box-shadow: 0 5px 15px var(--primary-bg-medium); } .api-list { @@ -66,7 +66,7 @@ .api-list li { margin-bottom: 15px; padding: 10px; - background-color: rgba(0, 0, 0, 0.4); + background-color: var(--nav-bg); border-radius: 4px; display: flex; flex-wrap: wrap; @@ -76,7 +76,7 @@ } .api-list li:hover { - background-color: rgba(0, 0, 0, 0.6); + background-color: var(--nav-bg-hover); } .method { @@ -84,15 +84,15 @@ border-radius: 4px; font-size: 0.7rem; font-weight: bold; - color: white; + color: var(--text-header); } .method.get { - background-color: #61affe; + background-color: var(--accent); } .method.post { - background-color: #49cc90; + background-color: var(--success); } .endpoint-link { @@ -107,7 +107,7 @@ a.endpoint-link:hover { .endpoint-desc { font-size: 0.85rem; - color: #aaa; + color: var(--text-dim); width: 100%; margin-top: 5px; } @@ -117,7 +117,7 @@ a.endpoint-link:hover { } .code-block { - background-color: rgba(0, 0, 0, 0.4); + background-color: var(--nav-bg); padding: 20px; border-radius: 8px; border-left: 4px solid var(--accent); @@ -125,6 +125,6 @@ a.endpoint-link:hover { } .hint { - color: #aaa; + color: var(--text-dim); font-style: italic; } \ No newline at end of file diff --git a/UIMod/onboard_bundled/assets/css/background.css b/UIMod/onboard_bundled/assets/css/background.css index 10282311..bc4ac966 100644 --- a/UIMod/onboard_bundled/assets/css/background.css +++ b/UIMod/onboard_bundled/assets/css/background.css @@ -16,8 +16,8 @@ header { position: absolute; inset: 0; background: - linear-gradient(rgba(0, 255, 171, 0.05) 1px, transparent 1px), - linear-gradient(90deg, rgba(0, 255, 171, 0.05) 1px, transparent 1px); + linear-gradient(color-mix(in srgb, var(--primary) 5%, transparent) 1px, transparent 1px), + linear-gradient(90deg, color-mix(in srgb, var(--primary) 5%, transparent) 1px, transparent 1px); background-size: 20px 20px; background-position: center center; z-index: 0; @@ -29,7 +29,7 @@ header { left: 0; width: 100%; height: 100%; - background: radial-gradient(ellipse at bottom, #1b2735 0%, var(--bg-dark) 100%); + background: radial-gradient(ellipse at bottom, color-mix(in srgb, var(--bg-dark) 70%, var(--bg-panel)) 0%, var(--bg-dark) 100%); z-index: -2; will-change: transform; overflow: hidden; @@ -106,7 +106,7 @@ header { radial-gradient(1px 1px at 102px 64px, var(--text-bright), transparent), radial-gradient(1px 1px at 127px 187px, var(--text-header), transparent), radial-gradient(700px 500px at 65% 75%, var(--primary-glow), transparent), - radial-gradient(500px 400px at 20% 25%, rgba(0, 132, 255, 0.15), transparent); + radial-gradient(500px 400px at 20% 25%, color-mix(in srgb, var(--accent) 15%, transparent), transparent); background-size: 250px 250px, 250px 250px, 250px 250px, 250px 250px, 250px 250px, 250px 250px, 250px 250px, 250px 250px, 250px 250px, 250px 250px, 100% 100%, 100% 100%; diff --git a/UIMod/onboard_bundled/assets/css/config.css b/UIMod/onboard_bundled/assets/css/config.css index f4dea0d1..ff3d3d2e 100644 --- a/UIMod/onboard_bundled/assets/css/config.css +++ b/UIMod/onboard_bundled/assets/css/config.css @@ -256,7 +256,7 @@ .fill-hint { font-size: 1rem; - color: #ffffff; + color: var(--text-header); padding: 2%; font-style: italic; text-align: center; @@ -312,7 +312,7 @@ select option { .input-info { font-size: 0.85rem; - color: #aaa; + color: var(--text-dim); margin-top: 5px; font-style: italic; } @@ -331,7 +331,7 @@ select option { gap: 15px; margin-bottom: 30px; padding: 15px; - background-color: rgba(0, 0, 0, 0.3); + background-color: var(--overlay-bg); border-radius: 8px; transition: transform var(--transition-normal), box-shadow var(--transition-normal); } @@ -349,7 +349,7 @@ select option { .feature-badge { background-color: var(--primary); - color: black; + color: var(--bg-dark); padding: 5px 10px; border-radius: 20px; font-size: 0.7rem; @@ -402,7 +402,7 @@ select option { .feature-list li { padding: 8px; margin-bottom: 10px; - background-color: rgba(0, 0, 0, 0.3); + background-color: var(--overlay-bg); border-radius: 4px; border-left: 3px solid var(--accent); transition: transform var(--transition-fast), background-color var(--transition-fast); @@ -410,7 +410,7 @@ select option { .feature-list li:hover { transform: translateX(3px); - background-color: rgba(0, 0, 0, 0.4); + background-color: var(--nav-bg); } @media (max-width: 768px) { @@ -455,11 +455,11 @@ select option { .slp-section:hover { border-color: var(--primary); - box-shadow: 0 0 15px rgba(0, 255, 150, 0.1); + box-shadow: 0 0 15px var(--primary-glow-soft); } .slp-section p { - color: var(--text-normal, #aaa); + color: var(--text-dim); font-family: 'Share Tech Mono', monospace; font-size: 0.9rem; line-height: 1.6; @@ -477,15 +477,15 @@ select option { } .slp-install-section { - background: linear-gradient(135deg, rgba(0, 255, 150, 0.08), rgba(0, 150, 255, 0.08)); + background: linear-gradient(135deg, var(--primary-glow-soft), color-mix(in srgb, var(--accent) 8%, transparent)); border: 2px solid var(--primary); - box-shadow: 0 0 25px rgba(0, 255, 150, 0.15); + box-shadow: 0 0 25px var(--primary-bg-light); text-align: center; padding: 40px; } .slp-install-section:hover { - box-shadow: 0 0 35px rgba(0, 255, 150, 0.25); + box-shadow: 0 0 35px var(--primary-bg-medium); } .slp-install-header { @@ -498,7 +498,7 @@ select option { .slp-install-icon { font-size: 52px; - filter: drop-shadow(0 0 10px rgba(0, 255, 150, 0.5)); + filter: drop-shadow(0 0 10px var(--primary-glow-strong)); } .slp-install-header h3 { @@ -508,7 +508,7 @@ select option { margin: 0; padding: 0; border: none; - text-shadow: 0 0 10px rgba(0, 255, 150, 0.3); + text-shadow: 0 0 10px var(--primary-glow); } .slp-button { @@ -526,7 +526,7 @@ select option { transition: all 0.2s ease; font-family: 'Share Tech Mono', monospace; font-size: 0.85rem; - box-shadow: 0 4px 15px rgba(0, 255, 150, 0.25); + box-shadow: 0 4px 15px var(--primary-bg-medium); margin: 8px 0; text-transform: uppercase; letter-spacing: 0.5px; @@ -534,12 +534,12 @@ select option { .slp-button:hover:not(:disabled) { transform: translateY(-3px); - box-shadow: 0 6px 25px rgba(0, 255, 150, 0.4); + box-shadow: 0 6px 25px var(--primary-glow-medium); } .slp-button:active:not(:disabled) { transform: translateY(-1px); - box-shadow: 0 3px 15px rgba(0, 255, 150, 0.3); + box-shadow: 0 3px 15px var(--primary-glow); } .slp-button:disabled { @@ -563,11 +563,11 @@ select option { min-width: 320px; padding: 18px 36px; font-size: 0.9rem; - box-shadow: 0 4px 20px rgba(0, 255, 150, 0.35); + box-shadow: 0 4px 20px var(--primary-glow); } .slp-button-large:hover:not(:disabled) { - box-shadow: 0 8px 35px rgba(0, 255, 150, 0.5); + box-shadow: 0 8px 35px var(--primary-glow-strong); } .slp-button-small { @@ -577,19 +577,19 @@ select option { } .slp-button.danger { - background-color: #e53935; - border-color: #e53935; - box-shadow: 0 4px 15px rgba(229, 57, 53, 0.25); + background-color: var(--danger); + border-color: var(--danger); + box-shadow: 0 4px 15px color-mix(in srgb, var(--danger) 25%, transparent); } .slp-button.danger:hover:not(:disabled) { - background-color: #ff5252; - border-color: #ff5252; - box-shadow: 0 6px 25px rgba(255, 82, 82, 0.4); + background-color: color-mix(in srgb, var(--danger) 80%, white); + border-color: color-mix(in srgb, var(--danger) 80%, white); + box-shadow: 0 6px 25px color-mix(in srgb, var(--danger) 40%, transparent); } .slp-manage-section { - border: 1px solid rgba(0, 255, 150, 0.2); + border: 1px solid var(--primary-bg-medium); } .manage-buttons { @@ -604,7 +604,7 @@ select option { display: flex; flex-direction: column; padding: 15px; - background: rgba(0, 0, 0, 0.2); + background: var(--form-bg); border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.05); } @@ -635,23 +635,23 @@ select option { text-align: center; cursor: pointer; transition: all 0.25s ease; - background: linear-gradient(145deg, rgba(0, 255, 150, 0.03), rgba(0, 150, 255, 0.03)); + background: linear-gradient(145deg, var(--primary-bg-subtle), color-mix(in srgb, var(--accent) 3%, transparent)); position: relative; margin: 20px 0; } #modPackageUploadZone:hover:not(.upload-zone-disabled) { border-color: var(--primary); - background: linear-gradient(145deg, rgba(0, 255, 150, 0.08), rgba(0, 150, 255, 0.08)); - box-shadow: 0 0 25px rgba(0, 255, 150, 0.15); + background: linear-gradient(145deg, var(--primary-glow-soft), color-mix(in srgb, var(--accent) 8%, transparent)); + box-shadow: 0 0 25px var(--primary-bg-light); transform: translateY(-2px); } #modPackageUploadZone.highlight { border-color: var(--primary); border-style: solid; - background: linear-gradient(145deg, rgba(0, 255, 150, 0.12), rgba(0, 150, 255, 0.12)); - box-shadow: 0 0 30px rgba(0, 255, 150, 0.25); + background: linear-gradient(145deg, var(--primary-bg-light), color-mix(in srgb, var(--accent) 12%, transparent)); + box-shadow: 0 0 30px var(--primary-bg-medium); } .upload-zone-disabled { @@ -670,7 +670,7 @@ select option { font-size: 56px; margin-bottom: 18px; display: block; - filter: drop-shadow(0 0 8px rgba(0, 255, 150, 0.3)); + filter: drop-shadow(0 0 8px var(--primary-glow)); } .upload-text { @@ -722,29 +722,29 @@ select option { text-align: center; max-width: 500px; padding: 50px; - background: linear-gradient(135deg, rgba(255, 170, 0, 0.08), rgba(255, 100, 0, 0.08)); - border: 2px solid rgba(255, 170, 0, 0.5); + background: linear-gradient(135deg, color-mix(in srgb, var(--warning) 8%, transparent), color-mix(in srgb, var(--warning) 5%, transparent)); + border: 2px solid color-mix(in srgb, var(--warning) 50%, transparent); border-radius: 16px; - box-shadow: 0 0 30px rgba(255, 170, 0, 0.15); + box-shadow: 0 0 30px color-mix(in srgb, var(--warning) 15%, transparent); } .slp-disclaimer-icon { font-size: 64px; display: block; margin-bottom: 20px; - filter: drop-shadow(0 0 15px rgba(255, 170, 0, 0.5)); + filter: drop-shadow(0 0 15px color-mix(in srgb, var(--warning) 50%, transparent)); } .slp-disclaimer-content h2 { - color: #ffaa00; + color: var(--warning); font-family: 'Press Start 2P', cursive; font-size: 1.2rem; margin: 0 0 25px 0; - text-shadow: 0 0 15px rgba(255, 170, 0, 0.4); + text-shadow: 0 0 15px color-mix(in srgb, var(--warning) 40%, transparent); } .slp-disclaimer-content p { - color: var(--text-normal, #ccc); + color: var(--text-dim); font-family: 'Share Tech Mono', monospace; font-size: 1rem; line-height: 1.8; @@ -776,27 +776,27 @@ select option { } .notification.success { - background-color: rgba(0, 255, 150, 0.1); - border-color: #00ff96; - color: #00ff96; + background-color: color-mix(in srgb, var(--success) 10%, transparent); + border-color: var(--success); + color: var(--success); } .notification.error { - background-color: rgba(255, 68, 68, 0.1); - border-color: #ff4444; - color: #ff6666; + background-color: color-mix(in srgb, var(--danger) 10%, transparent); + border-color: var(--danger); + color: var(--danger); } .notification.info { - background-color: rgba(0, 200, 255, 0.1); - border-color: #00c8ff; - color: #00d4ff; + background-color: color-mix(in srgb, var(--accent) 10%, transparent); + border-color: var(--accent); + color: var(--accent); } .notification.warning { - background-color: rgba(255, 200, 0, 0.1); - border-color: #ffc800; - color: #ffd700; + background-color: color-mix(in srgb, var(--warning) 10%, transparent); + border-color: var(--warning); + color: var(--warning); } @keyframes slideIn { @@ -818,7 +818,7 @@ select option { } .mod-card { - background: #11111142; + background: color-mix(in srgb, var(--bg-dark) 80%, transparent); border: 2px solid var(--primary-dim); border-radius: 12px; padding: 20px; @@ -835,7 +835,7 @@ select option { .mod-image-container { width: 100%; height: 250px; - background: var(--input-bg, #0f0f1e); + background: var(--input-bg); border: 2px solid var(--primary-dim); border-radius: 8px; overflow: hidden; @@ -857,7 +857,7 @@ select option { display: flex; align-items: center; justify-content: center; - color: var(--text-dim, #888); + color: var(--text-dim); font-size: 3rem; } @@ -879,14 +879,14 @@ select option { } .mod-version { - color: var(--text-dim, #888); + color: var(--text-dim); font-size: 0.8rem; margin-bottom: 10px; font-family: 'Share Tech Mono', monospace; } .mod-description { - color: var(--text-normal, #ccc); + color: var(--text-dim); font-size: 0.85rem; line-height: 1.5; word-wrap: break-word; @@ -1004,11 +1004,11 @@ select option { .mods-empty { text-align: center; - color: var(--text-dim, #888); + color: var(--text-dim); padding: 60px 30px; font-family: 'Share Tech Mono', monospace; font-size: 0.95rem; - background: rgba(0, 0, 0, 0.2); + background: var(--form-bg); border-radius: 12px; border: 2px dashed var(--primary-dim); margin: 10px 0; diff --git a/UIMod/onboard_bundled/assets/css/detectionmanager.css b/UIMod/onboard_bundled/assets/css/detectionmanager.css index 6bf80a88..21658fb6 100644 --- a/UIMod/onboard_bundled/assets/css/detectionmanager.css +++ b/UIMod/onboard_bundled/assets/css/detectionmanager.css @@ -1,13 +1,13 @@ @import '/static/css/variables.css'; #detection-list-container { - background: rgba(114, 137, 218, 0.1); + background: var(--discord-bg); } /* Detection list */ .detection-list { margin-top: 2rem; - border: 1px solid #ccc; + border: 1px solid var(--text-dim); border-radius: 4px; overflow: hidden; } @@ -18,7 +18,7 @@ display: grid; grid-template-columns: 100px 2fr 2fr 120px; gap: 1rem; - border-bottom: 1px solid #ccc; + border-bottom: 1px solid var(--text-dim); } .detection-item { @@ -26,7 +26,7 @@ display: grid; grid-template-columns: 100px 2fr 2fr 120px; gap: 1rem; - border-bottom: 1px solid #eee; + border-bottom: 1px solid var(--text-dim); align-items: center; } @@ -87,7 +87,7 @@ bottom: 4px; transition: .4s; border-radius: 50%; - background-color: white; + background-color: var(--text-header); } input:checked+.slider { @@ -109,25 +109,25 @@ input:checked+.slider:before { .type-keyword { background-color: var(--primary); - color: #0066cc; + color: var(--bg-dark); } .type-regex { background-color: var(--primary); - color: #fd7e14; + color: var(--bg-dark); } .empty-list { padding: 2rem; text-align: center; - color: #666; + color: var(--text-dim); font-style: italic; } /* Loading spinner */ .loader { - border: 5px solid #f3f3f3; - border-top: 5px solid #0066cc; + border: 5px solid var(--surface-dark); + border-top: 5px solid var(--accent); border-radius: 50%; width: 40px; height: 40px; @@ -153,7 +153,7 @@ input:checked+.slider:before { right: 20px; padding: 1rem; border-radius: 4px; - color: white; + color: var(--text-header); font-weight: bold; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); display: none; @@ -168,14 +168,14 @@ input:checked+.slider:before { } .notification-error { - background-color: #dc3546; + background-color: var(--danger); } .form-group input, .form-group textarea { width: 100%; padding: 0.75rem; - border: 1px solid #ccc; + border: 1px solid var(--text-dim); border-radius: 4px; box-sizing: border-box; background: var(--bg-dark); diff --git a/UIMod/onboard_bundled/assets/css/home.css b/UIMod/onboard_bundled/assets/css/home.css index 1578a958..5108525a 100644 --- a/UIMod/onboard_bundled/assets/css/home.css +++ b/UIMod/onboard_bundled/assets/css/home.css @@ -15,7 +15,7 @@ text-align: center; line-height: 2; padding: 10px; - background-color: rgba(0, 255, 171, 0.05); + background-color: var(--primary-bg-subtle); border-radius: 5px; border-left: 3px solid var(--primary); transition: opacity var(--transition-fast); @@ -27,8 +27,8 @@ top: 22px; font-family: 'Share Tech Mono', monospace; font-size: 0.8rem; - color: rgba(255, 255, 255, 0.75); - background-color: rgba(0, 0, 0, 0.35); + color: var(--text-dim); + background-color: var(--overlay-bg); padding: 2px 8px; border-radius: 4px; letter-spacing: 0.5px; @@ -48,14 +48,14 @@ } .status-indicator.online { - background-color: #4CAF50; + background-color: var(--success); animation: pulse 2s ease-in-out infinite; - box-shadow: 0 0 10px rgba(76, 175, 80, 0.7); + box-shadow: 0 0 10px color-mix(in srgb, var(--success) 70%, transparent); } .status-indicator.offline { - background-color: #F44336; - box-shadow: 0 0 10px rgba(244, 67, 54, 0.7); + background-color: var(--danger); + box-shadow: 0 0 10px color-mix(in srgb, var(--danger) 70%, transparent); } .status-indicator.offline::before, @@ -66,7 +66,7 @@ left: 50%; width: 10px; height: 2px; - background-color: #FFFFFF; + background-color: var(--text-header); } .status-indicator.offline::before { @@ -78,25 +78,25 @@ } .status-indicator.error { - background-color: #FFC107; - box-shadow: 0 0 10px rgba(255, 193, 7, 0.7); + background-color: var(--warning); + box-shadow: 0 0 10px color-mix(in srgb, var(--warning) 70%, transparent); animation: shake 0.5s ease-in-out infinite; } @keyframes pulse { 0% { transform: scale(1); - box-shadow: 0 0 10px rgba(76, 175, 80, 0.7); + box-shadow: 0 0 10px color-mix(in srgb, var(--success) 70%, transparent); } 50% { transform: scale(1.2); - box-shadow: 0 0 14px rgba(76, 175, 80, 0.9); + box-shadow: 0 0 14px color-mix(in srgb, var(--success) 90%, transparent); } 100% { transform: scale(1); - box-shadow: 0 0 10px rgba(76, 175, 80, 0.7); + box-shadow: 0 0 10px color-mix(in srgb, var(--success) 70%, transparent); } } @@ -123,7 +123,7 @@ padding: 20px; height: 400px; overflow-y: auto; - background-color: rgba(0, 0, 0, 0.85); + background-color: var(--overlay-bg-heavy); color: var(--primary); margin-bottom: 30px; border-radius: 8px; @@ -131,7 +131,7 @@ font-family: 'Share Tech Mono', 'Courier New', monospace; position: relative; scrollbar-width: thin; - scrollbar-color: var(--primary) #000; + scrollbar-color: var(--primary) var(--bg-dark); display: flex; flex-direction: column; } @@ -145,7 +145,7 @@ #console::-webkit-scrollbar-track, #detection-console::-webkit-scrollbar-track, #backendlog-console::-webkit-scrollbar-track { - background: #000; + background: var(--bg-dark); border-radius: 4px; } @@ -170,7 +170,7 @@ content: ''; position: absolute; inset: 0; - background: linear-gradient(transparent 50%, rgba(0, 255, 171, 0.03) 50%); + background: linear-gradient(transparent 50%, var(--primary-bg-subtle) 50%); background-size: 100% 4px; pointer-events: none; opacity: 0.3; @@ -181,7 +181,7 @@ margin: 10px 0; border-radius: 4px; border-left: 3px solid var(--primary); - background-color: rgba(0, 255, 171, 0.05); + background-color: var(--primary-bg-subtle); transition: all var(--transition-normal); word-wrap: break-word; } @@ -191,7 +191,7 @@ margin: 5px 0; border-radius: 4px; border-left: 3px solid var(--primary); - background-color: rgba(0, 255, 171, 0.05); + background-color: var(--primary-bg-subtle); transition: all var(--transition-normal); word-wrap: break-word; } @@ -205,12 +205,12 @@ } .detection-event:hover { - background-color: rgba(0, 255, 171, 0.1); + background-color: var(--primary-glow-soft); transform: translateX(3px); } .event-timestamp { - color: #888; + color: var(--text-muted); font-size: 0.9em; margin-right: 5px; } @@ -237,26 +237,26 @@ } .event-player-disconnect { - border-left-color: #FF7700; + border-left-color: var(--warning); } .event-world-saved { - border-left-color: #00FFFF; + border-left-color: var(--accent); } .event-exception { border-left-color: var(--danger); - background-color: rgba(255, 0, 0, 0.1); + background-color: color-mix(in srgb, var(--danger) 10%, transparent); } /* Backups */ #backups { position: relative; margin-top: 40px; - background-color: rgba(0, 255, 171, 0.05); + background-color: var(--primary-bg-subtle); padding: 20px; border-radius: 8px; - border: 1px solid rgba(0, 255, 171, 0.3); + border: 1px solid var(--primary-glow); transition: transform var(--transition-normal); } @@ -268,10 +268,10 @@ #players { position: relative; margin-top: 40px; - background-color: rgba(0, 255, 171, 0.05); + background-color: var(--primary-bg-subtle); padding: 20px; border-radius: 8px; - border: 1px solid rgba(0, 255, 171, 0.3); + border: 1px solid var(--primary-glow); transition: transform var(--transition-normal); } @@ -293,21 +293,21 @@ } .player-item { - background-color: rgba(0, 0, 0, 0.4); + background-color: var(--nav-bg); padding: 15px; margin-bottom: 15px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; - border: 2px solid rgba(0, 255, 171, 0.5); + border: 2px solid var(--primary-glow-strong); transition: all var(--transition-normal); line-height: 1.6; } .player-item:hover, .player-item.animate-in:hover { - background-color: rgba(0, 0, 0, 0.6); + background-color: var(--nav-bg-hover); border-color: var(--primary); transform: translateX(5px); } @@ -338,7 +338,7 @@ .player-name { font-size: 1.1rem; - color: #fff; + color: var(--text-header); } /* Backups */ @@ -371,23 +371,23 @@ #backupLimit { padding: 8px 12px; - background-color: rgba(0, 0, 0, 0.6); + background-color: var(--input-bg); color: var(--text-bright); - border: 1px solid rgba(0, 255, 171, 0.5); + border: 1px solid var(--primary-glow-strong); border-radius: 4px; font-family: 'Press Start 2P', cursive; font-size: 0.7rem; } .backup-item { - background-color: rgba(0, 0, 0, 0.4); + background-color: var(--nav-bg); padding: 20px; margin-bottom: 15px; border-radius: 12px; display: flex; justify-content: space-between; align-items: center; - border: 2px solid rgba(0, 255, 171, 0.3); + border: 2px solid var(--primary-glow); transition: all var(--transition-normal); position: relative; overflow: hidden; @@ -399,7 +399,7 @@ .backup-item:hover, .backup-item.animate-in:hover { - background-color: rgba(0, 0, 0, 0.6); + background-color: var(--nav-bg-hover); border-color: var(--primary); transform: translateX(5px); } @@ -430,15 +430,15 @@ } .backup-type.preterrain-trio { - background-color: rgba(255, 165, 0, 0.2); - color: #ffa500; - border: 1px solid rgba(255, 165, 0, 0.4); + background-color: color-mix(in srgb, var(--warning) 20%, transparent); + color: var(--warning); + border: 1px solid color-mix(in srgb, var(--warning) 40%, transparent); } .backup-type.dotsave { - background-color: rgba(0, 255, 171, 0.2); + background-color: var(--primary-bg-medium); color: var(--primary); - border: 1px solid rgba(0, 255, 171, 0.4); + border: 1px solid var(--primary-glow-medium); } .backup-date { @@ -456,7 +456,7 @@ .restore-btn, .download-btn { padding: 10px 18px; - background-color: rgba(0, 255, 171, 0.1); + background-color: var(--primary-glow-soft); color: var(--text-bright); border: 2px solid var(--primary); border-radius: 8px; @@ -470,7 +470,7 @@ .restore-btn:hover, .download-btn:hover { background-color: var(--primary); - color: #000; + color: var(--bg-dark); } .no-backups, @@ -479,9 +479,9 @@ padding: 40px; color: var(--text-dim); font-style: italic; - background-color: rgba(0, 0, 0, 0.2); + background-color: var(--form-bg); border-radius: 8px; - border: 1px dashed rgba(0, 255, 171, 0.3); + border: 1px dashed var(--primary-glow); } @keyframes slideIn { @@ -504,13 +504,13 @@ left: 20px; width: 60px; height: 60px; - background: linear-gradient(135deg, #ff6b6b, #ff4444); + background: linear-gradient(135deg, var(--danger), color-mix(in srgb, var(--danger) 80%, black)); border-radius: 50%; cursor: pointer; display: none; align-items: center; justify-content: center; - box-shadow: 0 4px 20px rgba(255, 68, 68, 0.5); + box-shadow: 0 4px 20px color-mix(in srgb, var(--danger) 50%, transparent); z-index: 9999; animation: bounce 2s ease-in-out infinite; transition: transform 0.2s ease; @@ -523,7 +523,7 @@ #update-indicator-float::before { content: "↑"; font-size: 32px; - color: white; + color: var(--text-header); font-weight: bold; animation: pulse-icon 1.5s ease-in-out infinite; } @@ -535,7 +535,7 @@ right: -5px; width: 20px; height: 20px; - background: #fff; + background: var(--text-header); border-radius: 50%; animation: ping 2s cubic-bezier(0, 0, 0.2, 1) infinite; } @@ -585,7 +585,7 @@ left: 0; width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.8); + background: var(--overlay-bg-heavy); z-index: 10000; align-items: center; justify-content: center; @@ -596,13 +596,13 @@ } .update-modal-content { - background: linear-gradient(135deg, #2d2d44, #1a1a2e); - border: 3px solid #ff4444; + background: linear-gradient(135deg, var(--surface-hover), var(--bg-dark)); + border: 3px solid var(--danger); border-radius: 16px; padding: 40px; max-width: 500px; width: 90%; - box-shadow: 0 8px 40px rgba(255, 68, 68, 0.3); + box-shadow: 0 8px 40px color-mix(in srgb, var(--danger) 30%, transparent); position: relative; animation: modalSlideIn 0.3s ease-out; } @@ -625,7 +625,7 @@ right: 15px; background: transparent; border: none; - color: #888; + color: var(--text-muted); font-size: 28px; cursor: pointer; width: 35px; @@ -639,38 +639,38 @@ .update-modal-close:hover { background: rgba(255, 255, 255, 0.1); - color: #fff; + color: var(--text-header); } .update-modal-icon { width: 80px; height: 80px; margin: 0 auto 20px; - background: linear-gradient(135deg, #ff6b6b, #ff4444); + background: linear-gradient(135deg, var(--danger), color-mix(in srgb, var(--danger) 80%, black)); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 48px; - color: white; + color: var(--text-header); } .update-modal-content h2 { - color: #fff; + color: var(--text-header); margin: 0 0 15px 0; text-align: center; font-size: 24px; } .update-modal-content p { - color: #ccc; + color: var(--text-dim); text-align: center; margin: 0 0 10px 0; line-height: 1.6; } .update-version { - color: #ff6b6b; + color: var(--danger); font-weight: bold; font-size: 20px; text-align: center; @@ -695,24 +695,24 @@ } #update-now-btn { - background: linear-gradient(135deg, #4CAF50, #45a049); - color: white; + background: linear-gradient(135deg, var(--success), color-mix(in srgb, var(--success) 90%, black)); + color: var(--text-header); } #update-now-btn:hover { transform: translateY(-2px); - box-shadow: 0 4px 15px rgba(76, 175, 80, 0.4); + box-shadow: 0 4px 15px color-mix(in srgb, var(--success) 40%, transparent); } #update-later-btn { background: rgba(255, 255, 255, 0.1); - color: #ccc; + color: var(--text-dim); border: 2px solid rgba(255, 255, 255, 0.2); } #update-later-btn:hover { background: rgba(255, 255, 255, 0.15); - color: #fff; + color: var(--text-header); } /* Update Status Messages in Modal */ @@ -727,23 +727,23 @@ .update-status-message.running { display: block; - background: rgba(33, 150, 243, 0.2); - border: 2px solid #2196F3; - color: #2196F3; + background: color-mix(in srgb, var(--accent) 20%, transparent); + border: 2px solid var(--accent); + color: var(--accent); } .update-status-message.success { display: block; - background: rgba(76, 175, 80, 0.2); - border: 2px solid #4CAF50; - color: #4CAF50; + background: color-mix(in srgb, var(--success) 20%, transparent); + border: 2px solid var(--success); + color: var(--success); } .update-status-message.failed { display: block; - background: rgba(255, 68, 68, 0.2); - border: 2px solid #ff4444; - color: #ff4444; + background: color-mix(in srgb, var(--danger) 20%, transparent); + border: 2px solid var(--danger); + color: var(--danger); } #update-button { @@ -766,7 +766,7 @@ display: none; margin-top: 20px; font-weight: bold; - color: #fff; + color: var(--text-header); } .update-status-message.running { @@ -780,8 +780,8 @@ right: 45px; width: 32px; height: 32px; - background: #F44336; - box-shadow: 0 0 10px rgba(244, 67, 54, 0.7); + background: var(--danger); + box-shadow: 0 0 10px color-mix(in srgb, var(--danger) 70%, transparent); border: none; cursor: pointer; padding: 0; @@ -792,6 +792,6 @@ } .update-icon:hover { - background: #F44336; + background: var(--danger); transform: scale(1.1); } \ No newline at end of file diff --git a/UIMod/onboard_bundled/assets/css/info-notice.css b/UIMod/onboard_bundled/assets/css/info-notice.css index ebcd7b3b..1a090ef3 100644 --- a/UIMod/onboard_bundled/assets/css/info-notice.css +++ b/UIMod/onboard_bundled/assets/css/info-notice.css @@ -1,11 +1,11 @@ .info-notice { - border: 1px solid #4a90e2; + border: 1px solid var(--accent); border-radius: 8px; padding: 15px; margin: 15px 0; - background: rgba(74, 144, 226, 0.1); - color: #e0e0e0; - box-shadow: 0 2px 8px rgba(74, 144, 226, 0.2); + background: color-mix(in srgb, var(--accent) 10%, transparent); + color: var(--text-bright); + box-shadow: 0 2px 8px color-mix(in srgb, var(--accent) 20%, transparent); } .info-notice h3 { @@ -14,7 +14,7 @@ display: flex; align-items: center; gap: 8px; - color: #4a90e2; + color: var(--accent); cursor: pointer; } @@ -44,12 +44,12 @@ } .info-notice a { - color: #4a90e2; + color: var(--accent); text-decoration: underline; } .info-notice a:hover { - color: #7bb3f0; + color: color-mix(in srgb, var(--accent) 70%, white); } .notice-icon { @@ -58,13 +58,13 @@ } .update { - border: 1px solid #af534c; + border: 1px solid color-mix(in srgb, var(--danger) 70%, black); border-radius: 8px; padding: 12px; margin: 10px 0; - background: #784a47; - color: #c8e6c9; - box-shadow: 0 2px 6px rgba(76, 175, 80, 0.2); + background: color-mix(in srgb, var(--danger) 30%, var(--bg-dark)); + color: var(--text-bright); + box-shadow: 0 2px 6px color-mix(in srgb, var(--success) 20%, transparent); } .update h3 { @@ -73,7 +73,7 @@ display: flex; align-items: center; gap: 6px; - color: #4caf50; + color: var(--success); } .update p { @@ -83,12 +83,12 @@ } .status-good { - color: #4caf50; + color: var(--success); font-weight: 600; } .status-bad { - color: #f44336; + color: var(--danger); font-weight: 600; } diff --git a/UIMod/onboard_bundled/assets/css/popup.css b/UIMod/onboard_bundled/assets/css/popup.css index cdf64cb2..4bae180d 100644 --- a/UIMod/onboard_bundled/assets/css/popup.css +++ b/UIMod/onboard_bundled/assets/css/popup.css @@ -38,44 +38,44 @@ .popup-content button { margin-top: 15px; padding: 10px 20px; - color: white; + color: var(--text-header); border: none; border-radius: 3px; cursor: pointer; } .popup.error .popup-content { - border-left: 2px solid #ff4d4d; + border-left: 2px solid var(--danger); } .popup.error button { - background-color: #ff4d4d; + background-color: var(--danger); } .popup.error button:hover { - background-color: #e60000; + background-color: color-mix(in srgb, var(--danger) 80%, black); } .popup.success .popup-content { - border-left: 2px solid #28a745; + border-left: 2px solid var(--success); } .popup.success button { - background-color: #28a745; + background-color: var(--success); } .popup.success button:hover { - background-color: #218838; + background-color: color-mix(in srgb, var(--success) 80%, black); } .popup.info .popup-content { - border-left: 2px solid #17a2b8; + border-left: 2px solid var(--accent); } .popup.info button { - background-color: #17a2b8; + background-color: var(--accent); } .popup.info button:hover { - background-color: #138496; + background-color: color-mix(in srgb, var(--accent) 80%, black); } \ No newline at end of file diff --git a/UIMod/onboard_bundled/assets/css/sscm.css b/UIMod/onboard_bundled/assets/css/sscm.css index 4050fc57..7b647837 100644 --- a/UIMod/onboard_bundled/assets/css/sscm.css +++ b/UIMod/onboard_bundled/assets/css/sscm.css @@ -39,7 +39,7 @@ #sscm-command-input:disabled { background: rgba(255, 255, 255, 0.1); - color: var(--text-bright, #ffffff); + color: var(--text-bright); opacity: 0.7; cursor: not-allowed; } @@ -56,7 +56,7 @@ right: 10px; max-height: 250px; overflow-y: auto; - background: var(--bg-dark, #2d333b); + background: var(--bg-dark); border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); z-index: 1000; @@ -68,7 +68,7 @@ display: flex; align-items: flex-start; padding: 10px 14px; - color: var(--text-bright, #ffffff); + color: var(--text-bright); cursor: pointer; transition: all 0.2s ease; gap: 10px; @@ -76,8 +76,8 @@ .sscm-suggestion-item:hover, .sscm-suggestion-item.highlighted { - background: var(--primary-glow, #3b82f6); - color: #ffffff; + background: var(--primary-glow); + color: var(--text-header); } .sscm-suggestion-name { @@ -87,7 +87,7 @@ } .sscm-suggestion-params { - color: var(--text-muted, #d1d5db); + color: var(--text-bright); flex: 0 0 250px; overflow-wrap: break-word; line-height: 1.3; @@ -96,7 +96,7 @@ } .sscm-suggestion-desc { - color: var(--text-dim, #9ca3af); + color: var(--text-dim); flex: 1; overflow-wrap: break-word; hyphens: auto; diff --git a/UIMod/onboard_bundled/assets/css/theme-editor.css b/UIMod/onboard_bundled/assets/css/theme-editor.css new file mode 100644 index 00000000..c77bf9fc --- /dev/null +++ b/UIMod/onboard_bundled/assets/css/theme-editor.css @@ -0,0 +1,242 @@ +@import '/static/css/variables.css'; + + +.theme-editor { + padding: 20px; +} + +.theme-editor-section { + margin-bottom: 30px; + padding: 20px; + background: var(--form-bg); + border-radius: 8px; + border: 1px solid var(--primary-dim); +} + +.theme-editor-section h3 { + margin: 0 0 15px 0; + padding-bottom: 10px; + border-bottom: 2px solid var(--primary-dim); + color: var(--text-bright); + font-family: 'Press Start 2P', cursive; + font-size: 0.85rem; + letter-spacing: 1px; +} + +.theme-color-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 18px; +} + +.theme-color-item { + display: flex; + flex-direction: column; + gap: 6px; +} + +.theme-color-item label { + font-family: 'Share Tech Mono', monospace; + font-size: 0.85rem; + color: var(--text-bright); + display: flex; + align-items: center; + gap: 8px; +} + +.theme-color-item .color-input-wrapper { + display: flex; + align-items: center; + gap: 8px; +} + +.theme-color-item input[type="color"] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 42px; + height: 42px; + border: 2px solid var(--primary-dim); + border-radius: 6px; + cursor: pointer; + background: transparent; + padding: 2px; + transition: border-color var(--transition-fast); +} + +.theme-color-item input[type="color"]:hover { + border-color: var(--primary); +} + +.theme-color-item input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; +} + +.theme-color-item input[type="color"]::-webkit-color-swatch { + border: none; + border-radius: 3px; +} + +.theme-color-item input[type="color"]::-moz-color-swatch { + border: none; + border-radius: 3px; +} + +.theme-color-item input[type="text"].color-hex-input { + width: 100px; + padding: 8px 10px; + font-family: 'Share Tech Mono', monospace; + font-size: 0.85rem; + background: var(--input-bg); + color: var(--primary); + border: 2px solid var(--primary-dim); + border-radius: 4px; + text-transform: uppercase; + margin: 0; +} + +.theme-color-item input[type="text"].color-hex-input:focus { + border-color: var(--primary); + box-shadow: 0 0 8px var(--primary-glow); + outline: none; +} + +.theme-color-item .color-description { + font-size: 0.75rem; + color: var(--text-dim); + font-style: italic; +} + +/* Preset Themes */ +.theme-presets { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 20px; +} + +.theme-preset-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + background: var(--tab-bg); + border: 2px solid var(--primary-dim); + border-radius: 8px; + color: var(--text-bright); + cursor: pointer; + font-family: 'Share Tech Mono', monospace; + font-size: 0.85rem; + transition: all var(--transition-normal); +} + +.theme-preset-btn:hover { + border-color: var(--primary); + transform: translateY(-2px); + box-shadow: 0 4px 12px var(--primary-glow); +} + +.theme-preset-btn.active { + background: var(--primary-bg-medium); + border-color: var(--primary); + box-shadow: 0 0 15px var(--primary-glow); +} + +.theme-preset-colors { + display: flex; + gap: 3px; +} + +.theme-preset-swatch { + width: 14px; + height: 14px; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +/* Theme Actions */ +.theme-actions { + display: flex; + gap: 12px; + margin-top: 20px; + flex-wrap: wrap; +} + +.theme-actions button { + padding: 10px 20px; + font-size: 0.85rem; +} + +/* Live Preview */ +.theme-preview-strip { + display: flex; + gap: 8px; + padding: 12px; + background: var(--bg-dark); + border-radius: 8px; + border: 1px solid var(--primary-dim); + margin-top: 15px; + flex-wrap: wrap; + align-items: center; +} + +.preview-swatch { + width: 32px; + height: 32px; + border-radius: 6px; + border: 2px solid rgba(255, 255, 255, 0.15); + position: relative; + transition: transform var(--transition-fast); +} + +.preview-swatch:hover { + transform: scale(1.15); +} + +.preview-swatch-label { + font-size: 0.7rem; + color: var(--text-dim); + text-align: center; + margin-top: 2px; + font-family: 'Share Tech Mono', monospace; +} + +.preview-swatch-group { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +/* Import/Export */ +.theme-import-export { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; +} + +.theme-import-export textarea { + width: 100%; + min-height: 60px; + padding: 10px; + font-family: 'Share Tech Mono', monospace; + font-size: 0.8rem; + background: var(--input-bg); + color: var(--primary); + border: 2px solid var(--primary-dim); + border-radius: 6px; + resize: vertical; +} + +.theme-import-export textarea:focus { + border-color: var(--primary); + box-shadow: 0 0 8px var(--primary-glow); + outline: none; +} + +/* Notification info type (not in detectionmanager.css) */ +.notification-info { + background-color: var(--accent); + color: var(--text-header); +} diff --git a/UIMod/onboard_bundled/assets/css/variables.css b/UIMod/onboard_bundled/assets/css/variables.css index 85d81679..faaa007a 100644 --- a/UIMod/onboard_bundled/assets/css/variables.css +++ b/UIMod/onboard_bundled/assets/css/variables.css @@ -1,59 +1,91 @@ :root { - /* Colors */ - --primary: - #00FFAB; - --primary-dim: rgba(0, 255, 171, 0.7); - --primary-glow: rgba(0, 255, 171, 0.3); - --bg-dark: - #0a0a14; - --bg-panel: #1b1b2f8f; - --accent: - #0084ff; - --text-bright: - #e0ffe9; - --text-header: - #ffffff; - --danger: - #ff3860; - --success: - #48c774; - --warning: - #ffdd57; - - /* Transitions */ + /* ═══════════════════════════════════════════ + SSUI Theme Variables + These are the user-customizable theme values. + Override via localStorage theme system. + ═══════════════════════════════════════════ + + ── Core Colors ── */ + --primary: #00FFAB; /* Main accent / brand color */ + --bg-dark: #0a0a14; /* Page background */ + --bg-panel: #1b1b2f8f; /* Panel/card backgrounds */ + --accent: #0084ff; /* Secondary accent (links, badges) */ + --danger: #ff3860; /* Errors, destructive actions */ + --success: #48c774; /* Success states */ + --warning: #ffdd57; /* Warnings */ + + /* ── Text Colors ── */ + --text-header: #ffffff; /* Headings, bright text */ + --text-bright: #e0ffe9; /* Primary readable text */ + --text-dim: #aaaaaa; /* Subdued text, hints, descriptions */ + --text-muted: #888888; /* Timestamps, least-important text */ + + /* ── Surface Colors ── */ + --surface-dark: #232338; /* Button backgrounds, elevated surfaces */ + --surface-hover: #333350; /* Hover state for surfaces */ + --surface-overlay: #1b1b2f; /* Dropdowns, overlays */ + + /* ── Discord (brand, not themeable) ── */ + --discord-accent: #7289da; + + /* ── Transitions ── */ --transition-fast: 0.2s ease; --transition-normal: 0.3s ease; --transition-slow: 0.5s ease; - /* v5.9+ Variables */ - --button-bg: - #232338; - --button-bg-hover: - #333350; - --button-glow: rgba(0, 255, 171, 0.4); - --button-glow-strong: rgba(0, 255, 171, 0.7); - --button-glow-soft: rgba(0, 255, 171, 0.1); - --button-glow-stronger: rgba(0, 255, 171, 0.5); + + /* ═══════════════════════════════════════════ + Derived Variables (computed from core colors) + Users don't set these directly — they auto-derive. + ═══════════════════════════════════════════ */ + + /* Primary alpha variants */ + --primary-dim: color-mix(in srgb, var(--primary) 70%, transparent); + --primary-glow: color-mix(in srgb, var(--primary) 30%, transparent); + --primary-glow-strong: color-mix(in srgb, var(--primary) 50%, transparent); + --primary-glow-soft: color-mix(in srgb, var(--primary) 10%, transparent); + --primary-glow-medium: color-mix(in srgb, var(--primary) 40%, transparent); + --primary-glow-intense: color-mix(in srgb, var(--primary) 70%, transparent); + --primary-bg-subtle: color-mix(in srgb, var(--primary) 5%, transparent); + --primary-bg-light: color-mix(in srgb, var(--primary) 15%, transparent); + --primary-bg-medium: color-mix(in srgb, var(--primary) 20%, transparent); + + /* Surface with transparency */ --input-bg: rgba(0, 0, 0, 0.6); - --submit-bg: rgba(0, 255, 171, 0.2); - --submit-hover-text: #000; --tab-bg: rgba(0, 0, 0, 0.3); - --tab-hover-bg: rgba(0, 255, 171, 0.1); - --tab-active-bg: rgba(0, 255, 171, 0.2); - --tab-active-glow: rgba(0, 255, 171, 0.5); - /* v5.9+ Variables config page*/ - --wizard-bg: rgba(0, 255, 171, 0.15); - --wizard-glow: rgba(0, 255, 171, 0.3); - --wizard-glow-strong: rgba(0, 255, 171, 0.6); + --form-bg: rgba(0, 0, 0, 0.2); --nav-bg: rgba(0, 0, 0, 0.4); --nav-bg-hover: rgba(0, 0, 0, 0.6); - --nav-border: rgba(0, 255, 171, 0.5); - --form-bg: rgba(0, 0, 0, 0.2); - --discord-accent: #7289da; - /* Discord brand colour */ - --discord-bg: rgba(114, 137, 218, 0.1); - /* rgba version of Discord blue */ - --discord-border: rgba(114, 137, 218, 0.5); - --discord-glow: rgba(114, 137, 218, 0.3); - --select-bg-hover: rgba(0, 0, 0, 0.8); - --option-bg: #1b1b2f; + --overlay-bg: rgba(0, 0, 0, 0.3); + --overlay-bg-heavy: rgba(0, 0, 0, 0.8); + + /* Discord derived */ + --discord-bg: color-mix(in srgb, var(--discord-accent) 10%, transparent); + --discord-border: color-mix(in srgb, var(--discord-accent) 50%, transparent); + --discord-glow: color-mix(in srgb, var(--discord-accent) 30%, transparent); + + /* ── Legacy aliases (mapped to new vars for compatibility) ── */ + --button-bg: var(--surface-dark); + --button-bg-hover: var(--surface-hover); + --button-glow: var(--primary-glow-medium); + --button-glow-strong: var(--primary-glow-intense); + --button-glow-soft: var(--primary-glow-soft); + --button-glow-stronger: var(--primary-glow-strong); + --submit-bg: var(--primary-bg-medium); + --submit-hover-text: #000; + --tab-hover-bg: var(--primary-glow-soft); + --tab-active-bg: var(--primary-bg-medium); + --tab-active-glow: var(--primary-glow-strong); + --wizard-bg: var(--primary-bg-light); + --wizard-glow: var(--primary-glow); + --wizard-glow-strong: color-mix(in srgb, var(--primary) 60%, transparent); + --nav-border: var(--primary-glow-strong); + --select-bg-hover: var(--overlay-bg-heavy); + --option-bg: var(--surface-overlay); + + /* Console colors (used by JS) */ + --console-info: #0af; + --console-warning: #ff0; + --console-error: #ff3333; + --console-success: #0f0; + --console-boot: #0f0; } \ No newline at end of file diff --git a/UIMod/onboard_bundled/assets/js/console-manager.js b/UIMod/onboard_bundled/assets/js/console-manager.js index 4c65fdd2..a7df2649 100644 --- a/UIMod/onboard_bundled/assets/js/console-manager.js +++ b/UIMod/onboard_bundled/assets/js/console-manager.js @@ -104,6 +104,7 @@ function handleConsole() { "Welcome home, Sir!" ]; + const cssVar = (name) => getComputedStyle(document.documentElement).getPropertyValue(name).trim(); const addMessage = (text, color, style = 'normal') => { const div = document.createElement('div'); div.textContent = text; @@ -162,13 +163,13 @@ function handleConsole() { typeTextWithCallback(consoleElement, bootTitle, 30, () => { // Show two funny messages while connecting const messageIndex1 = Math.floor(Math.random() * funMessages.length); - addMessage(funMessages[messageIndex1], '#0af', 'italic'); + addMessage(funMessages[messageIndex1], cssVar('--console-info'), 'italic'); let messageIndex2; do { messageIndex2 = Math.floor(Math.random() * funMessages.length); } while (messageIndex2 === messageIndex1); - addMessage(funMessages[messageIndex2], '#0af', 'italic'); + addMessage(funMessages[messageIndex2], cssVar('--console-info'), 'italic'); // Set up the persistent console stream outputEventSource = new EventSource('/console'); @@ -191,7 +192,7 @@ function handleConsole() { console.error("Console stream disconnected"); outputEventSource.close(); outputEventSource = null; - addMessage("Warning: Console stream unavailable. Retrying...", '#ff0'); + addMessage("Warning: Console stream unavailable. Retrying...", cssVar('--console-warning')); if (window.location.pathname === '/') { setTimeout(() => { if (!outputEventSource) { @@ -206,9 +207,9 @@ function handleConsole() { function finishInitialization() { if (bugChance < 0.05) { - addMessage(bugMessage, 'red'); + addMessage(bugMessage, cssVar('--console-error')); setTimeout(() => { - addMessage("Repair complete. Continuing initialization...", 'green'); + addMessage("Repair complete. Continuing initialization...", cssVar('--console-success')); completeBoot(); }, 1000); } else { @@ -219,7 +220,7 @@ function handleConsole() { function completeBoot() { setTimeout(() => { createCommandInput(); // Add input after boot - addMessage(bootCompleteMessage, '#0f0'); + addMessage(bootCompleteMessage, cssVar('--console-success')); //addMessage("StationeersServerUI is becoming SteamServerUI!", '#ff4500'); //addMessage("Please mind the New Terrain System warning below", '#ff4500'); consoleElement.scrollTop = consoleElement.scrollHeight; diff --git a/UIMod/onboard_bundled/assets/js/slp.js b/UIMod/onboard_bundled/assets/js/slp.js index 8f9ea091..f4588d19 100644 --- a/UIMod/onboard_bundled/assets/js/slp.js +++ b/UIMod/onboard_bundled/assets/js/slp.js @@ -185,7 +185,7 @@ function updateFileDisplay() { uploadZone.innerHTML = '' + '
File Selected
' + '
' + selectedModFile.name + '
' + - '
' + (selectedModFile.size / 1024 / 1024).toFixed(2) + ' MB
'; + '
' + (selectedModFile.size / 1024 / 1024).toFixed(2) + ' MB
'; uploadBtn.disabled = false; uploadBtn.style.opacity = '1'; } else { diff --git a/UIMod/onboard_bundled/assets/js/theme-editor.js b/UIMod/onboard_bundled/assets/js/theme-editor.js new file mode 100644 index 00000000..fd0d0dcf --- /dev/null +++ b/UIMod/onboard_bundled/assets/js/theme-editor.js @@ -0,0 +1,278 @@ +/** + * SSUI Theme Editor + * Renders the theme editing UI inside the theme config tab. + * Depends on SSUITheme (theme-engine.js) being loaded first. + */ + +(function () { + 'use strict'; + + function renderThemeEditor() { + const container = document.getElementById('theme-editor-root'); + if (!container) return; + + const currentTheme = SSUITheme.getCurrentTheme(); + + // Group variables by their group + const groups = {}; + SSUITheme.THEME_VARS.forEach(v => { + if (!groups[v.group]) groups[v.group] = []; + groups[v.group].push(v); + }); + + let html = ''; + + // ── Preset Buttons ── + html += '
'; + html += '

Theme Presets

'; + html += '
'; + Object.entries(SSUITheme.PRESETS).forEach(([name, preset]) => { + const swatches = [preset['--primary'], preset['--bg-dark'], preset['--accent'], preset['--danger'], preset['--success']]; + html += ``; + }); + html += '
'; + html += '
'; + + // ── Color Editors per Group ── + Object.entries(groups).forEach(([groupName, vars]) => { + html += `
`; + html += `

${groupName}

`; + html += `
`; + vars.forEach(v => { + const val = currentTheme[v.variable] || '#000000'; + // Normalize to hex for the color picker + const hexVal = toHex(val); + html += `
`; + html += ` `; + html += `
`; + html += ` `; + html += ` `; + html += `
`; + html += ` ${v.description}`; + html += `
`; + }); + html += `
`; + html += `
`; + }); + + // ── Preview Strip ── + html += `
`; + html += `

Live Preview

`; + html += `
`; + SSUITheme.THEME_VARS.filter(v => v.group !== 'Console Colors').forEach(v => { + html += `
`; + html += `
`; + html += ` ${v.label}`; + html += `
`; + }); + html += `
`; + html += `
`; + + // ── Actions ── + html += `
`; + html += `

Actions

`; + html += `
`; + html += ` `; + html += ` `; + html += ` `; + html += ` `; + html += `
`; + + // Import area (hidden by default) + html += ``; + + // Export area (hidden by default) + html += ``; + + html += `
`; + + container.innerHTML = html; + } + + // ── Color conversion helpers ── + + function toHex(colorStr) { + if (!colorStr) return '#000000'; + colorStr = colorStr.trim(); + + // Already hex + if (/^#[0-9a-f]{6}$/i.test(colorStr)) return colorStr; + if (/^#[0-9a-f]{3}$/i.test(colorStr)) { + return '#' + colorStr[1] + colorStr[1] + colorStr[2] + colorStr[2] + colorStr[3] + colorStr[3]; + } + // 8-char hex with alpha — just take RGB + if (/^#[0-9a-f]{8}$/i.test(colorStr)) return colorStr.slice(0, 7); + + // rgb/rgba + const rgbMatch = colorStr.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/); + if (rgbMatch) { + const r = parseInt(rgbMatch[1]).toString(16).padStart(2, '0'); + const g = parseInt(rgbMatch[2]).toString(16).padStart(2, '0'); + const b = parseInt(rgbMatch[3]).toString(16).padStart(2, '0'); + return `#${r}${g}${b}`; + } + + // Named colors — use a canvas to resolve + try { + const ctx = document.createElement('canvas').getContext('2d'); + ctx.fillStyle = colorStr; + return ctx.fillStyle; // returns hex + } catch { + return '#000000'; + } + } + + // ── Event Handlers (global scope for inline onclick) ── + + window.onThemeColorChange = function (input) { + const varName = input.dataset.var; + const value = input.value; + document.documentElement.style.setProperty(varName, value); + // Sync hex text input + const hexInput = input.parentElement.querySelector('.color-hex-input'); + if (hexInput) hexInput.value = value; + }; + + window.onThemeHexChange = function (input) { + const varName = input.dataset.var; + let value = input.value.trim(); + // Validate hex + if (/^#[0-9a-f]{3,8}$/i.test(value)) { + document.documentElement.style.setProperty(varName, value); + // Sync color picker + const colorInput = input.parentElement.querySelector('input[type="color"]'); + if (colorInput) colorInput.value = toHex(value); + } + }; + + window.applyPresetTheme = function (presetName) { + const preset = SSUITheme.PRESETS[presetName]; + if (!preset) return; + SSUITheme.applyTheme(preset); + SSUITheme.saveTheme(preset); + // Re-render to update inputs + renderThemeEditor(); + showThemeNotification('Theme "' + presetName + '" applied and saved!', 'success'); + }; + + window.saveCurrentTheme = function () { + const theme = SSUITheme.getCurrentTheme(); + SSUITheme.saveTheme(theme); + showThemeNotification('Theme saved to browser storage!', 'success'); + }; + + window.resetToDefaults = function () { + SSUITheme.clearTheme(); + renderThemeEditor(); + showThemeNotification('Theme reset to defaults.', 'info'); + }; + + window.exportCurrentTheme = function () { + const area = document.getElementById('theme-export-area'); + const text = document.getElementById('theme-export-text'); + if (area && text) { + text.value = SSUITheme.exportTheme(); + area.style.display = 'block'; + } + // hide import if open + const imp = document.getElementById('theme-import-area'); + if (imp) imp.style.display = 'none'; + }; + + window.hideExportTheme = function () { + const area = document.getElementById('theme-export-area'); + if (area) area.style.display = 'none'; + }; + + window.copyExportedTheme = function () { + const text = document.getElementById('theme-export-text'); + if (text) { + navigator.clipboard.writeText(text.value).then(() => { + showThemeNotification('Theme JSON copied to clipboard!', 'success'); + }).catch(() => { + // Fallback + showThemeNotification('Theme copy failed!', 'error'); + }); + } + }; + + window.showImportTheme = function () { + const area = document.getElementById('theme-import-area'); + if (area) area.style.display = 'block'; + // hide export if open + const exp = document.getElementById('theme-export-area'); + if (exp) exp.style.display = 'none'; + }; + + window.hideImportTheme = function () { + const area = document.getElementById('theme-import-area'); + if (area) area.style.display = 'none'; + }; + + window.doImportTheme = function () { + const text = document.getElementById('theme-import-text'); + if (!text || !text.value.trim()) { + showThemeNotification('Please paste a theme JSON first.', 'error'); + return; + } + if (SSUITheme.importTheme(text.value.trim())) { + renderThemeEditor(); + showThemeNotification('Theme imported and applied!', 'success'); + } else { + showThemeNotification('Invalid theme JSON. Please check the format.', 'error'); + } + }; + + function showThemeNotification(message, type) { + // Reuse existing notification element or create a temporary one + let notif = document.getElementById('theme-notification'); + if (!notif) { + notif = document.createElement('div'); + notif.id = 'theme-notification'; + notif.className = 'notification'; + notif.style.position = 'fixed'; + notif.style.top = '20px'; + notif.style.right = '20px'; + notif.style.zIndex = '9999'; + notif.style.maxWidth = '350px'; + document.body.appendChild(notif); + } + notif.className = 'notification notification-' + type; + notif.textContent = message; + notif.style.display = 'block'; + clearTimeout(notif._timer); + notif._timer = setTimeout(() => { + notif.style.display = 'none'; + }, 3000); + } + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', renderThemeEditor); + } else { + renderThemeEditor(); + } +})(); diff --git a/UIMod/onboard_bundled/assets/js/theme-engine.js b/UIMod/onboard_bundled/assets/js/theme-engine.js new file mode 100644 index 00000000..1f1c71df --- /dev/null +++ b/UIMod/onboard_bundled/assets/js/theme-engine.js @@ -0,0 +1,511 @@ +/** + * SSUI Theme Engine + * Manages user-customizable themes via CSS variables and localStorage. + */ + +const SSUITheme = (() => { + const STORAGE_KEY = 'ssui-theme'; + + // ── Themeable variables ── + // Each entry: { variable, label, description, group } + const THEME_VARS = [ + // Core Colors + { variable: '--primary', label: 'Primary', description: 'Main accent / brand color', group: 'Core Colors' }, + { variable: '--bg-dark', label: 'Background', description: 'Page background color', group: 'Core Colors' }, + { variable: '--bg-panel', label: 'Panel BG', description: 'Panel / card background', group: 'Core Colors' }, + { variable: '--accent', label: 'Accent', description: 'Secondary accent (links, badges)', group: 'Core Colors' }, + { variable: '--danger', label: 'Danger', description: 'Errors, destructive actions', group: 'Core Colors' }, + { variable: '--success', label: 'Success', description: 'Success states', group: 'Core Colors' }, + { variable: '--warning', label: 'Warning', description: 'Warning states', group: 'Core Colors' }, + // Text + { variable: '--text-header', label: 'Header Text', description: 'Headings, bright text', group: 'Text Colors' }, + { variable: '--text-bright', label: 'Bright Text', description: 'Primary readable text', group: 'Text Colors' }, + { variable: '--text-dim', label: 'Dim Text', description: 'Subdued text, hints', group: 'Text Colors' }, + { variable: '--text-muted', label: 'Muted Text', description: 'Timestamps, least-important', group: 'Text Colors' }, + // Surfaces + { variable: '--surface-dark', label: 'Surface', description: 'Button / elevated backgrounds', group: 'Surfaces' }, + { variable: '--surface-hover', label: 'Surface Hover', description: 'Hover state for surfaces', group: 'Surfaces' }, + { variable: '--surface-overlay', label: 'Overlay', description: 'Dropdowns, overlays', group: 'Surfaces' }, + // Console + { variable: '--console-info', label: 'Info', description: 'Informational console messages', group: 'Console Colors' }, + { variable: '--console-warning', label: 'Warning', description: 'Warning console messages', group: 'Console Colors' }, + { variable: '--console-error', label: 'Error', description: 'Error console messages', group: 'Console Colors' }, + { variable: '--console-success', label: 'Success', description: 'Success / boot complete messages',group: 'Console Colors' }, + ]; + + // ── Preset Themes ── + const PRESETS = { + 'StationeersServerUI (default)': { + '--primary': '#00FFAB', + '--bg-dark': '#0a0a14', + '--bg-panel': '#1b1b2f8f', + '--accent': '#0084ff', + '--danger': '#ff3860', + '--success': '#48c774', + '--warning': '#ffdd57', + '--text-header': '#ffffff', + '--text-bright': '#e0ffe9', + '--text-dim': '#aaaaaa', + '--text-muted': '#888888', + '--surface-dark': '#232338', + '--surface-hover': '#333350', + '--surface-overlay': '#1b1b2f', + '--console-info': '#0af', + '--console-warning': '#ff0', + '--console-error': '#ff3333', + '--console-success': '#0f0', + }, + 'Neon Blue': { + '--primary': '#00D4FF', + '--bg-dark': '#080818', + '--bg-panel': '#12122a8f', + '--accent': '#7B68EE', + '--danger': '#FF4466', + '--success': '#00E676', + '--warning': '#FFD740', + '--text-header': '#FFFFFF', + '--text-bright': '#D0F0FF', + '--text-dim': '#8899AA', + '--text-muted': '#667788', + '--surface-dark': '#1A1A3E', + '--surface-hover': '#2A2A55', + '--surface-overlay': '#15152D', + '--console-info': '#00D4FF', + '--console-warning': '#FFD740', + '--console-error': '#FF4466', + '--console-success': '#00E676', + }, + 'Hot Pink': { + '--primary': '#FF1493', + '--bg-dark': '#0A0008', + '--bg-panel': '#1E0A1A8f', + '--accent': '#9B59B6', + '--danger': '#FF3030', + '--success': '#00E5A0', + '--warning': '#FFD700', + '--text-header': '#FFFFFF', + '--text-bright': '#FFD0E8', + '--text-dim': '#AA7799', + '--text-muted': '#886688', + '--surface-dark': '#2A1025', + '--surface-hover': '#3A1A35', + '--surface-overlay': '#1E0A18', + '--console-info': '#FF69B4', + '--console-warning': '#FFD700', + '--console-error': '#FF3030', + '--console-success': '#00E5A0', + }, + 'Midnight Purple': { + '--primary': '#B388FF', + '--bg-dark': '#08061A', + '--bg-panel': '#1A1040ef', + '--accent': '#82B1FF', + '--danger': '#FF5252', + '--success': '#69F0AE', + '--warning': '#FFD740', + '--text-header': '#FFFFFF', + '--text-bright': '#E0D0FF', + '--text-dim': '#9988BB', + '--text-muted': '#776699', + '--surface-dark': '#1E1440', + '--surface-hover': '#2E2060', + '--surface-overlay': '#160E30', + '--console-info': '#82B1FF', + '--console-warning': '#FFD740', + '--console-error': '#FF5252', + '--console-success': '#69F0AE', + }, + 'Colourblind friendly': { + '--primary': '#ffb300', + '--bg-dark': '#121212', + '--bg-panel': '#1e1e1e', + '--accent': '#ffb300', + '--danger': '#ff3b3b', + '--success': '#66d020', + '--warning': '#ffdd00', + '--text-header': '#ffffff', + '--text-bright': '#ffffff', + '--text-dim': '#bfbfbf', + '--text-muted': '#999999', + '--surface-dark': '#2a2a2a', + '--surface-hover': '#383838', + '--surface-overlay': '#1e1e1e', + '--console-info': '#ffb300', + '--console-warning': '#ffdd00', + '--console-error': '#ff3b3b', + '--console-success': '#66d020', + }, + 'Tynningö': { + '--primary': '#6a9955', + '--bg-dark': '#1e1e1e', + '--bg-panel': '#252526', + '--accent': '#6a9955', + '--danger': '#ff3860', + '--success': '#6a9955', + '--warning': '#ce9178', + '--text-header': '#d4d4d4', + '--text-bright': '#d4d4d4', + '--text-dim': '#a9a9a9', + '--text-muted': '#808080', + '--surface-dark': '#2d2d2d', + '--surface-hover': '#3c3c3c', + '--surface-overlay': '#252526', + '--console-info': '#6a9955', + '--console-warning': '#ce9178', + '--console-error': '#ff3860', + '--console-success': '#6a9955', + }, + 'Dammstakärret': { + '--primary': '#7a9a7a', + '--bg-dark': '#121a12', + '--bg-panel': '#1b2a1b', + '--accent': '#7a9a7a', + '--danger': '#ff6b6b', + '--success': '#7a9a7a', + '--warning': '#c9a67a', + '--text-header': '#d9e6d9', + '--text-bright': '#d9e6d9', + '--text-dim': '#a3b3a3', + '--text-muted': '#7a8a7a', + '--surface-dark': '#243224', + '--surface-hover': '#2e3a2e', + '--surface-overlay': '#1b2a1b', + '--console-info': '#7a9a7a', + '--console-warning': '#c9a67a', + '--console-error': '#ff6b6b', + '--console-success': '#7a9a7a', + }, + 'Ramsö Sjöwind': { + '--primary': '#68c1e8', + '--bg-dark': '#1a2a38', + '--bg-panel': '#253545', + '--accent': '#68c1e8', + '--danger': '#ff6b6b', + '--success': '#68c1e8', + '--warning': '#f0ad4e', + '--text-header': '#e0eaf0', + '--text-bright': '#e0eaf0', + '--text-dim': '#b0c0d0', + '--text-muted': '#8aa0b0', + '--surface-dark': '#2f4055', + '--surface-hover': '#3a4c66', + '--surface-overlay': '#253545', + '--console-info': '#68c1e8', + '--console-warning': '#f0ad4e', + '--console-error': '#ff6b6b', + '--console-success': '#68c1e8', + }, + 'Rindö Solnedgang': { + '--primary': '#ff9e7a', + '--bg-dark': '#272133', + '--bg-panel': '#332940', + '--accent': '#ff9e7a', + '--danger': '#ff6b6b', + '--success': '#66d020', + '--warning': '#ffcc66', + '--text-header': '#f5e6ff', + '--text-bright': '#f5e6ff', + '--text-dim': '#d1b6e1', + '--text-muted': '#b89ac7', + '--surface-dark': '#3e304d', + '--surface-hover': '#4b3a5d', + '--surface-overlay': '#332940', + '--console-info': '#ff9e7a', + '--console-warning': '#ffcc66', + '--console-error': '#ff6b6b', + '--console-success': '#66d020', + }, + 'Mint Choklad': { + '--primary': '#7fe0c3', + '--bg-dark': '#1e2721', + '--bg-panel': '#26322a', + '--accent': '#7fe0c3', + '--danger': '#ff6b6b', + '--success': '#7fe0c3', + '--warning': '#d9b382', + '--text-header': '#e0f0e8', + '--text-bright': '#e0f0e8', + '--text-dim': '#b0c5b8', + '--text-muted': '#8fa9a3', + '--surface-dark': '#2e3d33', + '--surface-hover': '#38493e', + '--surface-overlay': '#26322a', + '--console-info': '#7fe0c3', + '--console-warning': '#d9b382', + '--console-error': '#ff6b6b', + '--console-success': '#7fe0c3', + }, + 'Lavendel Fält': { + '--primary': '#b28dff', + '--bg-dark': '#2b2440', + '--bg-panel': '#352e4e', + '--accent': '#b28dff', + '--danger': '#ff6b6b', + '--success': '#66d020', + '--warning': '#ffad9c', + '--text-header': '#ece8ff', + '--text-bright': '#ece8ff', + '--text-dim': '#c7c0e3', + '--text-muted': '#a99cc9', + '--surface-dark': '#3f385c', + '--surface-hover': '#4a426a', + '--surface-overlay': '#352e4e', + '--console-info': '#b28dff', + '--console-warning': '#ffad9c', + '--console-error': '#ff6b6b', + '--console-success': '#66d020', + }, + 'Midsommar': { + '--primary': '#ffd700', + '--bg-dark': '#1a1a2e', + '--bg-panel': '#232342', + '--accent': '#ffd700', + '--danger': '#ff6b6b', + '--success': '#66d020', + '--warning': '#ff6b9d', + '--text-header': '#fff4d6', + '--text-bright': '#fff4d6', + '--text-dim': '#e6d5a8', + '--text-muted': '#ccc088', + '--surface-dark': '#2d2d56', + '--surface-hover': '#3a3a6a', + '--surface-overlay': '#232342', + '--console-info': '#ffd700', + '--console-warning': '#ff6b9d', + '--console-error': '#ff6b6b', + '--console-success': '#66d020', + }, + 'Kireness': { + '--primary': '#80DEEA', + '--bg-dark': '#0A1215', + '--bg-panel': '#1225308f', + '--accent': '#4FC3F7', + '--danger': '#EF5350', + '--success': '#66BB6A', + '--warning': '#FFA726', + '--text-header': '#ECEFF1', + '--text-bright': '#B0BEC5', + '--text-dim': '#78909C', + '--text-muted': '#546E7A', + '--surface-dark': '#1A2D35', + '--surface-hover': '#253D48', + '--surface-overlay': '#152228', + '--console-info': '#4FC3F7', + '--console-warning': '#FFA726', + '--console-error': '#EF5350', + '--console-success': '#66BB6A', + }, + 'Kiruna': { + '--primary': '#3ddbd9', + '--bg-dark': '#0d1b2a', + '--bg-panel': '#1b263b', + '--accent': '#3ddbd9', + '--danger': '#ee6c4d', + '--success': '#3ddbd9', + '--warning': '#ee6c4d', + '--text-header': '#e0fbfc', + '--text-bright': '#e0fbfc', + '--text-dim': '#98c1d9', + '--text-muted': '#6a9ab8', + '--surface-dark': '#253347', + '--surface-hover': '#2f3e53', + '--surface-overlay': '#1b263b', + '--console-info': '#3ddbd9', + '--console-warning': '#ee6c4d', + '--console-error': '#ee6c4d', + '--console-success': '#3ddbd9', + }, + 'Lingon': { + '--primary': '#ff3864', + '--bg-dark': '#2d1b1e', + '--bg-panel': '#3d252a', + '--accent': '#ff3864', + '--danger': '#ff3864', + '--success': '#7fe0c3', + '--warning': '#ffa07a', + '--text-header': '#ffe8ed', + '--text-bright': '#ffe8ed', + '--text-dim': '#deb8c4', + '--text-muted': '#c49aaa', + '--surface-dark': '#4d2f36', + '--surface-hover': '#5d3942', + '--surface-overlay': '#3d252a', + '--console-info': '#ff3864', + '--console-warning': '#ffa07a', + '--console-error': '#ff3864', + '--console-success': '#7fe0c3', + }, + 'Saffran': { + '--primary': '#f4a261', + '--bg-dark': '#2a1f15', + '--bg-panel': '#3a2a1e', + '--accent': '#f4a261', + '--danger': '#e76f51', + '--success': '#66d020', + '--warning': '#e76f51', + '--text-header': '#fff5e6', + '--text-bright': '#fff5e6', + '--text-dim': '#e6d4b8', + '--text-muted': '#ccb8a0', + '--surface-dark': '#4a3527', + '--surface-hover': '#5a4030', + '--surface-overlay': '#3a2a1e', + '--console-info': '#f4a261', + '--console-warning': '#e76f51', + '--console-error': '#e76f51', + '--console-success': '#66d020', + }, + 'Göteborg': { + '--primary': '#ff10f0', + '--bg-dark': '#0f0320', + '--bg-panel': '#1a0835', + '--accent': '#ff10f0', + '--danger': '#ffff00', + '--success': '#39ff14', + '--warning': '#ffff00', + '--text-header': '#f0e6ff', + '--text-bright': '#f0e6ff', + '--text-dim': '#c8b6e2', + '--text-muted': '#a899c7', + '--surface-dark': '#250d4a', + '--surface-hover': '#30125f', + '--surface-overlay': '#1a0835', + '--console-info': '#00f0ff', + '--console-warning': '#ffff00', + '--console-error': '#ff10f0', + '--console-success': '#39ff14', + }, + 'Blåbär': { + '--primary': '#6b88ff', + '--bg-dark': '#1a1f3a', + '--bg-panel': '#242c4a', + '--accent': '#6b88ff', + '--danger': '#ff6b6b', + '--success': '#66d020', + '--warning': '#c77dff', + '--text-header': '#e8eeff', + '--text-bright': '#e8eeff', + '--text-dim': '#b8c8e8', + '--text-muted': '#9ab0d8', + '--surface-dark': '#2e395a', + '--surface-hover': '#38466a', + '--surface-overlay': '#242c4a', + '--console-info': '#6b88ff', + '--console-warning': '#c77dff', + '--console-error': '#ff6b6b', + '--console-success': '#66d020', + }, + 'Rabarber': { + '--primary': '#ff6b9d', + '--bg-dark': '#1f1a1d', + '--bg-panel': '#2d242a', + '--accent': '#ff6b9d', + '--danger': '#ff6b9d', + '--success': '#7fe0c3', + '--warning': '#ffa07a', + '--text-header': '#ffe8f5', + '--text-bright': '#ffe8f5', + '--text-dim': '#e6b8d8', + '--text-muted': '#cc9acc', + '--surface-dark': '#3b2e37', + '--surface-hover': '#493844', + '--surface-overlay': '#2d242a', + '--console-info': '#ff6b9d', + '--console-warning': '#ffa07a', + '--console-error': '#ff6b9d', + '--console-success': '#7fe0c3', + }, + }; + + /*Get the current saved theme from localStorage, or null if none.*/ + function getSavedTheme() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + return raw ? JSON.parse(raw) : null; + } catch { + return null; + } + } + + /*Save a theme object to localStorage.*/ + function saveTheme(themeObj) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(themeObj)); + } + + /* Remove the saved theme (revert to CSS defaults).*/ + function clearTheme() { + localStorage.removeItem(STORAGE_KEY); + // Remove all overrides from :root + const root = document.documentElement; + THEME_VARS.forEach(v => root.style.removeProperty(v.variable)); + } + + /* Apply a theme object to the document (sets CSS custom properties on :root).*/ + function applyTheme(themeObj) { + if (!themeObj) return; + const root = document.documentElement; + Object.entries(themeObj).forEach(([varName, value]) => { + if (value) root.style.setProperty(varName, value); + }); + } + + /* Read the current computed values of all theme variables.*/ + function getCurrentTheme() { + const computed = getComputedStyle(document.documentElement); + const theme = {}; + THEME_VARS.forEach(v => { + // Try inline override first, then computed + const inline = document.documentElement.style.getPropertyValue(v.variable).trim(); + const comp = computed.getPropertyValue(v.variable).trim(); + theme[v.variable] = inline || comp; + }); + return theme; + } + + /* Load and apply saved theme on page load.*/ + function init() { + const saved = getSavedTheme(); + if (saved) applyTheme(saved); + } + + /* Export the current theme as a JSON string (for sharing).*/ + + function exportTheme() { + const saved = getSavedTheme(); + return JSON.stringify(saved || getCurrentTheme(), null, 2); + } + + /* Import a theme from a JSON string.*/ + function importTheme(jsonStr) { + try { + const theme = JSON.parse(jsonStr); + if (typeof theme !== 'object' || theme === null) throw new Error('Invalid theme'); + applyTheme(theme); + saveTheme(theme); + return true; + } catch (e) { + console.error('Theme import failed:', e); + return false; + } + } + + // Auto-init on load + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + return { + THEME_VARS, + PRESETS, + getSavedTheme, + saveTheme, + clearTheme, + applyTheme, + getCurrentTheme, + exportTheme, + importTheme, + init, + }; +})(); diff --git a/UIMod/onboard_bundled/ui/config.html b/UIMod/onboard_bundled/ui/config.html index 95b3a538..329daeb3 100644 --- a/UIMod/onboard_bundled/ui/config.html +++ b/UIMod/onboard_bundled/ui/config.html @@ -13,6 +13,7 @@ + @@ -60,6 +61,10 @@

{{.UIText_ConfigHeadline}}

{{.UIText_DetectionManager}} +
@@ -588,7 +593,7 @@

Beta Feature

{{.UIText_SLP_Title}}

{{.UIText_SLP_Description}}

-

{{.UIText_SLP_ReadyToInstall}}

+

{{.UIText_SLP_ReadyToInstall}}

@@ -622,7 +627,7 @@

{{.UIText_SLP_UploadModPackage}}

{{.UIText_SLP_ManageInstallation}}

-

⚠️ {{.UIText_SLP_UninstallWarning}}

+

⚠️ {{.UIText_SLP_UninstallWarning}}

@@ -729,6 +734,10 @@

Regex Detection:

+
+
+
+
+ diff --git a/src/discordbot/logstream.go b/src/discordbot/logstream.go index 9212d9a5..f70ab3c8 100644 --- a/src/discordbot/logstream.go +++ b/src/discordbot/logstream.go @@ -34,9 +34,11 @@ func flushLogBufferToDiscord() { for len(message) > 0 { // Determine how much of the message we can send - chunkSize := discordMaxMessageLength - if len(message) < discordMaxMessageLength { - chunkSize = len(message) + chunkSize := min(len(message), discordMaxMessageLength) + + // if the message is empty len 0, break to avoid infinite loop + if chunkSize == 0 { + break } // Send the chunk to Discord From 2e843f2a5a3a16204bcec37d4b76b8ed90f46955 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 15 Feb 2026 14:27:31 +0100 Subject: [PATCH 19/20] fix: Improve error logging syntax in heap profile management --- src/cli/devcommands.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/devcommands.go b/src/cli/devcommands.go index fca97acf..08a1a868 100644 --- a/src/cli/devcommands.go +++ b/src/cli/devcommands.go @@ -64,19 +64,19 @@ func dumpHeapProfile() { runtime.GC() if _, err := os.Stat("heap.pprof"); err == nil { if err := os.Remove("heap.pprof"); err != nil { - logger.Main.Errorf("could not remove old heap profile", "err", err) + logger.Main.Errorf("could not remove old heap profile: %v", err) return } } f, err := os.Create("heap.pprof") if err != nil { - logger.Main.Errorf("could not create heap profile file", "err", err) + logger.Main.Errorf("could not create heap profile file: %v", err) return } defer f.Close() if err := pprof.WriteHeapProfile(f); err != nil { - logger.Main.Errorf("could not write heap profile", "err", err) + logger.Main.Errorf("could not write heap profile: %v", err) return } From 7ae0e56ff9322e797ec90844be87269cf497e88d Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 15 Feb 2026 15:41:07 +0100 Subject: [PATCH 20/20] removed pause command from SSCM autocomplete. fixes [StationeersSUI](Console) Remove Pause command from SSCM command input (Console) Fixes #145 --- UIMod/onboard_bundled/assets/js/sscm.js | 1 - 1 file changed, 1 deletion(-) diff --git a/UIMod/onboard_bundled/assets/js/sscm.js b/UIMod/onboard_bundled/assets/js/sscm.js index 724c5aee..d40cee34 100644 --- a/UIMod/onboard_bundled/assets/js/sscm.js +++ b/UIMod/onboard_bundled/assets/js/sscm.js @@ -34,7 +34,6 @@ const availableCommands = [ { name: "network", params: "", desc: "Shows network status" }, { name: "networkdebug", params: "", desc: "Displays network debug window" }, { name: "orbit", params: "[debug,view,celestials,simulate,set,timescale,makeoffset]", desc: "Controls orbital simulation" }, - { name: "pause", params: "[true,false]", desc: "Pauses/unpauses game" }, { name: "plant", params: "[grow ]", desc: "Plant debug functions" }, { name: "prefabs", params: "[Thumbnails]", desc: "Validates source prefabs" }, { name: "printgasinfo", params: "", desc: "Prints gas coefficients" },