Skip to content
Open
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
3 changes: 1 addition & 2 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ RUN mkdir -p /go/bin && chown -R vscode:vscode /go

# Install Node.js 22.15.0
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
apt-get install -y nodejs && \
npm install -g npm@latest
apt-get install -y nodejs

# Set up non-root user and workspace
USER vscode
Expand Down
9 changes: 9 additions & 0 deletions .devcontainer/devcontainer-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"features": {
"ghcr.io/devcontainers/features/go:1": {
"version": "1.3.4",
"resolved": "ghcr.io/devcontainers/features/go@sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032",
"integrity": "sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032"
}
}
}
5 changes: 2 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,13 @@
"vscode": {
"extensions": [
"golang.go",
"svelte.svelte-vscode",
"eamodio.gitlens"
"svelte.svelte-vscode"
],
"settings": {
"go.toolsManagement.autoUpdate": true,
"svelte.enable-ts-plugin": true
}
}
},
"postCreateCommand": "go mod tidy && cd frontend && npm install && npm install @sveltejs/vite-plugin-svelte"
"postCreateCommand": "go mod tidy && cd frontend && npm install && npm install @sveltejs/vite-plugin-svelte && npm audit fix"
Comment thread
akirilov marked this conversation as resolved.
}
33 changes: 33 additions & 0 deletions UIMod/onboard_bundled/assets/css/config.css
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,39 @@ select option {
font-size: 0.75rem;
}

.mod-update-button {
display: inline-flex;
align-items: center;
gap: 5px;
margin-top: 10px;
padding: 5px 12px;
background: transparent;
border: 1px solid var(--primary-dim);
color: var(--primary);
border-radius: 6px;
font-family: 'Share Tech Mono', monospace;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
letter-spacing: 0.3px;
}

.mod-update-button:hover:not(:disabled) {
border-color: var(--primary);
background: color-mix(in srgb, var(--primary) 10%, transparent);
box-shadow: 0 0 8px var(--button-glow-soft);
}

.mod-update-button:disabled {
opacity: 0.4;
cursor: not-allowed;
}

.mod-update-button.loading {
opacity: 0.6;
cursor: wait;
}

.slp-button.danger {
background-color: var(--danger);
border-color: var(--danger);
Expand Down
34 changes: 33 additions & 1 deletion UIMod/onboard_bundled/assets/js/slp.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function setButtonLoading(buttonId, isLoading) {
if (isLoading) {
button.disabled = true;
button.dataset.originalText = button.textContent;
button.textContent = '⏳ Please wait...';
button.textContent = button.classList.contains('mod-update-button') ? '⏳' : '⏳ Please wait...';
button.classList.add('loading');
} else {
button.disabled = false;
Expand Down Expand Up @@ -135,6 +135,32 @@ function reinstallSLP() {
});
}

function updateSingleMod(workshopHandle, index) {
const btnId = 'update-mod-btn-' + index;
setButtonLoading(btnId, true);
showPopup('info', 'Updating workshop mod ' + workshopHandle + '...\n\nPlease wait.');

fetch('/api/v2/steamcmd/updatemod', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ workshopHandle: workshopHandle })
})
.then(response => response.json())
.then(data => {
setButtonLoading(btnId, false);
if (data.success) {
showPopup('success', 'Workshop mod updated successfully!\n\nReloading mod list...');
loadInstalledMods();
} else {
showPopup('error', 'Failed to update mod:\n\n' + (data.error || 'Unknown error'));
}
})
.catch(error => {
showPopup('error', 'Failed to update mod:\n\n' + (error.message || 'Network error'));
setButtonLoading(btnId, false);
});
}

