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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion UIMod/onboard_bundled/localization/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
},
Expand Down
4 changes: 2 additions & 2 deletions UIMod/onboard_bundled/localization/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@
"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",
"UIText_SaveIntervalInfo": "Time in seconds between saves. Recommended to <strong> not </strong> recede 60 seconds.",
"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."
},
Expand Down
2 changes: 1 addition & 1 deletion UIMod/onboard_bundled/localization/sv-SE.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
},
Expand Down
117 changes: 83 additions & 34 deletions src/discordbot/connectedplayers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,77 +8,126 @@ import (

"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
}
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) {
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)
}

// 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)
}
Comment thread
JacksonTheMaster marked this conversation as resolved.
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()
}
49 changes: 0 additions & 49 deletions src/discordbot/handleReactions.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand All @@ -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())
}
}
6 changes: 5 additions & 1 deletion src/discordbot/handleSlashcommands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}},
Expand Down
5 changes: 3 additions & 2 deletions src/discordbot/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
53 changes: 1 addition & 52 deletions src/discordbot/sendMessage.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func SendMessageToSavesChannel(message string) {
}
}

func SendUntrackedMessageToErrorChannel(message string) {
func SendMessageToErrorChannel(message string) {
if !config.GetIsDiscordEnabled() {
return
}
Expand Down Expand Up @@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions src/managers/detectionmgr/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -140,7 +140,7 @@ func DefaultHandlers() map[EventType]Handler {

logger.Detection.Info(message)
ssestream.BroadcastDetectionEvent(message)
discordbot.SendUntrackedMessageToErrorChannel(message)
discordbot.SendMessageToErrorChannel(message)
}
},
}
Expand Down