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." }, 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/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/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}}, 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) diff --git a/src/discordbot/sendMessage.go b/src/discordbot/sendMessage.go index 1654183f..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 } @@ -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() { 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) } }, }