function updateWorkshopMods() {
setButtonLoading('updateWorkshopModsBtn', true);
showPopup('info', 'Updating workshop mods...\n\nThis may take some time depending on the number of mods. Please wait.');
Expand Down Expand Up @@ -424,12 +450,18 @@ function createModCard(mod, index) {
`;
}

let updateButtonHtml = '';
if (mod.WorkshopHandle) {
updateButtonHtml = `<button class="mod-update-button" id="update-mod-btn-${index}" onclick="updateSingleMod('${escapeHtml(mod.WorkshopHandle)}', ${index})">🔄 Update</button>`;
}

card.innerHTML = `
${imageHtml}
<div class="mod-title">${escapeHtml(mod.Name || 'Unknown Mod')}</div>
${mod.Author ? `<div class="mod-author">By ${escapeHtml(mod.Author)}</div>` : ''}
${mod.Version ? `<div class="mod-version">v${escapeHtml(mod.Version)}</div>` : ''}
${descriptionHtml}
${updateButtonHtml}
`;

return card;
Comment on lines +453 to 467

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems valid, if I understand correctly we don't control the workshop handle so this is a potential XSS vector. We should sanitize this

Expand Down
6 changes: 5 additions & 1 deletion src/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func init() {
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("downloadworkshopitem", downloadWorkshopItem, "Download a workshop item. When no arguments are provided, 3672138641/BlueprintMod is downloaded. Can be called like downloadworkshopitem 3505169479 or downloadworkshopitem 3505169479 3505115682 3505169479 ", true, "dwi")
RegisterCommand("dumpheapprofile", WrapNoReturn(dumpHeapProfile), "Dump a pprof heap profile for debugging", true, "dhp")
RegisterCommand("testserverstatuspaneldiscord", WrapNoReturn(testServerStatusPanelDiscord), "Send a fake player list to the Discord package to test the server status panel", true, "tsspd")
}
Expand Down Expand Up @@ -89,6 +89,10 @@ func stopServer() {
func exitfromcli() {
// send signal to the main process to exit
logger.Core.Info("I have to go...")
err := gamemgr.InternalStopServer()
if err != nil {
logger.Core.Error("Error stopping server:" + err.Error())
}
os.Exit(0)
}

Expand Down
11 changes: 9 additions & 2 deletions src/cli/devcommands.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,19 @@ import (

// COMMAND HANDLERS WITH COMMANDS USEFUL FOR DEVELOPMENT AND DEBUGGING

func downloadWorkshopItemTest() {
workshopHandles := []string{"3505169479"}
func downloadWorkshopItem(args []string) error {
var workshopHandles []string
if len(args) == 0 {
workshopHandles = []string{"3672138641"} // blueprint mod
} else {
workshopHandles = args
}
logger.Core.Info(fmt.Sprintf("Downloading workshop items: %v", workshopHandles))
_, err := steamcmd.DownloadWorkshopItems(workshopHandles)
if err != nil {
logger.Core.Error("Error downloading workshop items: " + err.Error())
}
return nil
Comment on lines 28 to +32

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, my bad

}

func listworkshophandles() {
Expand Down
4 changes: 2 additions & 2 deletions src/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

var (
// All configuration variables can be found in vars.go
Version = "5.13.2"
Version = "5.13.3"
Branch = "release"
)

Expand Down Expand Up @@ -176,7 +176,7 @@ func applyConfig(cfg *JsonConfig) {
IsNewTerrainAndSaveSystem = isNewTerrainAndSaveSystemVal
cfg.IsNewTerrainAndSaveSystem = &isNewTerrainAndSaveSystemVal

GameBranch = getString(cfg.GameBranch, "GAME_BRANCH", "public")
GameBranch = getString(strings.ToLower(cfg.GameBranch), "GAME_BRANCH", "public")
Difficulty = getString(cfg.Difficulty, "DIFFICULTY", "")
StartCondition = getString(cfg.StartCondition, "START_CONDITION", "")
StartLocation = getString(cfg.StartLocation, "START_LOCATION", "")
Expand Down
107 changes: 100 additions & 7 deletions src/steamcmd/steamcmd-helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"os/exec"
"path/filepath"
"runtime"
"slices"
"strings"
"time"

Expand Down Expand Up @@ -293,9 +294,64 @@ func untarWrapper(r io.ReaderAt, _ int64, dest string) error {
return untar(dest, io.NewSectionReader(r, 0, 1<<63-1)) // Use a large size for the section reader
}

type distroFamily int

const (
distroUnknown distroFamily = iota
distroDebian
distroRHEL
)

// parseOSRelease parses a /etc/os-release file into a key-value map.
func parseOSRelease(content string) map[string]string {
fields := make(map[string]string)
for _, line := range strings.Split(content, "\n") {
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
fields[parts[0]] = strings.Trim(parts[1], "\"'")
}
return fields
}

// detectDistroFamily reads /etc/os-release and returns the distro family.
// ID is checked first (single value); ID_LIKE is the fallback (space-separated list).
func detectDistroFamily() distroFamily {
data, err := os.ReadFile("/etc/os-release")
if err != nil {
return distroUnknown
}

debianIDs := []string{"ubuntu", "debian", "linuxmint", "pop", "elementary", "raspbian"}
rhelIDs := []string{"rhel", "centos", "fedora", "rocky", "almalinux", "ol"}

fields := parseOSRelease(string(data))

// Check ID first — it's a single value identifying the primary distro.
id := strings.ToLower(fields["ID"])
if slices.Contains(debianIDs, id) {
return distroDebian
}
if slices.Contains(rhelIDs, id) {
return distroRHEL
}

// Fall back to ID_LIKE — a space-separated list of closely related distros.
for _, like := range strings.Fields(strings.ToLower(fields["ID_LIKE"])) {
if slices.Contains(debianIDs, like) {
return distroDebian
}
if slices.Contains(rhelIDs, like) {
return distroRHEL
}
}

return distroUnknown
}

// installRequiredLibraries installs the required libraries for SteamCMD if they are not already installed.
func installRequiredLibraries() error {
// Check if the system is Debian-based
if runtime.GOOS != "linux" {
return nil // Only Linux systems need this
}
Expand All @@ -306,8 +362,19 @@ func installRequiredLibraries() error {
return nil
}

// According to https://developer.valvesoftware.com/wiki/SteamCMD#Manually only lib32gcc-s1 is needed
// List of required libraries
switch detectDistroFamily() {
case distroDebian:
return installRequiredLibrariesDebian()
case distroRHEL:
return installRequiredLibrariesRHEL()
default:
return fmt.Errorf("unsupported Linux distribution: only Ubuntu/Debian and RHEL-based distros are supported")
}
Comment on lines +370 to +372

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also a fair point. Will revisit this PR..later today or tomorrow, possibly

}

// installRequiredLibrariesDebian installs SteamCMD dependencies on Ubuntu/Debian using apt-get.
// According to https://developer.valvesoftware.com/wiki/SteamCMD#Manually only lib32gcc-s1 is needed.
func installRequiredLibrariesDebian() error {
requiredLibs := []string{
"lib32gcc-s1",
//"lib32stdc++6",
Expand All @@ -316,23 +383,49 @@ func installRequiredLibraries() error {
// Check and install each library
for _, lib := range requiredLibs {
// Check if the library is already installed
cmd := exec.Command("dpkg", "-s", lib)
if err := cmd.Run(); err == nil {
if err := exec.Command("dpkg", "-s", lib).Run(); err == nil {
logger.Install.Debug("✅ Library already installed: " + lib + "\n")
continue // Library is already installed, skip to the next one
continue
}

// Library is not installed, attempt to install it
logger.Install.Debug("🔄 Installing library: " + lib + "\n")
installCmd := exec.Command("sudo", "apt-get", "install", "-y", lib)
installCmd.Stdout = os.Stdout
installCmd.Stderr = os.Stderr

if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install library %s: %w", lib, err)
}
logger.Install.Debug("✅ Installed library: " + lib + "\n")
}
return nil
}

// installRequiredLibrariesRHEL installs SteamCMD dependencies on RHEL-based distros using dnf.
// libgcc.i686 is the RHEL equivalent of lib32gcc-s1 on Debian-based distros.
func installRequiredLibrariesRHEL() error {
requiredLibs := []string{
"libgcc.i686",
"libstdc++.i686",
}

// Check and install each library
for _, lib := range requiredLibs {
// Check if the library is already installed
if err := exec.Command("rpm", "-q", lib).Run(); err == nil {
logger.Install.Debug("✅ Library already installed: " + lib + "\n")
continue
}

// Library is not installed, attempt to install it
logger.Install.Debug("🔄 Installing library: " + lib + "\n")
installCmd := exec.Command("sudo", "dnf", "install", "-y", lib)
installCmd.Stdout = os.Stdout
installCmd.Stderr = os.Stderr
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install library %s: %w", lib, err)
}
logger.Install.Debug("✅ Installed library: " + lib + "\n")
}
return nil
}
1 change: 1 addition & 0 deletions src/web/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) {
protectedMux.HandleFunc("/api/v2/slp/upload", UploadModPackageHandler)
protectedMux.HandleFunc("/api/v2/slp/mods", GetInstalledModDetailsHandler)
protectedMux.HandleFunc("/api/v2/steamcmd/updatemods", UpdateWorkshopModsHandler)
protectedMux.HandleFunc("/api/v2/steamcmd/updatemod", UpdateSingleWorkshopModHandler)

return mux, protectedMux
}
Loading
Loading