diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..5b73033c --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,25 @@ +# Use Debian-based VS Code dev container image +FROM mcr.microsoft.com/vscode/devcontainers/base:bookworm + +# Install dependencies in a single layer to reduce image size +RUN apt-get update && apt-get install -y \ + curl \ + git \ + make \ + lib32gcc-s1 \ + dbus \ + wget \ + && rm -rf /var/lib/apt/lists/* + +# Install Go 1.24.2 +RUN wget -q https://go.dev/dl/go1.24.2.linux-amd64.tar.gz && \ + tar -C /usr/local -xzf go1.24.2.linux-amd64.tar.gz && \ + rm go1.24.2.linux-amd64.tar.gz +ENV PATH="/usr/local/go/bin:${PATH}" +ENV GOPATH=/go +RUN mkdir -p /go/bin && chown -R vscode:vscode /go + + +# Set up non-root user and workspace +USER vscode +WORKDIR /workspaces \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..9df0bb67 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,31 @@ +{ + "name": "StationeersServerUI (Go)", + "build": { + "dockerfile": "Dockerfile" + }, + "workspaceFolder": "/workspaces/project", + "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/project,type=bind,consistency=cached", + "features": { + "ghcr.io/devcontainers/features/go:1": { + "version": "1.24.2" + } + }, + "forwardPorts": [8443], + "portsAttributes": { + "8443": { "label": "Go Backend Server" } + + }, + "customizations": { + "vscode": { + "extensions": [ + "golang.go", + "supermaven.supermaven", + "eamodio.gitlens" + ], + "settings": { + "go.toolsManagement.autoUpdate": true + } + } + }, + "postCreateCommand": "go mod tidy" +} \ No newline at end of file diff --git a/.github/mock-backend-for-demo.js b/.github/mock-backend-for-demo.js deleted file mode 100644 index 74858369..00000000 --- a/.github/mock-backend-for-demo.js +++ /dev/null @@ -1,369 +0,0 @@ -// /static/script.js - MOCKED VERSION FOR GITHUB PREVIEW -// This is in the .github/workflows/ folder because I really couldn't figure out how where else to put it, realistically. -document.addEventListener('DOMContentLoaded', () => { - typeText(document.querySelector('h1'), 30); - setupTabs(); - mockDetectionEvents(); - mockBackups(); - mockConsole(); - // Create planets with size, orbit radius, speed, and color - const planetContainer = document.getElementById('planet-container'); - createPlanet(planetContainer, 80, 650, 34, 'rgba(200, 100, 50, 0.7)'); - createPlanet(planetContainer, 50, 1000, 46, 'rgba(100, 200, 150, 0.5)'); - createPlanet(planetContainer, 30, 1250, 63, 'rgba(50, 150, 250, 0.6)'); - createPlanet(planetContainer, 70, 400, 28, 'rgba(200, 150, 200, 0.7)'); -}); - -// Mock data for preview -const MOCK = { - detectionEvents: [ - "🎮 [Gameserver] 🕑 Server is starting up...", - "🎮 [Gameserver] ✅ Server process has started!", - "🎮 [Gameserver] ⚙️ Setting StartLocalHost changed from False to True", - "🎮 [Gameserver] 🔔 Server is ready to connect!", - "🎮 [Gameserver] 💾 World Saved: BackupIndex: 9 UTC Time: 2025-03-30T12:40:08Z", - "🎮 [Gameserver] 📡 Player BobTheBuilder connecting from 192.168.1.100", - "🎮 [Gameserver] 📡 Player BobTheBuilder ready", - "🎮 [Gameserver] 📡 Player SpaceCowboy connecting from 192.168.1.101", - "🎮 [Gameserver] 📡 Player SpaceCowboy ready", - "🎮 [Gameserver] 💀 Player BobTheBuilder disconnected", - "🎮 [Gameserver] ❌ Exception in thread 'main': unity.Exception: random.unity.exeption caught and handled", - "🎮 [Gameserver] 🚨 Server is stopping...", - "🎮 [Gameserver] 🚨 Server process has stopped!", - "🎮 [Gameserver] 🕑 Server is starting up...", - "🎮 [Gameserver] ✅ Server process has started!", - ], - backups: [ - "BackupIndex: 8, Created: 29.03.2025 18:59:47", - "BackupIndex: 7, Created: 28.03.2025 12:30:45", - "BackupIndex: 6, Created: 27.03.2025 08:15:22", - "BackupIndex: 5, Created: 26.03.2025 19:20:00", - "BackupIndex: 4, Created: 25.03.2025 16:45:11", - "BackupIndex: 3, Created: 24.03.2025 14:30:00", - "BackupIndex: 2, Created: 23.03.2025 10:15:22", - "BackupIndex: 1, Created: 22.03.2025 08:45:11" - ], - consoleMessages: [ - "Preview mode active, simulating after-startconsole output", - "***Stationeers - 0.2.5499.24517***", - "loaded 48 systems successfully", - "game manager initialized", - "World Loaded in 0:0", - "RocketNet Succesfully hosted with Address: 0.0.0.0 Port: 27016", - "14:40:06: StartSession. config:", - "gameName: Preview Server", - "mapName: Preview Server", - "No clients connected. Auto pause timer started (10000ms)", - "Ready" - ] -}; - -// Utility function for typing text -function typeText(element, speed) { - const fullText = element.textContent; - element.textContent = ''; - let i = 0; - - const typeChar = () => { - if (i < fullText.length) { - element.textContent += fullText.charAt(i++); - setTimeout(typeChar, speed); - } - }; - typeChar(); -} - -// Utility function for typing text with a callback -function typeTextWithCallback(element, text, speed, callback) { - element.textContent = ''; - let i = 0; - - const typeChar = () => { - if (i < text.length) { - element.textContent += text.charAt(i++); - setTimeout(typeChar, speed); - } else if (callback) { - setTimeout(callback, 50); - } - }; - typeChar(); -} - -// Tab management -function setupTabs() { - showTab('console-tab'); -} - -function showTab(tabId) { - document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active')); - document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); - const tab = document.getElementById(tabId); - tab.classList.add('active'); - document.querySelector(`.tab-button[onclick*="showTab('${tabId}')"]`).classList.add('active'); -} - -// Mocked server control functions -function startServer() { - mockToggleServer('start'); -} - -function stopServer() { - mockToggleServer('stop'); -} - -function mockToggleServer(action) { - const status = document.getElementById('status'); - const messages = { - start: "Server started. Tthis is a preview - no actual server is running. But try to set this tool up yourself - it's easy! 😉", - stop: "Server stopped. This is a preview - no actual server was running. Have you tried to set this tool up yourself yet? Come on, you even hit this button! 😉" - }; - - status.hidden = false; - typeTextWithCallback(status, messages[action], 20, () => { - setTimeout(() => status.hidden = true, 20000); - }); -} - -function navigateTo(url) { - window.location.href = url; -} - -// Mock detection events -function mockDetectionEvents() { - const detectionConsole = document.getElementById('detection-console'); - - // Initial population - addInitialEvents(); - - // Then add new events periodically - setInterval(() => { - const randomEvent = MOCK.detectionEvents[Math.floor(Math.random() * MOCK.detectionEvents.length)]; - addDetectionEvent(randomEvent); - }, 5000); - - function addInitialEvents() { - // Add a subset of events initially - const initialEvents = [ - "Preview mode active, simulating console output", - ]; - - initialEvents.forEach(eventText => { - addDetectionEvent(eventText); - }); - } - - function addDetectionEvent(eventText) { - const message = document.createElement('div'); - message.className = `detection-event ${getEventClassName(eventText)}`; - - const timestamp = document.createElement('span'); - timestamp.className = 'event-timestamp'; - timestamp.textContent = `${new Date().toLocaleTimeString()}: `; - - const content = document.createElement('span'); - content.textContent = eventText; - - message.append(timestamp, content); - detectionConsole.appendChild(message); - - const maxMessages = 500; - while (detectionConsole.childElementCount > maxMessages) { - detectionConsole.firstChild.remove(); - } - detectionConsole.scrollTop = detectionConsole.scrollHeight; - - const detectionTab = document.getElementById('detection-tab'); - if (!detectionTab.classList.contains('active')) { - const tabButton = document.querySelector('.tab-button[onclick*="detection-tab"]'); - tabButton.classList.add('notification'); - setTimeout(() => tabButton.classList.remove('notification'), 3000); - } - } -} - -function getEventClassName(eventText) { - const checks = [ - ['Server is ready', 'event-server-ready'], - ['Server is starting', 'event-server-starting'], - ['Server error', 'event-server-error'], - ['Player', 'connecting', 'event-player-connecting'], - ['Player', 'ready', 'event-player-ready'], - ['Player', 'disconnected', 'event-player-disconnect'], - ['World Saved', 'event-world-saved'], - ['Exception', 'event-exception'] - ]; - - return checks.find(([text, condition]) => - condition ? eventText.includes(text) && eventText.includes(condition) : eventText.includes(text) - )?.[1] || ''; -} - -// Mock backups functionality -function mockBackups() { - const backupList = document.getElementById('backupList'); - backupList.innerHTML = ''; - - MOCK.backups.forEach(backup => { - const li = document.createElement('li'); - li.className = 'backup-item'; - li.innerHTML = `${backup} `; - backupList.appendChild(li); - }); -} - -function extractIndex(backupText) { - return backupText.match(/Index: (\d+)/)?.[1] || null; -} - -function restoreBackup(index) { - const status = document.getElementById('status'); - const message = `Restored backup with index ${index}. (This is a preview - no actual restoration occurred)`; - - status.hidden = false; - typeTextWithCallback(status, message, 20, () => { - setTimeout(() => status.hidden = true, 30000); - }); -} - -// Mock console with simulated output -function mockConsole() { - const consoleElement = document.getElementById('console'); - consoleElement.innerHTML = ''; - const bootTitle = "Interface initializing..."; - const bootCompleteMessage = "Interface ready.🎮 Happy gaming! 🎮"; - const bugChance = Math.random(); - const bugMessage = "ERROR: Nuclear parts in airflow detected! Initiating repair sequence..."; - - const funMessages = [ - "Calibrating quantum flux capacitors...", - "Initializing player happiness modules...", - "Checking for monsters under the server...", - "Brewing coffee for the CPU...", - "Charging laser sharks...", - "Teaching AI to say 'please' and 'thank you'...", - "Polishing pixels to a mirror shine...", - "Convincing electrons to flow in the right direction...", - "Rebooting atmospheric systems for the 17th time...", - "Attempting to locate your body after that last airlock malfunction...", - "Converting oxygen to errors at alarming efficiency...", - "Persuading physics engine to acknowledge gravity exists...", - "Calculating ways your base will catastrophically depressurize...", - "Optimizing unity garbage collection (good luck with that)...", - "Aligning planetary rotation with server tick rate...", - "Patching holes in space-time continuum and your habitat...", - "Convincing solar panels that 'sun' is not just a theoretical concept...", - "Negotiating peace treaty between logic circuits and the laws of thermodynamics...", - "Compressing atmosphere until your CPU begs for mercy...", - "Measuring distance between you and nearest fatal bug...", - "Attempting to explain 'pipe networks' to confused server hamsters...", - "Calculating probability of survival (spoiler: it's low)...", - "Wrangling rogue Unity instances back into containment...", - "Sacrificing RAM to the gods of stable framerates...", - "Convincing electrons to flow in the right direction... nope, the power grid's borked.", - "Patching hull breaches with duct tape and prayers...", - "Recalculating O2 levels... wait, why is it all CO2 now?", - "Spinning up the fabricator... hope it doesn't eat the server this time.", - "Debugging Unity physics... object launched into orbit, send help.", - "Warming up the furnace... or just setting the base on fire, 50/50 shot.", - "Rerouting pipes... because who needs logical fluid dynamics anyway?", - "Loading terrain... oh look, it's floating 3 meters above the ground again.", - "Processing ore... into a fine paste of lag and despair.", - "Stabilizing frame rate... lol, just kidding, welcome to 12 FPS city.", - "Checking for updates... new bug introduced, feature still broken!", - "Assembling solar tracker... now it's tracking the admin instead.", - "Balancing gas mixtures... kaboom imminent, run you fool!" - ]; - - const addMessage = (text, color, style = 'normal') => { - const div = document.createElement('div'); - div.textContent = text; - div.style.color = color; - div.style.fontStyle = style; - consoleElement.appendChild(div); - consoleElement.scrollTop = consoleElement.scrollHeight; - }; - - // Start with initializing message - typeTextWithCallback(consoleElement, bootTitle, 30, () => { - // Show two funny messages while connecting - const messageIndex1 = Math.floor(Math.random() * funMessages.length); - addMessage(funMessages[messageIndex1], '#0af', 'italic'); - - let messageIndex2; - do { - messageIndex2 = Math.floor(Math.random() * funMessages.length); - } while (messageIndex2 === messageIndex1); - addMessage(funMessages[messageIndex2], '#0af', 'italic'); - - // Simulate connection to server - setTimeout(() => { - // Add bug message occasionally - if (bugChance < 0.05) { - addMessage(bugMessage, 'red'); - setTimeout(() => { - addMessage("Repair complete. Continuing initialization...", 'green'); - completeBootAndSimulateMessages(); - }, 1000); - } else { - completeBootAndSimulateMessages(); - } - }, 1000); - }); - - function completeBootAndSimulateMessages() { - // Show boot complete message - addMessage(bootCompleteMessage, '#0f0'); - - // Start simulating console messages - let messageIndex = 0; - - function addNextMessage() { - if (messageIndex < MOCK.consoleMessages.length) { - addMessage(MOCK.consoleMessages[messageIndex], '#fff'); - messageIndex++; - - const delay = 2000 + Math.random() * 3000; // Random delay between 2-5 seconds - setTimeout(addNextMessage, delay); - } else { - // When we've shown all messages, start again with random selection - setTimeout(() => { - const randomMessage = MOCK.consoleMessages[Math.floor(Math.random() * MOCK.consoleMessages.length)]; - addMessage(randomMessage, '#fff'); - setTimeout(addNextMessage, 3000 + Math.random() * 5000); - }, 3000); - } - } - - // Start showing console messages after a delay - setTimeout(addNextMessage, 1500); - } -} - -function createPlanet(container, size, orbitRadius, speed, color) { - const orbit = document.createElement('div'); - orbit.classList.add('orbit'); - orbit.style.width = `${orbitRadius * 2}px`; - orbit.style.height = `${orbitRadius * 2}px`; - orbit.style.position = 'absolute'; - orbit.style.left = '50%'; - orbit.style.top = '50%'; - orbit.style.transform = 'translate(-50%, -50%)'; - - // Add random delay to start animation at different points - const randomDelay = -(Math.random() * speed); // Negative delay to offset start - orbit.style.animation = `orbit ${speed}s linear infinite ${randomDelay}s`; - - const planet = document.createElement('div'); - planet.classList.add('planet'); - planet.style.width = `${size}px`; - planet.style.height = `${size}px`; - planet.style.position = 'absolute'; - planet.style.left = '0%'; - planet.style.top = '50%'; - planet.style.backgroundColor = color; - planet.style.borderRadius = '50%'; - planet.style.boxShadow = `0 0 20px ${color}`; - - orbit.appendChild(planet); - container.appendChild(orbit); -} \ No newline at end of file diff --git a/.github/workflows/deploy-ui-preview.yml.disabled b/.github/workflows/deploy-ui-preview.yml.disabled deleted file mode 100644 index d52bb5bb..00000000 --- a/.github/workflows/deploy-ui-preview.yml.disabled +++ /dev/null @@ -1,69 +0,0 @@ -name: Deploy UI Preview to GitHub Pages - -on: - push: - branches: [main] - -permissions: - pages: write - id-token: write - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout main - uses: actions/checkout@v4 - with: - ref: main - - - name: Set up static folder - run: | - mkdir -p static - cp UIMod/ui/config.html static/ - cp UIMod/assets/apiinfo.html static/ - cp UIMod/ui/style.css static/ - cp UIMod/assets/favicon.ico static/ - cp UIMod/assets/stationeers.png static/ - cp .github/mock-backend-for-demo.js static/script.js - - - name: Modify index.html - run: | - # Copy index.html to root - cp UIMod/ui/index.html . - # Fix button navigation - sed -i "s|navigateTo('/config')|navigateTo('/static/config.html')|g" index.html - # Fix static paths with repo name - REPO_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f2) - sed -i "s|/static/|/${REPO_NAME}/static/|g" index.html - - - name: Modify config.html - run: | - REPO_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f2) - sed -i "s|/static/|/${REPO_NAME}/static/|g" ./static/config.html - # Fix back buttons - sed -i "s|window.location.href = '/'|window.location.href = '/${REPO_NAME}/'|g" ./static/config.html - # Fix save buttons to redirect instead of submit - sed -i "s|document.getElementById('server-config-form').submit()|window.location.href = '/${REPO_NAME}/'|g" ./static/config.html - - - name: Modify apiinfo.html - run: | - REPO_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f2) - sed -i "s|/static/|/${REPO_NAME}/static/|g" ./static/apiinfo.html - # Fix back to dashboard button - sed -i "s|window.location.href = '/'|window.location.href = '/${REPO_NAME}/'|g" ./static/apiinfo.html - - - name: Update version and branch in index.html - run: | - VERSION=$(git describe --tags --abbrev=0 || echo "v4.X") - BRANCH="Demo" - sed -i "s|

Stationeers Server UI v{{.Version}} ({{.Branch}})

|

Stationeers Server UI ${VERSION} (${BRANCH})

|g" index.html - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: . - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 96964aac..d5794090 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ rocketstation_DedicatedServer_Data/ UnityCrashHandler64.exe UnityPlayer.dll setting.xml -rocketstation_DedicatedServer.exe +rocketstation_DedicatedServer* /saves/* .github/workflows/nightly-sync.yml repos.md @@ -22,3 +22,21 @@ Blacklist.txt UIMod/detectionmanager/customdetections.json UIMod/tls/cert.pem UIMod/tls/key.pem +steamapps/** +steamcmd/** +rocketstation_BurstDebugInformation_DoNotShip/** +StationeersServerControlv* +UnityPlayer.so +BepInEx/** +*doorstep* +*doorstop* +run_bepinex.sh +debug.log +modconfig.xml +UIMod/config/config.json +C:/custom/file.txt +UIMod/config/customdetections.json +winhttp.dll +autostart* +__debug_bin* + diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..8e47c3b8 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "golang.go", + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..87340238 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Go Server", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/server.go", + "console": "integratedTerminal", + "showLog": false, // Hides some Go Debugger(Delve) log stuff that is not useful for debugging atm + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..282dbd0c --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,20 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "shell", + "label": "go: run build.go", + "command": "go", + "args": [ + "run", + "./build/build.go" + ], + "problemMatcher": [ + "$go" + ], + "group": "build", + "detail": "Build Go binarys in ./build with fresh svelte assets", + "hide": false + } + ] +} \ No newline at end of file diff --git a/UIMod/detectionmanager/detectionmanager.html b/UIMod/detectionmanager/detectionmanager.html deleted file mode 100644 index e6a8e87b..00000000 --- a/UIMod/detectionmanager/detectionmanager.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - Custom Detection Manager - - - - - - - - - - -
-
- -
-
-

Custom Detection Manager

-
-
- - - -
-
- -
-
-
-
-
Type
-
Pattern
-
Message
-
Actions
-
-
-
No custom detections found. Add one to get started.
-
-
-
- -
-
-
-
- Change Detection Mode - - Keyword -
- -
- - -
Text to match exactly (case-sensitive)
-
- -
- - -
Message to display when pattern is detected
-
- - - -
-
- -
-
-
- -
-

Custom Detection Patterns

-

Custom detections allow you to create custom patterns for detection. These patterns can be used to detect specific events in the server logs.

-

To create a custom detection, you can use the "Add Detection Tab" to define a regex pattern or alternatively a simple string match ("keyword") and a message that will be logged in the Events and if enabled in Discord when the pattern is detected. It is not possible to create patterns that have faulty regex.

-
-

Creating Effective Detections

-
-
-

Keyword Detection:

-

For example, to detect the "Unsupported shader" message that unity logs when a shader is not supported, you would use the following pattern:

- Pattern: "Unsupported shader" - Message: "Unity detected an unsupported shader. This may cause unexpected behavior." -
-
-

Regex Detection:

-

For example, to detect (fictional) the "Player (.+) has reached level (\d+)" message that is logged when a player reaches a certain level in an elevator, you would use the following pattern:

- Pattern: "Player (.+) has reached level (\d+)" - Message: "Player {1} has reached level {2}" -

The AI of your choise will be more than happy to help you create effective detections. You can also use the Regex101 tool to test your patterns.

-
-
-

For more information, visit the GitHub Wiki

-
-
- -
- -
- -
-
- - - - - - \ No newline at end of file diff --git a/UIMod/detectionmanager/detectionmanager.js b/UIMod/detectionmanager/detectionmanager.js deleted file mode 100644 index 5f361742..00000000 --- a/UIMod/detectionmanager/detectionmanager.js +++ /dev/null @@ -1,211 +0,0 @@ -// Show active tab -function showTab(tabId) { - document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active')); - document.querySelectorAll('.tab-button').forEach(button => button.classList.remove('active')); - - document.getElementById(tabId).classList.add('active'); - document.querySelector(`.tab-button[data-tab="${tabId}"]`).classList.add('active'); - - if (tabId === 'detection-list-tab') { - loadDetections(); - } -} - -// Toggle detection type -function setupDetectionTypeToggle() { - const toggle = document.getElementById('detection-type-toggle'); - const typeLabel = document.getElementById('detection-type-label'); - const typeInput = document.getElementById('type'); - const patternInfo = document.getElementById('pattern-info'); - const messageInfo = document.getElementById('message-info'); - - toggle.addEventListener('change', function() { - if (this.checked) { - typeLabel.textContent = 'Regex'; - typeInput.value = 'regex'; - patternInfo.textContent = 'Regular expression pattern (e.g., "Player (.+) has reached level (\\d+)")'; - messageInfo.textContent = 'Message to display when pattern is detected. Use {1}, {2}, etc. for captured groups'; - } else { - typeLabel.textContent = 'Keyword'; - typeInput.value = 'keyword'; - patternInfo.textContent = 'Text to match exactly (case-sensitive)'; - messageInfo.textContent = 'Message to display when pattern is detected'; - } - }); -} - -// Load detections -function loadDetections() { - const loader = document.getElementById('list-loader'); - const detectionItems = document.getElementById('detection-items'); - - loader.style.display = 'block'; - - fetch('/api/v2/custom-detections') - .then(response => { - if (!response.ok) throw new Error('Failed to load detections'); - return response.json(); - }) - .then(detections => { - loader.style.display = 'none'; - - if (detections.length === 0) { - detectionItems.innerHTML = '
No custom detections found. Add one to get started.
'; - return; - } - - detectionItems.innerHTML = ''; - detections.forEach(detection => { - const item = document.createElement('div'); - item.className = 'detection-item'; - item.innerHTML = ` -
${detection.type}
-
${escapeHtml(detection.pattern)}
-
${escapeHtml(detection.message)}
-
- -
- `; - detectionItems.appendChild(item); - }); - }) - .catch(error => { - loader.style.display = 'none'; - showNotification('Error: ' + error.message, 'error'); - console.error('Error loading detections:', error); - }); -} - -// Submit detection -function submitDetection() { - const form = document.getElementById('detection-form'); - const type = document.getElementById('type').value; - const pattern = document.getElementById('pattern').value.trim(); - const message = document.getElementById('message').value.trim(); - - if (!pattern || !message) { - showNotification('Please fill in all fields', 'error'); - return; - } - - const data = { - type: type, - pattern: pattern, - eventType: 'CUSTOM_DETECTION', - message: message - }; - - fetch('/api/v2/custom-detections', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }) - .then(response => { - if (!response.ok) { - return response.text().then(text => { throw new Error(text || 'Failed to add detection'); }); - } - return response.json(); - }) - .then(() => { - form.reset(); - document.getElementById('detection-type-toggle').checked = false; - document.getElementById('detection-type-label').textContent = 'Keyword'; - document.getElementById('type').value = 'keyword'; - showNotification('Detection added successfully', 'success'); - showTab('detection-list-tab'); - }) - .catch(error => { - showNotification('Error: ' + error.message, 'error'); - console.error('Error adding detection:', error); - }); -} - -// Delete detection -function deleteDetection(id) { - - fetch(`/api/v2/custom-detections/delete/?id=${id}`, { method: 'DELETE' }) - .then(response => { - if (!response.ok) { - return response.text().then(text => { throw new Error(text || 'Failed to delete detection'); }); - } - showNotification('Detection deleted successfully', 'success'); - loadDetections(); - }) - .catch(error => { - showNotification('Error: ' + error.message, 'error'); - console.error('Error deleting detection:', error); - }); -} - -// Show notification -function showNotification(message, type) { - const notification = document.getElementById('notification'); - notification.textContent = message; - notification.className = `notification notification-${type}`; - notification.style.display = 'block'; - - setTimeout(() => { - notification.style.display = 'none'; - }, 5000); -} - -// Helper function to escape HTML -function escapeHtml(unsafe) { - return unsafe - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} - -// Event listeners -document.addEventListener('DOMContentLoaded', () => { - loadDetections(); - setupDetectionTypeToggle(); - document.querySelectorAll('.tab-button').forEach(button => { - button.addEventListener('click', () => showTab(button.getAttribute('data-tab'))); - }); - document.querySelector('.add-button').addEventListener('click', submitDetection); -}); - - -function resourceSaver(pause) { - // Get space background once outside the loop - const spaceBackground = document.getElementById('space-background'); - - // Handle animation states for all elements - document.querySelectorAll('*').forEach(element => { - element.style.animationPlayState = pause ? 'paused' : 'running'; - }); - - // Fade the space background in/out instead of abrupt display change - if (pause) { - // Fade out - spaceBackground.style.transition = 'opacity 0.5s ease'; - spaceBackground.style.opacity = '0'; - // Only hide it after the fade completes - setTimeout(() => { - if (document.hasFocus() === false) { // Double-check we're still unfocused - spaceBackground.style.display = 'none'; - } - }, 500); - } else { - // Make it visible first, then fade in - spaceBackground.style.display = 'block'; - // Use setTimeout to ensure the display change is processed before starting the fade - setTimeout(() => { - spaceBackground.style.transition = 'opacity 0.5s ease'; - spaceBackground.style.opacity = '1'; - }, 10); - } -} - -// Event listeners for window focus and blur -window.addEventListener('focus', () => { - resourceSaver(false); // Resume animations when page is in focus -}); - -window.addEventListener('blur', () => { - resourceSaver(true); // Pause animations when page loses focus -}); \ No newline at end of file diff --git a/UIMod/assets/apiinfo.html b/UIMod/onboard_bundled/assets/apiinfo.html similarity index 100% rename from UIMod/assets/apiinfo.html rename to UIMod/onboard_bundled/assets/apiinfo.html diff --git a/UIMod/assets/css/apiinfo.css b/UIMod/onboard_bundled/assets/css/apiinfo.css similarity index 100% rename from UIMod/assets/css/apiinfo.css rename to UIMod/onboard_bundled/assets/css/apiinfo.css diff --git a/UIMod/assets/css/background.css b/UIMod/onboard_bundled/assets/css/background.css similarity index 100% rename from UIMod/assets/css/background.css rename to UIMod/onboard_bundled/assets/css/background.css diff --git a/UIMod/assets/css/base.css b/UIMod/onboard_bundled/assets/css/base.css similarity index 100% rename from UIMod/assets/css/base.css rename to UIMod/onboard_bundled/assets/css/base.css diff --git a/UIMod/assets/css/components.css b/UIMod/onboard_bundled/assets/css/components.css similarity index 98% rename from UIMod/assets/css/components.css rename to UIMod/onboard_bundled/assets/css/components.css index 66e0fdf0..a1ef562f 100644 --- a/UIMod/assets/css/components.css +++ b/UIMod/onboard_bundled/assets/css/components.css @@ -16,6 +16,8 @@ button { position: relative; overflow: hidden; will-change: transform, box-shadow; + overflow-wrap: break-word; + hyphens: auto; } button::before { diff --git a/UIMod/assets/css/config.css b/UIMod/onboard_bundled/assets/css/config.css similarity index 100% rename from UIMod/assets/css/config.css rename to UIMod/onboard_bundled/assets/css/config.css diff --git a/UIMod/assets/css/detectionmanager.css b/UIMod/onboard_bundled/assets/css/detectionmanager.css similarity index 100% rename from UIMod/assets/css/detectionmanager.css rename to UIMod/onboard_bundled/assets/css/detectionmanager.css diff --git a/UIMod/assets/css/home.css b/UIMod/onboard_bundled/assets/css/home.css similarity index 100% rename from UIMod/assets/css/home.css rename to UIMod/onboard_bundled/assets/css/home.css diff --git a/UIMod/assets/css/mobile.css b/UIMod/onboard_bundled/assets/css/mobile.css similarity index 100% rename from UIMod/assets/css/mobile.css rename to UIMod/onboard_bundled/assets/css/mobile.css diff --git a/UIMod/assets/css/sscm.css b/UIMod/onboard_bundled/assets/css/sscm.css similarity index 88% rename from UIMod/assets/css/sscm.css rename to UIMod/onboard_bundled/assets/css/sscm.css index bba0c745..4050fc57 100644 --- a/UIMod/assets/css/sscm.css +++ b/UIMod/onboard_bundled/assets/css/sscm.css @@ -66,11 +66,12 @@ .sscm-suggestion-item { display: flex; - align-items: center; + align-items: flex-start; padding: 10px 14px; color: var(--text-bright, #ffffff); cursor: pointer; transition: all 0.2s ease; + gap: 10px; } .sscm-suggestion-item:hover, @@ -81,27 +82,27 @@ .sscm-suggestion-name { font-weight: 600; - flex: 0 0 150px; + flex: 0 0 120px; + align-self: center; } .sscm-suggestion-params { color: var(--text-muted, #d1d5db); - font-size: 12px; - flex: 0 0 200px; - margin-left: 10px; + flex: 0 0 250px; + overflow-wrap: break-word; + line-height: 1.3; + max-height: 60px; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } .sscm-suggestion-desc { color: var(--text-dim, #9ca3af); - font-size: 12px; flex: 1; - margin-left: 10px; + overflow-wrap: break-word; + hyphens: auto; + line-height: 1.3; + max-height: 60px; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } .sscm-suggestions::-webkit-scrollbar { diff --git a/UIMod/assets/css/style.css b/UIMod/onboard_bundled/assets/css/style.css similarity index 100% rename from UIMod/assets/css/style.css rename to UIMod/onboard_bundled/assets/css/style.css diff --git a/UIMod/assets/css/tabs.css b/UIMod/onboard_bundled/assets/css/tabs.css similarity index 100% rename from UIMod/assets/css/tabs.css rename to UIMod/onboard_bundled/assets/css/tabs.css diff --git a/UIMod/assets/css/variables.css b/UIMod/onboard_bundled/assets/css/variables.css similarity index 100% rename from UIMod/assets/css/variables.css rename to UIMod/onboard_bundled/assets/css/variables.css diff --git a/UIMod/assets/favicon.ico b/UIMod/onboard_bundled/assets/favicon.ico similarity index 100% rename from UIMod/assets/favicon.ico rename to UIMod/onboard_bundled/assets/favicon.ico diff --git a/UIMod/assets/js/console-manager.js b/UIMod/onboard_bundled/assets/js/console-manager.js similarity index 99% rename from UIMod/assets/js/console-manager.js rename to UIMod/onboard_bundled/assets/js/console-manager.js index 82f62c2e..70f9c04a 100644 --- a/UIMod/assets/js/console-manager.js +++ b/UIMod/onboard_bundled/assets/js/console-manager.js @@ -216,7 +216,7 @@ function handleConsole() { createCommandInput(); // Add input after boot addMessage(bootCompleteMessage, '#0f0'); //addMessage("StationeersServerUI is becoming SteamServerUI!", '#ff4500'); - addMessage("Please mind the New Terrain System warning below", '#ff4500'); + //addMessage("Please mind the New Terrain System warning below", '#ff4500'); consoleElement.scrollTop = consoleElement.scrollHeight; }, 500); } diff --git a/UIMod/assets/js/detectionmanager.js b/UIMod/onboard_bundled/assets/js/detectionmanager.js similarity index 100% rename from UIMod/assets/js/detectionmanager.js rename to UIMod/onboard_bundled/assets/js/detectionmanager.js diff --git a/UIMod/assets/js/main.js b/UIMod/onboard_bundled/assets/js/main.js similarity index 100% rename from UIMod/assets/js/main.js rename to UIMod/onboard_bundled/assets/js/main.js diff --git a/UIMod/assets/js/server-api.js b/UIMod/onboard_bundled/assets/js/server-api.js similarity index 81% rename from UIMod/assets/js/server-api.js rename to UIMod/onboard_bundled/assets/js/server-api.js index fea515d2..9ac55440 100644 --- a/UIMod/assets/js/server-api.js +++ b/UIMod/onboard_bundled/assets/js/server-api.js @@ -22,6 +22,26 @@ function toggleServer(endpoint) { .catch(err => console.error(`Failed to ${endpoint}:`, err)); } +function triggerSteamCMD() { + const status = document.getElementById('status'); + status.hidden = false; + typeTextWithCallback(status, 'Triggering SteamCMD, please wait. SteamCMD will print log output only to the CLI ', 20, () => { + fetch('/api/v2/steamcmd/run') + .then(response => response.json()) + .then(data => { + typeTextWithCallback(status, data.message, 20, () => { + setTimeout(() => status.hidden = true, 10000); + }); + }) + .catch(err => { + typeTextWithCallback(status, 'Error: Failed to trigger SteamCMD', 20, () => { + setTimeout(() => status.hidden = true, 10000); + }); + console.error(`Failed to trigger SteamCMD:`, err); + }); + }); +} + // Backup management function fetchBackups() { fetch('/api/v2/backups?mode=classic') diff --git a/UIMod/sscm/sscm.js b/UIMod/onboard_bundled/assets/js/sscm.js similarity index 98% rename from UIMod/sscm/sscm.js rename to UIMod/onboard_bundled/assets/js/sscm.js index 8850f8ba..ce639c3c 100644 --- a/UIMod/sscm/sscm.js +++ b/UIMod/onboard_bundled/assets/js/sscm.js @@ -57,7 +57,7 @@ const availableCommands = [ { name: "test", params: "", desc: "Tests colors" }, { name: "testbytearray", params: "", desc: "Tests network read/write" }, { name: "testoctree", params: "[[number of iterations]]", desc: "Benchmarks read density" }, - { name: "thing", params: "[find ,delete ,spawn [amount],info ,...]", desc: "Manages things" }, + { name: "thing", params: "[find ,delete ,spawn [amount],info ]", desc: "Manages things" }, { name: "trader", params: "[regenerate,land,depart,contacts,buys,sells,evaluate,checksum]", desc: "Trader debug commands" }, { name: "unstuck", params: "", desc: "Attempts to unstick player" }, { name: "upnp", params: "", desc: "Shows UPnP state" }, @@ -67,7 +67,6 @@ const availableCommands = [ { name: "worldsetting", params: "", desc: "Authors WorldSetting info" } ]; -// Check if SSCM is enabled (unchanged) async function checkSSCMEnabled() { try { const response = await fetch('/api/v2/SSCM/enabled', { @@ -80,7 +79,7 @@ async function checkSSCMEnabled() { input.placeholder = "Enter command..."; } else { input.onclick = () => { - window.location.href = "/setup?step=sscm_opt_in"; + window.location.href = "/setup?step=sscm"; }; input.placeholder = "SSCM is not enabled, commands unavailable. Click here to configure."; } diff --git a/UIMod/assets/js/ui-utils.js b/UIMod/onboard_bundled/assets/js/ui-utils.js similarity index 100% rename from UIMod/assets/js/ui-utils.js rename to UIMod/onboard_bundled/assets/js/ui-utils.js diff --git a/UIMod/assets/script.js b/UIMod/onboard_bundled/assets/script.js similarity index 100% rename from UIMod/assets/script.js rename to UIMod/onboard_bundled/assets/script.js diff --git a/UIMod/assets/stationeers.png b/UIMod/onboard_bundled/assets/stationeers.png similarity index 100% rename from UIMod/assets/stationeers.png rename to UIMod/onboard_bundled/assets/stationeers.png diff --git a/UIMod/ui/detectionmanager.html b/UIMod/onboard_bundled/detectionmanager/detectionmanager.html similarity index 100% rename from UIMod/ui/detectionmanager.html rename to UIMod/onboard_bundled/detectionmanager/detectionmanager.html diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/UIMod/onboard_bundled/localization/de-DE.json new file mode 100644 index 00000000..2c992566 --- /dev/null +++ b/UIMod/onboard_bundled/localization/de-DE.json @@ -0,0 +1,307 @@ +{ + "UIText": { + "index": { + "UIText_StartButton": "Server starten", + "UIText_StopButton": "Server stoppen", + "UIText_Settings": "Optionen", + "UIText_Update_SteamCMD": "Server Updaten", + "UIText_Console": "Konsole", + "UIText_Detection_Events": "Ereignisse", + "UIText_Backup_Manager": "Spielstands-Manager", + "UIText_Discord_Info": "Tritt dem Discord bei und hilf uns SSUI besser zu machen oder Support anzufragen!", + "UIText_API_Info": "API-Endpunktdokumentation", + "UIText_Copyright": "Urheberrecht", + "UIText_Copyright1": "Lizenziert unter", + "UIText_Copyright2": "Proprietärer Lizenz." + }, + "config": { + "UIText_ServerConfig": "Server Konfiguration", + "UIText_DiscordIntegration": "Discord Integration", + "UIText_DetectionManager": "Erkennungsmanager", + "UIText_ConfigurationWizard": "Konfigurations-Assistent", + "UIText_PleaseSelectSection": "Bitte wähle oben eine Konfigurationssektion aus", + "UIText_UseWizardAlternative": "Alternativ nutze den Konfigurations-Assistenten zur Serverkonfiguration.", + "UIText_BasicSettings": "Grundeinstellungen", + "UIText_NetworkSettings": "Netzwerk-Einstellungen", + "UIText_AdvancedSettings": "Erweiterte Einstellungen", + "UIText_BetaSettings": "Beta-Einstellungen", + "basic": { + "UIText_BasicServerSettings": "Grundlegende Servereinstellungen", + "UIText_ServerName": "Servername", + "UIText_ServerNameInfo": "Name in der Serverliste angezeigt", + "UIText_SaveFileName": "Speicherdatei Name", + "UIText_SaveFileNameInfo": "Name des Speicherordners. Muss großgeschrieben sein. Für neue Welt, Welttyp angeben. (MeineVulkanKarte Vulcan) Welttypen im Stationeers Wiki -> Dedicated Server.", + "UIText_MaxPlayers": "Max Spieler", + "UIText_MaxPlayersInfo": "Maximale Anzahl erlaubter Spieler", + "UIText_ServerPassword": "Server Passwort", + "UIText_ServerPasswordInfo": "Leer lassen für kein Passwort", + "UIText_AdminPassword": "Admin Passwort", + "UIText_AdminPasswordInfo": "Server Admin Passwort", + "UIText_AutoSave": "Auto Speichern", + "UIText_AutoSaveInfo": "Auf TRUE setzen für automatisches Speichern", + "UIText_SaveInterval": "Speicher Intervall", + "UIText_SaveIntervalInfo": "Zeit in Sekunden zwischen Speichervorgängen", + "UIText_AutoPauseServer": "Server Auto Pausieren", + "UIText_AutoPauseServerInfo": "Server automatisch pausieren wenn keine Spieler verbunden" + }, + "network": { + "UIText_NetworkConfiguration": "Netzwerk Konfiguration", + "UIText_GamePort": "Spiel Port", + "UIText_GamePortInfo": "Standard: 27016", + "UIText_UpdatePort": "Update Port", + "UIText_UpdatePortInfo": "Standard: 27015", + "UIText_UPNPEnabled": "UPNP Aktiviert", + "UIText_UPNPEnabledInfo": "Automatische UPNP Portweiterleitung aktivieren", + "UIText_LocalIpAddress": "Lokale IP Adresse", + "UIText_LocalIpAddressInfo": "IP Adresse zum Binden", + "UIText_StartLocalHost": "Lokalen Host Starten", + "UIText_StartLocalHostInfo": "Auf TRUE setzen um nur im lokalen Netzwerk zu hören", + "UIText_ServerVisible": "Server Sichtbar", + "UIText_ServerVisibleInfo": "Auf TRUE setzen um Server öffentlich zu listen", + "UIText_UseSteamP2P": "Steam P2P Nutzen", + "UIText_UseSteamP2PInfo": "Steam Peer-to-Peer Netzwerk aktivieren" + }, + "advanced": { + "UIText_AdvancedConfiguration": "Erweiterte Konfiguration", + "UIText_ServerAuthSecret": "Server Auth Geheimnis", + "UIText_ServerAuthSecretInfo": "Authentifizierungsgeheimnis für Server (optional)", + "UIText_ServerExePath": "Server Ausführungspfad", + "UIText_ServerExePathInfo": "Systempfad zur Server-Anwendung", + "UIText_ServerExePathInfo2": "Aus Sicherheitsgründen nicht über UI editierbar, aber manuell in config.json änderbar.", + "UIText_AdditionalParams": "Zusätzliche Parameter", + "UIText_AdditionalParamsInfo": "Format: EigenParam1 Wert1 EigenParam2 Wert2", + "UIText_AutoRestartServerTimer": "Geplanter Gameserver Neustart", + "UIText_AutoRestartServerTimerInfo": "Zeitrahmen in Minuten für automatischen Gameserver Neustart. 0 = deaktiviert, 1440 = 24 Stunden, etc. Mit SSCM siehst du \"Achtung, Server startet neu in 30/20/10/5 Sekunden!\" Nachrichten im Spiel.", + "UIText_GameBranch": "Spiel Branch", + "UIText_GameBranchInfo": "Branch des Spiels. Bei Änderung SSUI Neustart erforderlich!" + }, + "beta": { + "UIText_BetaOnlySettings": "NUR BETA: NEUE TERRAIN UND SPEICHERSYSTEM EINSTELLUNGEN", + "UIText_BetaWarning": "Diese Einstellungen sind nur nützlich bei Stationeers Dedicated Server Beta. Wechsel möglich unter Erweiterte Einstellungen -> Spiel Branch. Alte Speicherstände NICHT kompatibel, aber migrierbar mit JacksonTheMaster's Migration Tool. \"How to\" Infos im FAQ gelten auch für Dedicated Server.", + "UIText_UseNewTerrainAndSave": "Neues Terrain- und Speichersystem Nutzen", + "UIText_UseNewTerrainAndSaveInfo": "Auf TRUE setzen für .save Dateien im Backup Manager und Argument-Parsing. Bei false funktionieren nur Stationeers Versionen vor Terrain-Überarbeitung (≈ Mitte 2025). Standard false bis neues System veröffentlicht.", + "UIText_Difficulty": "Schwierigkeit", + "UIText_DifficultyInfo": "Schwierigkeit für Welterstellung. Standard Normal wenn leer.", + "UIText_StartCondition": "Startbedingung", + "UIText_StartConditionInfo": "Startbedingung für Welterstellung. Standard-Startbedingung für Welttyp wenn leer.", + "UIText_StartLocation": "Startort", + "UIText_StartLocationInfo": "Startort für Welterstellung. Standard DefaultStartLocation wenn leer.", + "UIText_AutoStartServerOnStartup": "Server Auto-Start beim Hochfahren", + "UIText_AutoStartServerOnStartupInfo": "Gameserver automatisch starten wenn SSUI gestartet wird. Standard false." + }, + "discord": { + "UIText_DiscordIntegrationTitle": "Discord Integration Vorteile", + "UIText_DiscordBotToken": "Discord Bot Token", + "UIText_DiscordBotTokenInfo": "Authentifizierungstoken deines Discord Bots", + "UIText_ChannelConfiguration": "Channel Konfiguration", + "UIText_AdminCommandChannel": "Admin Command Channel", + "UIText_AdminCommandChannelInfo": "Channel für Admin-Befehle", + "UIText_ControlPanelChannel": "Kontrollpanel Channel", + "UIText_ControlPanelChannelInfo": "Channel für Kontrollpanel", + "UIText_StatusChannel": "Status Channel", + "UIText_StatusChannelInfo": "Server Status Updates", + "UIText_ConnectionListChannel": "Verbindungslisten Channel", + "UIText_ConnectionListChannelInfo": "Spielerverbindungs-Tracking", + "UIText_LogChannel": "Log Channel", + "UIText_LogChannelInfo": "Server Log Ausgabe", + "UIText_SaveInfoChannel": "Speicherinfo Channel", + "UIText_SaveInfoChannelInfo": "Speicherdatei Informationen", + "UIText_ErrorChannel": "Fehler Channel", + "UIText_ErrorChannelInfo": "Server Fehlermeldungen", + "UIText_BannedPlayersListPath": "Gesperrte Spieler Liste Pfad", + "UIText_BannedPlayersListPathInfo": "Dateipfad zur gesperrten Spieler Liste", + "UIText_DiscordIntegrationBenefits": "Discord Integration Vorteile", + "UIText_DiscordBenefit1": "Server Status in Echtzeit überwachen", + "UIText_DiscordBenefit2": "Neustarts und Wiederherstellungen remote verwalten", + "UIText_DiscordBenefit3": "Spielerverbindungen verfolgen", + "UIText_DiscordBenefit4": "Community-Management Optionen", + "UIText_DiscordBenefit5": "Echtzeit Fehlerbenachrichtigungen", + "UIText_DiscordSetupInstructions": "Für Setup-Anweisungen besuche die" + } + }, + "setup": { + "UIText_FooterText": "Hilfe benötigt? Schaue ins Stationeers Server UI Github Wiki.", + "UIText_SSCM_FooterText": "Nutze SSCM für das mächtigste Stationeers Server Management! Du kannst Befehle von der Web-Konsole ausführen ohne Vanilla-Verhalten zu stören!", + "UIText_Welcome_Title": "Stationeers Server UI", + "UIText_Welcome_SubmitButton": "Setup Starten", + "UIText_Welcome_SkipButton": "Setup Überspringen", + "UIText_PlsRead_Title": "Bitte lesen!", + "UIText_PlsRead_HeaderTitle": "Wir empfehlen stark, die Texte in diesem Setup-Assistenten zu lesen!", + "UIText_PlsRead_StepMessage": "Die meisten gemeldeten Probleme entstehen durch Fehlkonfiguration.", + "UIText_PlsRead_SubmitButton": "Verstanden", + "UIText_PlsRead_SkipButton": "Verstanden", + "UIText_ServerName_Title": "Stationeers Server UI", + "UIText_ServerName_HeaderTitle": "Servername Setup", + "UIText_ServerName_StepMessage": "Gib deinem Server einen Namen wie 'Weltraumstation 13'", + "UIText_ServerName_PrimaryPlaceholder": "Mein Stationeers Server mit UI", + "UIText_ServerName_PrimaryLabel": "Servername", + "UIText_ServerName_SubmitButton": "Speichern & Weiter", + "UIText_ServerName_SkipButton": "Überspringen", + "UIText_SaveIdentifier_Title": "Stationeers Server UI", + "UIText_SaveIdentifier_HeaderTitle": "Speicher-Identifikator Setup", + "UIText_SaveIdentifier_StepMessage": "Setze einen Speicher-Identifikator wie 'Weltraumstation13 Vulcan'. Ersten Buchstaben jedes Wortes groß schreiben. Welttypen im Stationeers Wiki -> Dedicated Server", + "UIText_SaveIdentifier_PrimaryPlaceholder": "Benötigt SaveName und WorldType für ersten Start!", + "UIText_SaveIdentifier_PrimaryLabel": "Speicher-Identifikator", + "UIText_SaveIdentifier_SubmitButton": "Speichern & Weiter", + "UIText_SaveIdentifier_SkipButton": "Überspringen", + "UIText_MaxPlayers_Title": "Stationeers Server UI", + "UIText_MaxPlayers_HeaderTitle": "Spielerlimit Setup", + "UIText_MaxPlayers_StepMessage": "Wähle die maximale Anzahl Spieler die sich verbinden können.", + "UIText_MaxPlayers_PrimaryPlaceholder": "8", + "UIText_MaxPlayers_PrimaryLabel": "Max Spieler", + "UIText_MaxPlayers_SubmitButton": "Speichern & Weiter", + "UIText_MaxPlayers_SkipButton": "Überspringen", + "UIText_ServerPassword_Title": "Stationeers Server UI", + "UIText_ServerPassword_HeaderTitle": "Server Passwort Setup", + "UIText_ServerPassword_StepMessage": "Setze ein Gameserver Passwort oder überspringe diesen Schritt.", + "UIText_ServerPassword_PrimaryPlaceholder": "Server Passwort", + "UIText_ServerPassword_PrimaryLabel": "Server Passwort", + "UIText_ServerPassword_SubmitButton": "Speichern & Weiter", + "UIText_ServerPassword_SkipButton": "Überspringen", + "UIText_GameBranch_Title": "Stationeers Server UI", + "UIText_GameBranch_HeaderTitle": "Spiel Branch Setup", + "UIText_GameBranch_StepMessage": "Gib einen Beta-Branch ein oder überspringe für Release-Version. Bei Branch-Wechsel SSUI nach Assistenten n e u s t a r t e n.", + "UIText_GameBranch_PrimaryPlaceholder": "beta", + "UIText_GameBranch_PrimaryLabel": "Spiel Branch", + "UIText_GameBranch_SubmitButton": "Speichern & Weiter", + "UIText_GameBranch_SkipButton": "Release Version nutzen", + "UIText_NewTerrainAndSaveSystem_Title": "TERRAINSYSTEM WÄHLEN", + "UIText_NewTerrainAndSaveSystem_HeaderTitle": "Sehr wichtiger Schritt!", + "UIText_NewTerrainAndSaveSystem_StepMessage": "Gerade zu Beta gewechselt? Terrain- und Speichersystem umschalten! 'ja' eingeben zum Aktivieren oder 'nein' zum Deaktivieren.", + "UIText_NewTerrainAndSaveSystem_PrimaryPlaceholder": "ja/nein", + "UIText_NewTerrainAndSaveSystem_PrimaryLabel": "Neues System aktivieren", + "UIText_NewTerrainAndSaveSystem_SubmitButton": "Speichern & Weiter", + "UIText_NewTerrainAndSaveSystem_SkipButton": "Überspringen", + "UIText_DiscordEnabled_Title": "Stationeers Server UI", + "UIText_DiscordEnabled_HeaderTitle": "Discord Integration", + "UIText_DiscordEnabled_StepMessage": "Discord Integration aktivieren? 'ja' eingeben zum Aktivieren oder Überspringen zum Deaktivieren.", + "UIText_DiscordEnabled_PrimaryPlaceholder": "ja", + "UIText_DiscordEnabled_PrimaryLabel": "Discord Aktivieren", + "UIText_DiscordEnabled_SubmitButton": "Speichern & Weiter", + "UIText_DiscordEnabled_SkipButton": "Überspringen (Discord Deaktivieren)", + "UIText_DiscordToken_Title": "Stationeers Server UI", + "UIText_DiscordToken_HeaderTitle": "Discord Bot Token", + "UIText_DiscordToken_StepMessage": "Gib deinen Discord Bot Token für Server Integration ein", + "UIText_DiscordToken_PrimaryPlaceholder": "Discord Bot Token", + "UIText_DiscordToken_PrimaryLabel": "Discord Token", + "UIText_DiscordToken_SubmitButton": "Speichern & Weiter", + "UIText_DiscordToken_SkipButton": "Überspringen", + "UIText_ControlPanelChannel_Title": "Stationeers Server UI", + "UIText_ControlPanelChannel_HeaderTitle": "Discord Channel Setup (1/6)", + "UIText_ControlPanelChannel_StepMessage": "Discord Kontrollpanel Channel ID eingeben", + "UIText_ControlPanelChannel_PrimaryPlaceholder": "Channel ID", + "UIText_ControlPanelChannel_PrimaryLabel": "Kontrollpanel Channel ID", + "UIText_ControlPanelChannel_SubmitButton": "Speichern & Weiter", + "UIText_ControlPanelChannel_SkipButton": "Überspringen", + "UIText_SaveChannel_Title": "Stationeers Server UI", + "UIText_SaveChannel_HeaderTitle": "Discord Channel Setup (2/6)", + "UIText_SaveChannel_StepMessage": "Discord Speicher Channel ID eingeben", + "UIText_SaveChannel_PrimaryPlaceholder": "Channel ID", + "UIText_SaveChannel_PrimaryLabel": "Speicher Channel ID", + "UIText_SaveChannel_SubmitButton": "Speichern & Weiter", + "UIText_SaveChannel_SkipButton": "Überspringen", + "UIText_LogChannel_Title": "Stationeers Server UI", + "UIText_LogChannel_HeaderTitle": "Discord Channel Setup (3/6)", + "UIText_LogChannel_StepMessage": "Discord Log Channel ID eingeben", + "UIText_LogChannel_PrimaryPlaceholder": "Channel ID", + "UIText_LogChannel_PrimaryLabel": "Log Channel ID", + "UIText_LogChannel_SubmitButton": "Speichern & Weiter", + "UIText_LogChannel_SkipButton": "Überspringen", + "UIText_ConnectionListChannel_Title": "Stationeers Server UI", + "UIText_ConnectionListChannel_HeaderTitle": "Discord Channel Setup (4/6)", + "UIText_ConnectionListChannel_StepMessage": "Discord Verbindungslisten Channel ID eingeben", + "UIText_ConnectionListChannel_PrimaryPlaceholder": "Channel ID", + "UIText_ConnectionListChannel_PrimaryLabel": "Verbindungslisten Channel ID", + "UIText_ConnectionListChannel_SubmitButton": "Speichern & Weiter", + "UIText_ConnectionListChannel_SkipButton": "Überspringen", + "UIText_StatusChannel_Title": "Stationeers Server UI", + "UIText_StatusChannel_HeaderTitle": "Discord Channel Setup (5/6)", + "UIText_StatusChannel_StepMessage": "Discord Status Channel ID eingeben", + "UIText_StatusChannel_PrimaryPlaceholder": "Channel ID", + "UIText_StatusChannel_PrimaryLabel": "Status Channel ID", + "UIText_StatusChannel_SubmitButton": "Speichern & Weiter", + "UIText_StatusChannel_SkipButton": "Überspringen", + "UIText_ControlChannel_Title": "Stationeers Server UI", + "UIText_ControlChannel_HeaderTitle": "Discord Channel Setup (6/6)", + "UIText_ControlChannel_StepMessage": "Discord Control Channel ID eingeben", + "UIText_ControlChannel_PrimaryPlaceholder": "Channel ID", + "UIText_ControlChannel_PrimaryLabel": "Control Channel ID", + "UIText_ControlChannel_SubmitButton": "Speichern & Weiter", + "UIText_ControlChannel_SkipButton": "Überspringen", + "UIText_NetworkConfigChoice_Title": "Stationeers Server UI", + "UIText_NetworkConfigChoice_HeaderTitle": "Netzwerk Konfiguration", + "UIText_NetworkConfigChoice_StepMessage": "Netzwerkeinstellungen konfigurieren? 'ja' für Konfiguration oder Überspringen für Standards. Hinweis: Netzwerkkonfiguration besonders wichtig auf Linux Servern.", + "UIText_NetworkConfigChoice_PrimaryPlaceholder": "ja", + "UIText_NetworkConfigChoice_PrimaryLabel": "Netzwerk Konfigurieren", + "UIText_NetworkConfigChoice_SubmitButton": "Weiter", + "UIText_NetworkConfigChoice_SkipButton": "Überspringen (Standards nutzen)", + "UIText_GamePort_Title": "Stationeers Server UI", + "UIText_GamePort_HeaderTitle": "Netzwerk Setup (4/4)", + "UIText_GamePort_StepMessage": "Port-Nummer für Spielverbindungen eingeben", + "UIText_GamePort_PrimaryPlaceholder": "27016", + "UIText_GamePort_PrimaryLabel": "Spiel Port", + "UIText_GamePort_SubmitButton": "Speichern & Weiter", + "UIText_GamePort_SkipButton": "Überspringen", + "UIText_UpdatePort_Title": "Stationeers Server UI", + "UIText_UpdatePort_HeaderTitle": "Netzwerk Setup (4/4)", + "UIText_UpdatePort_StepMessage": "Port-Nummer für Update-Verbindungen eingeben", + "UIText_UpdatePort_PrimaryPlaceholder": "27015", + "UIText_UpdatePort_PrimaryLabel": "Update Port", + "UIText_UpdatePort_SubmitButton": "Speichern & Weiter", + "UIText_UpdatePort_SkipButton": "Überspringen", + "UIText_UPnPEnabled_Title": "Stationeers Server UI", + "UIText_UPnPEnabled_HeaderTitle": "Netzwerk Setup (4/4)", + "UIText_UPnPEnabled_StepMessage": "UPnP aktivieren? 'ja' zum Aktivieren oder 'nein' zum Deaktivieren.", + "UIText_UPnPEnabled_PrimaryPlaceholder": "ja/nein", + "UIText_UPnPEnabled_PrimaryLabel": "UPnP Aktivieren", + "UIText_UPnPEnabled_SubmitButton": "Speichern & Weiter", + "UIText_UPnPEnabled_SkipButton": "Überspringen", + "UIText_LocalIPAddress_Title": "Stationeers Server UI", + "UIText_LocalIPAddress_HeaderTitle": "Netzwerk Setup (4/4)", + "UIText_LocalIPAddress_StepMessage": "Lokale IP-Adresse des Servers im Format 0.0.0.0 eingeben (keine CIDR Notation)", + "UIText_LocalIPAddress_PrimaryPlaceholder": "0.0.0.0", + "UIText_LocalIPAddress_PrimaryLabel": "Lokale IP-Adresse", + "UIText_LocalIPAddress_SubmitButton": "Speichern & Weiter", + "UIText_LocalIPAddress_SkipButton": "Überspringen", + "UIText_AdminAccount_Title": "Stationeers Server UI", + "UIText_AdminAccount_HeaderTitle": "Admin Account Setup", + "UIText_AdminAccount_StepMessage": "Richte deinen Admin-Account ein.", + "UIText_AdminAccount_PrimaryPlaceholder": "Benutzername", + "UIText_AdminAccount_PrimaryLabel": "Benutzername", + "UIText_AdminAccount_SecondaryLabel": "Passwort", + "UIText_AdminAccount_SecondaryPlaceholder": "Passwort", + "UIText_AdminAccount_SubmitButton": "Speichern & Weiter", + "UIText_AdminAccount_SkipButton": "Authentifizierung Überspringen", + "UIText_SSCM_Title": "Stationeers Server Command Manager", + "UIText_SSCM_HeaderTitle": "Einzigartige Funktion", + "UIText_SSCM_StepMessage": "SSCM ist ein maßgeschneidertes Server-Plugin für direkte Serverbefehl-Ausführung aus SSUI. Es ermöglicht Befehle aus der Web-Konsole ohne Störung des Vanilla-Verhaltens, keine Client-seitigen Mods nötig.", + "UIText_SSCM_PrimaryPlaceholder": "'nein' eingeben zum Deaktivieren", + "UIText_SSCM_PrimaryLabel": "Deaktivierung NICHT empfohlen.", + "UIText_SSCM_SubmitButton": "Weiter", + "UIText_SSCM_SkipButton": "Aktiviert lassen", + "UIText_Finalize_Title": "Setup Abschließen", + "UIText_Finalize_StepMessage": "Bereit zum Abschließen? Deine Konfiguration wurde bereits während des Setups gespeichert. Für Änderungen klicke 'Zurück zum Start' und überspringe was behalten werden soll. Meiste Optionen auch im Config Tab änderbar.", + "UIText_Finalize_SubmitButton": "Zurück zum Start", + "UIText_Finalize_SkipButton": "Authentifizierung Überspringen", + "UIText_Login_Title": "Stationeers Server UI", + "UIText_Login_PrimaryLabel": "Benutzername", + "UIText_Login_SecondaryLabel": "Passwort", + "UIText_Login_PrimaryPlaceholder": "Benutzername eingeben", + "UIText_Login_SecondaryPlaceholder": "Passwort eingeben", + "UIText_Login_SubmitButton": "Anmelden", + "UIText_ChangeUser_Title": "Stationeers Server UI", + "UIText_ChangeUser_HeaderTitle": "Benutzer Verwalten", + "UIText_ChangeUser_PrimaryLabel": "Benutzername Hinzufügen/Aktualisieren", + "UIText_ChangeUser_SecondaryLabel": "Neues Passwort", + "UIText_ChangeUser_SecondaryPlaceholder": "Passwort", + "UIText_ChangeUser_SubmitButton": "Benutzer Hinzufügen/Aktualisieren" + } + }, + "BackendText": { + "top1": {}, + "nest1": { + "nestINnest1": {}, + "nestINnest2": {} + } + } +} \ No newline at end of file diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json new file mode 100644 index 00000000..9ddbceb5 --- /dev/null +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -0,0 +1,311 @@ +{ + "UIText": { + "index": { + "UIText_StartButton": "Start Server", + "UIText_StopButton": "Stop Server", + "UIText_Settings": "Edit Config", + "UIText_Update_SteamCMD": "Update Server", + "UIText_Console": "Console", + "UIText_Detection_Events": "Detection Events", + "UIText_Backup_Manager": "Backup Manager", + "UIText_Discord_Info": "Join the Discord and help make SSUI better or get support!", + "UIText_API_Info": "API Endpoint Reference", + "UIText_Copyright": "Copyright", + "UIText_Copyright1": "Licensed under", + "UIText_Copyright2": "Proprietary License." + }, + "config": { + "UIText_ServerConfig": "Server Configuration", + "UIText_DiscordIntegration": "Discord Integration", + "UIText_DetectionManager": "Detection Manager", + "UIText_ConfigurationWizard": "Configuration Wizard", + "UIText_PleaseSelectSection": "Please select a configuration section above", + "UIText_UseWizardAlternative": "Alternatively, use the Configuration Wizard to configure the server.", + "UIText_BasicSettings": "Basic Settings", + "UIText_NetworkSettings": "Network Settings", + "UIText_AdvancedSettings": "Advanced Settings", + "UIText_BetaSettings": "Beta Settings", + "basic": { + "UIText_BasicServerSettings": "Basic Server Settings", + "UIText_ServerName": "Server Name", + "UIText_ServerNameInfo": "Name displayed in server list", + "UIText_SaveFileName": "Save File Name", + "UIText_SaveFileNameInfo": "Name of save folder. Must be capitalized. To create a new world, provide the World type to generate. (MyVulcanMap Vulcan) WorldTypes can be found in the Stationeers Wiki -> Dedicated Server page.", + "UIText_MaxPlayers": "Max Players", + "UIText_MaxPlayersInfo": "Maximum number of players allowed", + "UIText_ServerPassword": "Server Password", + "UIText_ServerPasswordInfo": "Leave empty for no password", + "UIText_AdminPassword": "Admin Password", + "UIText_AdminPasswordInfo": "Server Admin Password", + "UIText_AutoSave": "Auto Save", + "UIText_AutoSaveInfo": "Set to TRUE to enable automatic saving", + "UIText_SaveInterval": "Save Interval", + "UIText_SaveIntervalInfo": "Time in seconds between saves", + "UIText_AutoPauseServer": "Auto Pause Server", + "UIText_AutoPauseServerInfo": "Automatically pause server when no players are connected" + }, + "network": { + "UIText_NetworkConfiguration": "Network Configuration", + "UIText_GamePort": "Game Port", + "UIText_GamePortInfo": "Default: 27016", + "UIText_UpdatePort": "Update Port", + "UIText_UpdatePortInfo": "Default: 27015", + "UIText_UPNPEnabled": "UPNP Enabled", + "UIText_UPNPEnabledInfo": "Enable automatic UPNP port forwarding", + "UIText_LocalIpAddress": "Local IP Address", + "UIText_LocalIpAddressInfo": "IP address to bind to", + "UIText_StartLocalHost": "Start Local Host", + "UIText_StartLocalHostInfo": "Set to TRUE to listen only on local network", + "UIText_ServerVisible": "Server Visible", + "UIText_ServerVisibleInfo": "Set to TRUE to list server publicly", + "UIText_UseSteamP2P": "Use Steam P2P", + "UIText_UseSteamP2PInfo": "Enable Steam Peer-to-Peer networking" + }, + "advanced": { + "UIText_AdvancedConfiguration": "Advanced Configuration", + "UIText_ServerAuthSecret": "Server Auth Secret", + "UIText_ServerAuthSecretInfo": "Authentication secret for the server (optional)", + "UIText_ServerExePath": "Server Executable Path", + "UIText_ServerExePathInfo": "System path to server executable", + "UIText_ServerExePathInfo2": "Not editable from the UI for security reasons, but you can edit it manually in the config.json file.", + "UIText_AdditionalParams": "Additional Parameters", + "UIText_AdditionalParamsInfo": "Format: CustomParam1 Value1 CustomParam2 Value2", + "UIText_AutoRestartServerTimer": "Scheduled Gameserver Restart", + "UIText_AutoRestartServerTimerInfo": "Timeframe in minutes to schedule an automatic gameserver restart. 0 = disabled, 1440 = 24 hours, etc.. If SSCM is enabled, you will see \"Attention, server is restarting in 30/20/10/5 seconds!\" messages ingame before restarting.", + "UIText_GameBranch": "Game Branch", + "UIText_GameBranchInfo": "Branch of the game to use. When changed, requires to restart SSUI!" + }, + "beta": { + "UIText_BetaOnlySettings": "BETA ONLY: NEW TERRAIN AND SAVE SYSTEM SETTINGS", + "UIText_BetaWarning": "These settings are only useful if you are running the Stationeers Dedicated Server Beta. Switching is possible from Advanced Settings -> Game Branch. Old savegames are NOT compatible with the new system, but can be migrated with JacksonTheMaster's Migration tool.. Related \"how to\" Information on the FAQ there also applies to the Dedicated Server.", + "UIText_UseNewTerrainAndSave": "Use New Terrain and Save System", + "UIText_UseNewTerrainAndSaveInfo": "Set to TRUE to enable handling of .save files in the Backup manager and argument parsing. If set to false, only Stationeers versions before the terrain rework (≈ mid 2025) will work. Defaults to false until new Stationeers Terrain and Save System is released.", + "UIText_Difficulty": "Difficulty", + "UIText_DifficultyInfo": "Difficulty to create the world with. Defaults to Normal if empty.", + "UIText_StartCondition": "Start Condition", + "UIText_StartConditionInfo": "Start condition to create the world with. Defaults to the default start condition for the world type if empty.", + "UIText_StartLocation": "Start Location", + "UIText_StartLocationInfo": "Start location to create the world with. Defaults to DefaultStartLocation if empty.", + "UIText_AutoStartServerOnStartup": "Auto Start Server on Startup", + "UIText_AutoStartServerOnStartupInfo": "Automatically start the gameserver when the SSUI is started. Defaults to false." + }, + "discord": { + "UIText_DiscordIntegrationTitle": "Discord Integration Benefits", + "UIText_DiscordBotToken": "Discord Bot Token", + "UIText_DiscordBotTokenInfo": "Your Discord bot's authentication token", + "UIText_ChannelConfiguration": "Channel Configuration", + "UIText_AdminCommandChannel": "Admin Command Channel", + "UIText_AdminCommandChannelInfo": "Channel for admin commands", + "UIText_ControlPanelChannel": "Control Panel Channel", + "UIText_ControlPanelChannelInfo": "Channel for control panel", + "UIText_StatusChannel": "Status Channel", + "UIText_StatusChannelInfo": "Server status updates", + "UIText_ConnectionListChannel": "Connection List Channel", + "UIText_ConnectionListChannelInfo": "Player connection tracking", + "UIText_LogChannel": "Log Channel", + "UIText_LogChannelInfo": "Server log output", + "UIText_SaveInfoChannel": "Save Info Channel", + "UIText_SaveInfoChannelInfo": "Save file information", + "UIText_ErrorChannel": "Error Channel", + "UIText_ErrorChannelInfo": "Server error messages", + "UIText_BannedPlayersListPath": "Banned Players List Path", + "UIText_BannedPlayersListPathInfo": "File path to banned players list", + "UIText_DiscordIntegrationBenefits": "Discord Integration Benefits", + "UIText_DiscordBenefit1": "Monitor server status in real-time", + "UIText_DiscordBenefit2": "Manage restarts and restores remotely", + "UIText_DiscordBenefit3": "Track player connections", + "UIText_DiscordBenefit4": "Community management options", + "UIText_DiscordBenefit5": "Real-time error notifications", + "UIText_DiscordSetupInstructions": "For setup instructions, visit the" + } + }, + "setup": { + "UIText_FooterText": "Need help? Check the Stationeers Server UI Github Wiki.", + "UIText_SSCM_FooterText": "Use SSCM for the most powerful Stationeers server management! You can run commands from the Web console without disrupting vanilla behaviour!", + "UIText_Welcome_Title": "Stationeers Server UI", + "UIText_Welcome_SubmitButton": "Start Setup", + "UIText_Welcome_SkipButton": "Skip Setup", + "UIText_PlsRead_Title": "Please read!", + "UIText_PlsRead_HeaderTitle": "We strongly recommend you to read the texts in this setup wizard!", + "UIText_PlsRead_StepMessage": "Most reported issues occur because of a misconfiguration.", + "UIText_PlsRead_SubmitButton": "I understand", + "UIText_PlsRead_SkipButton": "I understand", + "UIText_ServerName_Title": "Stationeers Server UI", + "UIText_ServerName_HeaderTitle": "Server Name Setup", + "UIText_ServerName_StepMessage": "Give your server a name like 'Space Station 13'", + "UIText_ServerName_PrimaryPlaceholder": "My Stationeers Server with UI", + "UIText_ServerName_PrimaryLabel": "Server Name", + "UIText_ServerName_SubmitButton": "Save & Continue", + "UIText_ServerName_SkipButton": "Skip", + "UIText_SaveIdentifier_Title": "Stationeers Server UI", + "UIText_SaveIdentifier_HeaderTitle": "Save Identifier Setup", + "UIText_SaveIdentifier_StepMessage": "Set a save identifier like 'SpaceStation13 Vulcan'. Capitalize the first letter of each word. Possible World types can be found in the Stationeers Wiki -> Dedicated Server", + "UIText_SaveIdentifier_PrimaryPlaceholder": "Requires a SaveName and WorldType for first start!", + "UIText_SaveIdentifier_PrimaryLabel": "Save Identifier", + "UIText_SaveIdentifier_SubmitButton": "Save & Continue", + "UIText_SaveIdentifier_SkipButton": "Skip", + "UIText_MaxPlayers_Title": "Stationeers Server UI", + "UIText_MaxPlayers_HeaderTitle": "Player Limit Setup", + "UIText_MaxPlayers_StepMessage": "Choose the maximum number of players that can connect to the server.", + "UIText_MaxPlayers_PrimaryPlaceholder": "8", + "UIText_MaxPlayers_PrimaryLabel": "Max Players", + "UIText_MaxPlayers_SubmitButton": "Save & Continue", + "UIText_MaxPlayers_SkipButton": "Skip", + "UIText_ServerPassword_Title": "Stationeers Server UI", + "UIText_ServerPassword_HeaderTitle": "Server Password Setup", + "UIText_ServerPassword_StepMessage": "Set a gameserver password or skip this step.", + "UIText_ServerPassword_PrimaryPlaceholder": "Server Password", + "UIText_ServerPassword_PrimaryLabel": "Server Password", + "UIText_ServerPassword_SubmitButton": "Save & Continue", + "UIText_ServerPassword_SkipButton": "Skip", + "UIText_GameBranch_Title": "Stationeers Server UI", + "UIText_GameBranch_HeaderTitle": "Game Branch Setup", + "UIText_GameBranch_StepMessage": "Enter a beta branch or skip this to use the release version. If switching branches, make sure to r e s t a r t SSUI after completing this wizzard.", + "UIText_GameBranch_PrimaryPlaceholder": "beta", + "UIText_GameBranch_PrimaryLabel": "Game Branch", + "UIText_GameBranch_SubmitButton": "Save & Continue", + "UIText_GameBranch_SkipButton": "Use Release Version", + "UIText_NewTerrainAndSaveSystem_Title": "CHOOSE TERRAIN SYSTEM", + "UIText_NewTerrainAndSaveSystem_HeaderTitle": "Very important step!", + "UIText_NewTerrainAndSaveSystem_StepMessage": "Just switched to Beta? Flip Terrain and Save System to support that! Enter 'yes' to enable or 'no' to disable.", + "UIText_NewTerrainAndSaveSystem_PrimaryPlaceholder": "yes/no", + "UIText_NewTerrainAndSaveSystem_PrimaryLabel": "Enable new System", + "UIText_NewTerrainAndSaveSystem_SubmitButton": "Save & Continue", + "UIText_NewTerrainAndSaveSystem_SkipButton": "Skip", + "UIText_DiscordEnabled_Title": "Stationeers Server UI", + "UIText_DiscordEnabled_HeaderTitle": "Discord Integration", + "UIText_DiscordEnabled_StepMessage": "Do you want to enable Discord integration? Enter 'yes' to enable or Skip to disable.", + "UIText_DiscordEnabled_PrimaryPlaceholder": "yes", + "UIText_DiscordEnabled_PrimaryLabel": "Enable Discord", + "UIText_DiscordEnabled_SubmitButton": "Save & Continue", + "UIText_DiscordEnabled_SkipButton": "Skip (Disable Discord)", + "UIText_DiscordToken_Title": "Stationeers Server UI", + "UIText_DiscordToken_HeaderTitle": "Discord Bot Token", + "UIText_DiscordToken_StepMessage": "Enter your Discord bot token for server integration", + "UIText_DiscordToken_PrimaryPlaceholder": "Discord Bot Token", + "UIText_DiscordToken_PrimaryLabel": "Discord Token", + "UIText_DiscordToken_SubmitButton": "Save & Continue", + "UIText_DiscordToken_SkipButton": "Skip", + "UIText_ControlPanelChannel_Title": "Stationeers Server UI", + "UIText_ControlPanelChannel_HeaderTitle": "Discord Channel Setup (1/6)", + "UIText_ControlPanelChannel_StepMessage": "Enter Discord Control Panel Channel ID", + "UIText_ControlPanelChannel_PrimaryPlaceholder": "Channel ID", + "UIText_ControlPanelChannel_PrimaryLabel": "Control Panel Channel ID", + "UIText_ControlPanelChannel_SubmitButton": "Save & Continue", + "UIText_ControlPanelChannel_SkipButton": "Skip", + "UIText_SaveChannel_Title": "Stationeers Server UI", + "UIText_SaveChannel_HeaderTitle": "Discord Channel Setup (2/6)", + "UIText_SaveChannel_StepMessage": "Enter Discord Save Channel ID", + "UIText_SaveChannel_PrimaryPlaceholder": "Channel ID", + "UIText_SaveChannel_PrimaryLabel": "Save Channel ID", + "UIText_SaveChannel_SubmitButton": "Save & Continue", + "UIText_SaveChannel_SkipButton": "Skip", + "UIText_LogChannel_Title": "Stationeers Server UI", + "UIText_LogChannel_HeaderTitle": "Discord Channel Setup (3/6)", + "UIText_LogChannel_StepMessage": "Enter Discord Log Channel ID", + "UIText_LogChannel_PrimaryPlaceholder": "Channel ID", + "UIText_LogChannel_PrimaryLabel": "Log Channel ID", + "UIText_LogChannel_SubmitButton": "Save & Continue", + "UIText_LogChannel_SkipButton": "Skip", + "UIText_ConnectionListChannel_Title": "Stationeers Server UI", + "UIText_ConnectionListChannel_HeaderTitle": "Discord Channel Setup (4/6)", + "UIText_ConnectionListChannel_StepMessage": "Enter Discord Connection List Channel ID", + "UIText_ConnectionListChannel_PrimaryPlaceholder": "Channel ID", + "UIText_ConnectionListChannel_PrimaryLabel": "Connection List Channel ID", + "UIText_ConnectionListChannel_SubmitButton": "Save & Continue", + "UIText_ConnectionListChannel_SkipButton": "Skip", + "UIText_StatusChannel_Title": "Stationeers Server UI", + "UIText_StatusChannel_HeaderTitle": "Discord Channel Setup (5/6)", + "UIText_StatusChannel_StepMessage": "Enter Discord Status Channel ID", + "UIText_StatusChannel_PrimaryPlaceholder": "Channel ID", + "UIText_StatusChannel_PrimaryLabel": "Status Channel ID", + "UIText_StatusChannel_SubmitButton": "Save & Continue", + "UIText_StatusChannel_SkipButton": "Skip", + "UIText_ControlChannel_Title": "Stationeers Server UI", + "UIText_ControlChannel_HeaderTitle": "Discord Channel Setup (6/6)", + "UIText_ControlChannel_StepMessage": "Enter Discord Control Channel ID", + "UIText_ControlChannel_PrimaryPlaceholder": "Channel ID", + "UIText_ControlChannel_PrimaryLabel": "Control Channel ID", + "UIText_ControlChannel_SubmitButton": "Save & Continue", + "UIText_ControlChannel_SkipButton": "Skip", + "UIText_NetworkConfigChoice_Title": "Stationeers Server UI", + "UIText_NetworkConfigChoice_HeaderTitle": "Network Configuration", + "UIText_NetworkConfigChoice_StepMessage": "Do you want to configure network settings? Enter 'yes' to configure or Skip to use defaults. Note: Network configuration is especially important on Linux servers.", + "UIText_NetworkConfigChoice_PrimaryPlaceholder": "yes", + "UIText_NetworkConfigChoice_PrimaryLabel": "Configure Network", + "UIText_NetworkConfigChoice_SubmitButton": "Continue", + "UIText_NetworkConfigChoice_SkipButton": "Skip (Use Defaults)", + "UIText_GamePort_Title": "Stationeers Server UI", + "UIText_GamePort_HeaderTitle": "Network Setup (1/4)", + "UIText_GamePort_StepMessage": "Enter the port number for game connections", + "UIText_GamePort_PrimaryPlaceholder": "27016", + "UIText_GamePort_PrimaryLabel": "Game Port", + "UIText_GamePort_SubmitButton": "Save & Continue", + "UIText_GamePort_SkipButton": "Skip", + "UIText_UpdatePort_Title": "Stationeers Server UI", + "UIText_UpdatePort_HeaderTitle": "Network Setup (2/4)", + "UIText_UpdatePort_StepMessage": "Enter the port number for update connections", + "UIText_UpdatePort_PrimaryPlaceholder": "27015", + "UIText_UpdatePort_PrimaryLabel": "Update Port", + "UIText_UpdatePort_SubmitButton": "Save & Continue", + "UIText_UpdatePort_SkipButton": "Skip", + "UIText_UPnPEnabled_Title": "Stationeers Server UI", + "UIText_UPnPEnabled_HeaderTitle": "Network Setup (3/4)", + "UIText_UPnPEnabled_StepMessage": "Enable UPnP? Enter 'yes' to enable or 'no' to disable.", + "UIText_UPnPEnabled_PrimaryPlaceholder": "yes/no", + "UIText_UPnPEnabled_PrimaryLabel": "Enable UPnP", + "UIText_UPnPEnabled_SubmitButton": "Save & Continue", + "UIText_UPnPEnabled_SkipButton": "Skip", + "UIText_LocalIPAddress_Title": "Stationeers Server UI", + "UIText_LocalIPAddress_HeaderTitle": "Network Setup (4/4)", + "UIText_LocalIPAddress_StepMessage": "Enter server's local IP address in format 0.0.0.0 (no CIDR notation)", + "UIText_LocalIPAddress_PrimaryPlaceholder": "0.0.0.0", + "UIText_LocalIPAddress_PrimaryLabel": "Local IP Address", + "UIText_LocalIPAddress_SubmitButton": "Save & Continue", + "UIText_LocalIPAddress_SkipButton": "Skip", + "UIText_AdminAccount_Title": "Stationeers Server UI", + "UIText_AdminAccount_HeaderTitle": "Admin Account Setup", + "UIText_AdminAccount_StepMessage": "Set up your admin account.", + "UIText_AdminAccount_PrimaryPlaceholder": "Username", + "UIText_AdminAccount_PrimaryLabel": "Username", + "UIText_AdminAccount_SecondaryLabel": "Password", + "UIText_AdminAccount_SecondaryPlaceholder": "Password", + "UIText_AdminAccount_SubmitButton": "Save & Continue", + "UIText_AdminAccount_SkipButton": "Skip Authentication", + "UIText_SSCM_Title": "Stationeers Server Command Manager", + "UIText_SSCM_HeaderTitle": "Unique Feature", + "UIText_SSCM_StepMessage": "SSCM is a custom server plugin that allows you to execute server commands directly from SSUI. It gives you the ability to run commands from the Web console without disrupting vanlilla behaviour, you dont need any client side mods.", + "UIText_SSCM_PrimaryPlaceholder": "type 'no' to disable", + "UIText_SSCM_PrimaryLabel": "Opting out is NOT recommended.", + "UIText_SSCM_SubmitButton": "Continue", + "UIText_SSCM_SkipButton": "Keep enabled", + "UIText_Finalize_Title": "Finalize Setup", + "UIText_Finalize_StepMessage": "Ready to finalize? Your configuration has already been saved while you completed this setup. If you want to change any of the settings, you may click Return to Start and skip whatever you want to keep. Most options can also be changed on the config Tab in the UI.", + "UIText_Finalize_SubmitButton": "Return to Start", + "UIText_Finalize_SkipButton": "Skip Authentication", + "UIText_Login_Title": "Stationeers Server UI", + "UIText_Login_PrimaryLabel": "Username", + "UIText_Login_SecondaryLabel": "Password", + "UIText_Login_PrimaryPlaceholder": "Enter Username", + "UIText_Login_SecondaryPlaceholder": "Enter Password", + "UIText_Login_SubmitButton": "Login", + "UIText_ChangeUser_Title": "Stationeers Server UI", + "UIText_ChangeUser_HeaderTitle": "Manage Users", + "UIText_ChangeUser_PrimaryLabel": "Username to Add/Update", + "UIText_ChangeUser_SecondaryLabel": "New Password", + "UIText_ChangeUser_SecondaryPlaceholder": "Password", + "UIText_ChangeUser_SubmitButton": "Add/Update User" + } + }, + "BackendText": { + "gamemgr": { + "BackendText_ServerStarted": "Server started.", + "BackendText_ServerNotRunningOrAlreadyStopped": "Server was not running or was already stopped", + "BackendText_ServerStopped": "Server stopped." + } + }, + "nest1": { + "nestINnest1": {}, + "nestINnest2": {} + } +} \ No newline at end of file diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/UIMod/onboard_bundled/localization/sv-SE.json new file mode 100644 index 00000000..1baf27f8 --- /dev/null +++ b/UIMod/onboard_bundled/localization/sv-SE.json @@ -0,0 +1,307 @@ +{ + "UIText": { + "index": { + "UIText_StartButton": "Starta server", + "UIText_StopButton": "Stoppa server", + "UIText_Settings": "Redigera konfig", + "UIText_Update_SteamCMD": "Uppdatera server", + "UIText_Console": "Konsol", + "UIText_Detection_Events": "Detekteringshändelser", + "UIText_Backup_Manager": "Backup-hanterare", + "UIText_Discord_Info": "Gå med i Discord och hjälp till att förbättra SSUI eller få support!", + "UIText_API_Info": "API-slutpunktsreferens", + "UIText_Copyright": "Upphovsrätt", + "UIText_Copyright1": "Licensierad under", + "UIText_Copyright2": "Proprietär licens" + }, + "config": { + "UIText_ServerConfig": "Konfiguration", + "UIText_DiscordIntegration": "Discord-integration", + "UIText_DetectionManager": "Detektering", + "UIText_ConfigurationWizard": "Konfigurationsguide", + "UIText_PleaseSelectSection": "Välj en konfigurationssektion ovan", + "UIText_UseWizardAlternative": "Alternativt, använd konfigurationsguiden för att konfigurera servern.", + "UIText_BasicSettings": "Grund", + "UIText_NetworkSettings": "Nätverk", + "UIText_AdvancedSettings": "Avancerad", + "UIText_BetaSettings": "Beta", + "basic": { + "UIText_BasicServerSettings": "Grundläggande serverinställningar", + "UIText_ServerName": "Servernamn", + "UIText_ServerNameInfo": "Namn som visas i serverlistan", + "UIText_SaveFileName": "Sparfilsnamn", + "UIText_SaveFileNameInfo": "Namn på sparmappen. Måste börja med stor bokstav. För att skapa en ny värld, ange världstypen att generera. (MyVulcanMap Vulcan) Världstyper finns på Stationeers Wiki -> Dedicated Server-sidan.", + "UIText_MaxPlayers": "Max spelare", + "UIText_MaxPlayersInfo": "Maximalt antal tillåtna spelare", + "UIText_ServerPassword": "Serverlösenord", + "UIText_ServerPasswordInfo": "Lämna tomt för inget lösenord", + "UIText_AdminPassword": "Adminlösenord", + "UIText_AdminPasswordInfo": "Lösenord för serveradministratör", + "UIText_AutoSave": "Autospara", + "UIText_AutoSaveInfo": "Sätt till TRUE för att aktivera automatisk sparning", + "UIText_SaveInterval": "Sparintervall", + "UIText_SaveIntervalInfo": "Tid i sekunder mellan sparningar", + "UIText_AutoPauseServer": "Autopausa server", + "UIText_AutoPauseServerInfo": "Pausa servern automatiskt när inga spelare är anslutna" + }, + "network": { + "UIText_NetworkConfiguration": "Nätverkskonfiguration", + "UIText_GamePort": "Spelport", + "UIText_GamePortInfo": "Standard: 27016", + "UIText_UpdatePort": "Uppdateringsport", + "UIText_UpdatePortInfo": "Standard: 27015", + "UIText_UPNPEnabled": "UPNP aktiverad", + "UIText_UPNPEnabledInfo": "Aktivera automatisk UPNP-portvidarebefordran", + "UIText_LocalIpAddress": "Lokal IP-adress", + "UIText_LocalIpAddressInfo": "IP-adress att binda till", + "UIText_StartLocalHost": "Starta lokal värd", + "UIText_StartLocalHostInfo": "Sätt till TRUE för att endast lyssna på lokalt nätverk", + "UIText_ServerVisible": "Server synlig", + "UIText_ServerVisibleInfo": "Sätt till TRUE för att visa servern offentligt", + "UIText_UseSteamP2P": "Använd Steam P2P", + "UIText_UseSteamP2PInfo": "Aktivera Steam Peer-to-Peer-nätverk" + }, + "advanced": { + "UIText_AdvancedConfiguration": "Avancerad konfiguration", + "UIText_ServerAuthSecret": "Serverautentiseringshemlighet", + "UIText_ServerAuthSecretInfo": "Autentiseringshemlighet för servern (valfritt)", + "UIText_ServerExePath": "Sökväg till serverprogram", + "UIText_ServerExePathInfo": "Systemsökväg till serverprogrammet", + "UIText_ServerExePathInfo2": "Kan inte redigeras från gränssnittet av säkerhetsskäl, men du kan ändra det manuellt i config.json-filen.", + "UIText_AdditionalParams": "Ytterligare parametrar", + "UIText_AdditionalParamsInfo": "Format: AnpassadParam1 Värde1 AnpassadParam2 Värde2", + "UIText_AutoRestartServerTimer": "Schemalagd spelserveromstart", + "UIText_AutoRestartServerTimerInfo": "Tidsram i minuter för att schemalägga en automatisk spelserveromstart. 0 = inaktiverad, 1440 = 24 timmar, osv. Om SSCM är aktiverat visas meddelanden som \"Varning, servern startar om om 30/20/10/5 sekunder!\" i spelet före omstart.", + "UIText_GameBranch": "Spelgren", + "UIText_GameBranchInfo": "Spelgren att använda. Vid ändring krävs omstart av SSUI!" + }, + "beta": { + "UIText_BetaOnlySettings": "ENDAST BETA: INSTÄLLNINGAR FÖR NYTT TERRÄNG- OCH SPARSYSTEM", + "UIText_BetaWarning": "Dessa inställningar är endast användbara om du kör Stationeers Dedicated Server Beta. Byte är möjligt via Avancerade inställningar -> Spelgren. Gamla sparfiler är INTE kompatibla med det nya systemet, men kan migreras med JacksonTheMasters Migreringsverktyg. Relaterad \"hur man gör\"-info i FAQ gäller även för Dedicated Server.", + "UIText_UseNewTerrainAndSave": "Använd nytt system", + "UIText_UseNewTerrainAndSaveInfo": "Sätt till TRUE för att aktivera hantering av .save-filer i Backup-hanteraren och argumentparsning. Om satt till false fungerar endast Stationeers-versioner före terrängomarbetningen (≈ mitten av 2025). Standard är false tills nya Stationeers terräng- och sparsystemet släpps.", + "UIText_Difficulty": "Svårighetsgrad", + "UIText_DifficultyInfo": "Svårighetsgrad för världsskapande. Standard är Normal om tomt.", + "UIText_StartCondition": "Startvillkor", + "UIText_StartConditionInfo": "Startvillkor för världsskapande. Standard är världstypens standardvillkor om tomt.", + "UIText_StartLocation": "Startplats", + "UIText_StartLocationInfo": "Startplats för världsskapande. Standard är DefaultStartLocation om tomt.", + "UIText_AutoStartServerOnStartup": "Starta servern automatiskt vid uppstart", + "UIText_AutoStartServerOnStartupInfo": "Starta spelservern automatiskt när SSUI startas. Standard är false." + }, + "discord": { + "UIText_DiscordIntegrationTitle": "Fördelar med Discord-integration", + "UIText_DiscordBotToken": "Discord-bot-token", + "UIText_DiscordBotTokenInfo": "Autentiseringstoken för din Discord-bot", + "UIText_ChannelConfiguration": "Kanalkonfiguration", + "UIText_AdminCommandChannel": "Admin-kommandokanal", + "UIText_AdminCommandChannelInfo": "Kanal för admin-kommandon", + "UIText_ControlPanelChannel": "Kontrollpanelkanal", + "UIText_ControlPanelChannelInfo": "Kanal för kontrollpanel", + "UIText_StatusChannel": "Statuskanal", + "UIText_StatusChannelInfo": "Uppdateringar om serverstatus", + "UIText_ConnectionListChannel": "Anslutningslistkanal", + "UIText_ConnectionListChannelInfo": "Spårning av spelaranslutningar", + "UIText_LogChannel": "Loggkanal", + "UIText_LogChannelInfo": "Utdata för serverloggar", + "UIText_SaveInfoChannel": "Sparinfokanal", + "UIText_SaveInfoChannelInfo": "Information om sparfiler", + "UIText_ErrorChannel": "Felkanal", + "UIText_ErrorChannelInfo": "Felmeddelanden från servern", + "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", + "UIText_DiscordBenefit1": "Övervaka serverstatus i realtid", + "UIText_DiscordBenefit2": "Hantera omstarter och återställningar på distans", + "UIText_DiscordBenefit3": "Spåra spelaranslutningar", + "UIText_DiscordBenefit4": "Alternativ för community-hantering", + "UIText_DiscordBenefit5": "Felnotiser i realtid", + "UIText_DiscordSetupInstructions": "För installationsinstruktioner, besök" + } + }, + "setup": { + "UIText_FooterText": "Behöver du hjälp? Kolla Stationeers Server UI Github Wiki.", + "UIText_SSCM_FooterText": "Använd SSCM för den mest kraftfulla hanteringen av Stationeers-servrar! Du kan köra kommandon från webbkonsolen utan att störa vanliga funktioner!", + "UIText_Welcome_Title": "Stationeers Server UI", + "UIText_Welcome_SubmitButton": "Börja konfigurera", + "UIText_Welcome_SkipButton": "Hoppa över konfiguration", + "UIText_PlsRead_Title": "Läs detta!", + "UIText_PlsRead_HeaderTitle": "Viktigt att läsa!", + "UIText_PlsRead_StepMessage": "De flesta rapporterade problemen beror på felkonfiguration.", + "UIText_PlsRead_SubmitButton": "Jag förstår", + "UIText_PlsRead_SkipButton": "Jag förstår", + "UIText_ServerName_Title": "Stationeers Server UI", + "UIText_ServerName_HeaderTitle": "Servernamninställning", + "UIText_ServerName_StepMessage": "Ge din server ett namn, t.ex. 'Rymdstation 13'", + "UIText_ServerName_PrimaryPlaceholder": "Min Stationeers-server med UI", + "UIText_ServerName_PrimaryLabel": "Servernamn", + "UIText_ServerName_SubmitButton": "Spara & fortsätt", + "UIText_ServerName_SkipButton": "Hoppa över", + "UIText_SaveIdentifier_Title": "Stationeers Server UI", + "UIText_SaveIdentifier_HeaderTitle": "Sparidentifieringsinställning", + "UIText_SaveIdentifier_StepMessage": "Ange en sparidentifierare, t.ex. 'SpaceStation13 Vulcan'. Använd stor bokstav i början av varje ord. Världstyper finns på Stationeers Wiki -> Dedicated Server.", + "UIText_SaveIdentifier_PrimaryPlaceholder": "Kräver ett sparnamn och världstyp vid första start!", + "UIText_SaveIdentifier_PrimaryLabel": "Sparidentifierare", + "UIText_SaveIdentifier_SubmitButton": "Spara & fortsätt", + "UIText_SaveIdentifier_SkipButton": "Hoppa över", + "UIText_MaxPlayers_Title": "Stationeers Server UI", + "UIText_MaxPlayers_HeaderTitle": "Spelargränsinställning", + "UIText_MaxPlayers_StepMessage": "Välj maximalt antal spelare som kan ansluta till servern.", + "UIText_MaxPlayers_PrimaryPlaceholder": "8", + "UIText_MaxPlayers_PrimaryLabel": "Max spelare", + "UIText_MaxPlayers_SubmitButton": "Spara & fortsätt", + "UIText_MaxPlayers_SkipButton": "Hoppa över", + "UIText_ServerPassword_Title": "Stationeers Server UI", + "UIText_ServerPassword_HeaderTitle": "Serverlösenordsinställning", + "UIText_ServerPassword_StepMessage": "Ange ett serverlösenord eller hoppa över detta steg.", + "UIText_ServerPassword_PrimaryPlaceholder": "Serverlösenord", + "UIText_ServerPassword_PrimaryLabel": "Serverlösenord", + "UIText_ServerPassword_SubmitButton": "Spara & fortsätt", + "UIText_ServerPassword_SkipButton": "Hoppa över", + "UIText_GameBranch_Title": "Stationeers Server UI", + "UIText_GameBranch_HeaderTitle": "Spelgreninställning", + "UIText_GameBranch_StepMessage": "Ange en betagren eller hoppa över för att använda standardversionen. Vid byte av gren, starta om SSUI efter guiden.", + "UIText_GameBranch_PrimaryPlaceholder": "beta", + "UIText_GameBranch_PrimaryLabel": "Spelgren", + "UIText_GameBranch_SubmitButton": "Spara & fortsätt", + "UIText_GameBranch_SkipButton": "Använd standardversion", + "UIText_NewTerrainAndSaveSystem_Title": "VÄLJ TERRÄNGSYSTEM", + "UIText_NewTerrainAndSaveSystem_HeaderTitle": "Viktigt steg!", + "UIText_NewTerrainAndSaveSystem_StepMessage": "Bytt till beta? Aktivera terräng- och sparsystem för att stödja det! Ange 'ja' för att aktivera eller 'nej' för att inaktivera.", + "UIText_NewTerrainAndSaveSystem_PrimaryPlaceholder": "yes/no", + "UIText_NewTerrainAndSaveSystem_PrimaryLabel": "Aktivera nytt system", + "UIText_NewTerrainAndSaveSystem_SubmitButton": "Spara & fortsätt", + "UIText_NewTerrainAndSaveSystem_SkipButton": "Hoppa över", + "UIText_DiscordEnabled_Title": "Stationeers Server UI", + "UIText_DiscordEnabled_HeaderTitle": "Discord-integration", + "UIText_DiscordEnabled_StepMessage": "Vill du aktivera Discord-integration? Ange 'ja' för att aktivera eller hoppa över för att inaktivera.", + "UIText_DiscordEnabled_PrimaryPlaceholder": "yes/no", + "UIText_DiscordEnabled_PrimaryLabel": "Aktivera Discord", + "UIText_DiscordEnabled_SubmitButton": "Spara & fortsätt", + "UIText_DiscordEnabled_SkipButton": "Hoppa över (inaktivera Discord)", + "UIText_DiscordToken_Title": "Stationeers Server UI", + "UIText_DiscordToken_HeaderTitle": "Discord-bot-token", + "UIText_DiscordToken_StepMessage": "Ange din Discord-bot-token för serverintegration", + "UIText_DiscordToken_PrimaryPlaceholder": "Discord-bot-token", + "UIText_DiscordToken_PrimaryLabel": "Discord-token", + "UIText_DiscordToken_SubmitButton": "Spara & fortsätt", + "UIText_DiscordToken_SkipButton": "Hoppa över", + "UIText_ControlPanelChannel_Title": "Stationeers Server UI", + "UIText_ControlPanelChannel_HeaderTitle": "Discord-kanalinställning (1/6)", + "UIText_ControlPanelChannel_StepMessage": "Ange Discord-kontrollpanelkanalens ID", + "UIText_ControlPanelChannel_PrimaryPlaceholder": "Kanal-ID", + "UIText_ControlPanelChannel_PrimaryLabel": "Kontrollpanelkanal-ID", + "UIText_ControlPanelChannel_SubmitButton": "Spara & fortsätt", + "UIText_ControlPanelChannel_SkipButton": "Hoppa över", + "UIText_SaveChannel_Title": "Stationeers Server UI", + "UIText_SaveChannel_HeaderTitle": "Discord-kanalinställning (2/6)", + "UIText_SaveChannel_StepMessage": "Ange Discord-sparfilskanalens ID", + "UIText_SaveChannel_PrimaryPlaceholder": "Kanal-ID", + "UIText_SaveChannel_PrimaryLabel": "Sparkanal-ID", + "UIText_SaveChannel_SubmitButton": "Spara & fortsätt", + "UIText_SaveChannel_SkipButton": "Hoppa över", + "UIText_LogChannel_Title": "Stationeers Server UI", + "UIText_LogChannel_HeaderTitle": "Discord-kanalinställning (3/6)", + "UIText_LogChannel_StepMessage": "Ange Discord-loggkanalens ID", + "UIText_LogChannel_PrimaryPlaceholder": "Kanal-ID", + "UIText_LogChannel_PrimaryLabel": "Loggkanal-ID", + "UIText_LogChannel_SubmitButton": "Spara & fortsätt", + "UIText_LogChannel_SkipButton": "Hoppa över", + "UIText_ConnectionListChannel_Title": "Stationeers Server UI", + "UIText_ConnectionListChannel_HeaderTitle": "Discord-kanalinställning (4/6)", + "UIText_ConnectionListChannel_StepMessage": "Ange Discord-anslutningslistkanalens ID", + "UIText_ConnectionListChannel_PrimaryPlaceholder": "Kanal-ID", + "UIText_ConnectionListChannel_PrimaryLabel": "Anslutningslistkanal-ID", + "UIText_ConnectionListChannel_SubmitButton": "Spara & fortsätt", + "UIText_ConnectionListChannel_SkipButton": "Hoppa över", + "UIText_StatusChannel_Title": "Stationeers Server UI", + "UIText_StatusChannel_HeaderTitle": "Discord-kanalinställning (5/6)", + "UIText_StatusChannel_StepMessage": "Ange Discord-statuskanalens ID", + "UIText_StatusChannel_PrimaryPlaceholder": "Kanal-ID", + "UIText_StatusChannel_PrimaryLabel": "Statuskanal-ID", + "UIText_StatusChannel_SubmitButton": "Spara & fortsätt", + "UIText_StatusChannel_SkipButton": "Hoppa över", + "UIText_ControlChannel_Title": "Stationeers Server UI", + "UIText_ControlChannel_HeaderTitle": "Discord-kanalinställning (6/6)", + "UIText_ControlChannel_StepMessage": "Ange Discord-kontrollkanalens ID", + "UIText_ControlChannel_PrimaryPlaceholder": "Kanal-ID", + "UIText_ControlChannel_PrimaryLabel": "Kontrollkanal-ID", + "UIText_ControlChannel_SubmitButton": "Spara & fortsätt", + "UIText_ControlChannel_SkipButton": "Hoppa över", + "UIText_NetworkConfigChoice_Title": "Stationeers Server UI", + "UIText_NetworkConfigChoice_HeaderTitle": "Nätverkskonfiguration", + "UIText_NetworkConfigChoice_StepMessage": "Vill du konfigurera nätverksinställningar? Ange 'ja' för att konfigurera eller hoppa över för att använda standardvärden. Obs: Nätverkskonfiguration är särskilt viktigt på Linux-servrar.", + "UIText_NetworkConfigChoice_PrimaryPlaceholder": "ja", + "UIText_NetworkConfigChoice_PrimaryLabel": "Konfigurera nätverk", + "UIText_NetworkConfigChoice_SubmitButton": "Fortsätt", + "UIText_NetworkConfigChoice_SkipButton": "Hoppa över (använd standardvärden)", + "UIText_GamePort_Title": "Stationeers Server UI", + "UIText_GamePort_HeaderTitle": "Nätverksinställning (1/4)", + "UIText_GamePort_StepMessage": "Ange portnummer för spelanslutningar", + "UIText_GamePort_PrimaryPlaceholder": "27016", + "UIText_GamePort_PrimaryLabel": "Spelport", + "UIText_GamePort_SubmitButton": "Spara & fortsätt", + "UIText_GamePort_SkipButton": "Hoppa över", + "UIText_UpdatePort_Title": "Stationeers Server UI", + "UIText_UpdatePort_HeaderTitle": "Nätverksinställning (2/4)", + "UIText_UpdatePort_StepMessage": "Ange portnummer för uppdateringsanslutningar", + "UIText_UpdatePort_PrimaryPlaceholder": "27015", + "UIText_UpdatePort_PrimaryLabel": "Uppdateringsport", + "UIText_UpdatePort_SubmitButton": "Spara & fortsätt", + "UIText_UpdatePort_SkipButton": "Hoppa över", + "UIText_UPnPEnabled_Title": "Stationeers Server UI", + "UIText_UPnPEnabled_HeaderTitle": "Nätverksinställning (3/4)", + "UIText_UPnPEnabled_StepMessage": "Aktivera UPnP? Ange 'ja' för att aktivera eller 'nej' för att inaktivera.", + "UIText_UPnPEnabled_PrimaryPlaceholder": "yes/no", + "UIText_UPnPEnabled_PrimaryLabel": "Aktivera UPnP", + "UIText_UPnPEnabled_SubmitButton": "Spara & fortsätt", + "UIText_UPnPEnabled_SkipButton": "Hoppa över", + "UIText_LocalIPAddress_Title": "Stationeers Server UI", + "UIText_LocalIPAddress_HeaderTitle": "Nätverksinställning (4/4)", + "UIText_LocalIPAddress_StepMessage": "Ange serverns lokala IP-adress i formatet 0.0.0.0 (ingen CIDR-notation)", + "UIText_LocalIPAddress_PrimaryPlaceholder": "0.0.0.0", + "UIText_LocalIPAddress_PrimaryLabel": "Lokal IP-adress", + "UIText_LocalIPAddress_SubmitButton": "Spara & fortsätt", + "UIText_LocalIPAddress_SkipButton": "Hoppa över", + "UIText_AdminAccount_Title": "Stationeers Server UI", + "UIText_AdminAccount_HeaderTitle": "Admin-kontoinställning", + "UIText_AdminAccount_StepMessage": "Konfigurera ditt admin-konto.", + "UIText_AdminAccount_PrimaryPlaceholder": "Användarnamn", + "UIText_AdminAccount_PrimaryLabel": "Användarnamn", + "UIText_AdminAccount_SecondaryLabel": "Lösenord", + "UIText_AdminAccount_SecondaryPlaceholder": "Lösenord", + "UIText_AdminAccount_SubmitButton": "Spara & fortsätt", + "UIText_AdminAccount_SkipButton": "Hoppa över autentisering", + "UIText_SSCM_Title": "Stationeers Server Command Manager", + "UIText_SSCM_HeaderTitle": "Unik funktion", + "UIText_SSCM_StepMessage": "SSCM är ett anpassat serverplugin som låter dig köra serverkommandon direkt från SSUI. Det ger dig möjlighet att köra kommandon från webbkonsolen utan att störa vanliga funktioner, inga klientsidemoddar behövs.", + "UIText_SSCM_PrimaryPlaceholder": "skriv 'nej' för att inaktivera", + "UIText_SSCM_PrimaryLabel": "Att välja bort rekommenderas INTE.", + "UIText_SSCM_SubmitButton": "Fortsätt", + "UIText_SSCM_SkipButton": "Behåll aktiverad", + "UIText_Finalize_Title": "Slutför konfiguration", + "UIText_Finalize_StepMessage": "Redo att slutföra? Din konfiguration har redan sparats under guiden. Om du vill ändra inställningar kan du klicka på Gå tillbaka till start och hoppa över det du vill behålla. De flesta inställningar kan också ändras i konfigurationsfliken i gränssnittet.", + "UIText_Finalize_SubmitButton": "Gå tillbaka till start", + "UIText_Finalize_SkipButton": "Hoppa över autentisering", + "UIText_Login_Title": "Stationeers Server UI", + "UIText_Login_PrimaryLabel": "Användarnamn", + "UIText_Login_SecondaryLabel": "Lösenord", + "UIText_Login_PrimaryPlaceholder": "Ange användarnamn", + "UIText_Login_SecondaryPlaceholder": "Ange lösenord", + "UIText_Login_SubmitButton": "Logga in", + "UIText_ChangeUser_Title": "Stationeers Server UI", + "UIText_ChangeUser_HeaderTitle": "Hantera användare", + "UIText_ChangeUser_PrimaryLabel": "Användarnamn att lägga till/uppdatera", + "UIText_ChangeUser_SecondaryLabel": "Nytt lösenord", + "UIText_ChangeUser_SecondaryPlaceholder": "Lösenord", + "UIText_ChangeUser_SubmitButton": "Lägg till/uppdatera användare" + } + }, + "BackendText": { + "top1": {}, + "nest1": { + "nestINnest1": {}, + "nestINnest2": {} + } + } +} \ No newline at end of file diff --git a/UIMod/onboard_bundled/scripts/autostart.ps1 b/UIMod/onboard_bundled/scripts/autostart.ps1 new file mode 100644 index 00000000..e7e67420 --- /dev/null +++ b/UIMod/onboard_bundled/scripts/autostart.ps1 @@ -0,0 +1,51 @@ +# Path to this script file +$scriptPath = $MyInvocation.MyCommand.Path + +# Path to user's startup folder +$startupFolder = [Environment]::GetFolderPath("Startup") + +# Shortcut name in startup +$shortcutName = "Start-StationeersServerUI.lnk" +$shortcutPath = Join-Path $startupFolder $shortcutName + +# Function to create shortcut +function New-Shortcut { + param ( + [string]$targetPath, + [string]$shortcutPath + ) + + $shell = New-Object -ComObject WScript.Shell + $shortcut = $shell.CreateShortcut($shortcutPath) + # Set the target to powershell.exe and pass the script as an argument + $shortcut.TargetPath = "powershell.exe" + $shortcut.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$targetPath`"" + $shortcut.WorkingDirectory = Split-Path $targetPath + $shortcut.Save() +} + +# Check if shortcut exists in startup folder +if (-not (Test-Path $shortcutPath)) { + Write-Output "Shortcut not found in Startup folder. Creating shortcut to enable autostart..." + New-Shortcut -targetPath $scriptPath -shortcutPath $shortcutPath + Write-Output "Shortcut created. You may need to restart your session to apply autostart." + Read-Host "Press Enter to exit" + exit +} + +# Folder where the executables are located (folder of this script) +$exeFolder = Split-Path -Parent $scriptPath + +# Find latest StationeersServerControl*.exe by last write time as old executables are prefixed _old anyway +$latestExe = Get-ChildItem -Path $exeFolder -Filter "StationeersServerControl*.exe" | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 + +if ($null -eq $latestExe) { + Write-Error "No executable found to start in $exeFolder" + Read-Host "Press Enter to exit" + exit 1 +} + +Write-Output "Starting $($latestExe.FullName)..." +Start-Process -FilePath $latestExe.FullName \ No newline at end of file diff --git a/UIMod/onboard_bundled/scripts/autostart.sh b/UIMod/onboard_bundled/scripts/autostart.sh new file mode 100644 index 00000000..b3d220b4 --- /dev/null +++ b/UIMod/onboard_bundled/scripts/autostart.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +# not yet implemented \ No newline at end of file diff --git a/UIMod/twoboxform/twoboxform.css b/UIMod/onboard_bundled/twoboxform/twoboxform.css similarity index 100% rename from UIMod/twoboxform/twoboxform.css rename to UIMod/onboard_bundled/twoboxform/twoboxform.css diff --git a/UIMod/twoboxform/twoboxform.html b/UIMod/onboard_bundled/twoboxform/twoboxform.html similarity index 100% rename from UIMod/twoboxform/twoboxform.html rename to UIMod/onboard_bundled/twoboxform/twoboxform.html diff --git a/UIMod/twoboxform/twoboxform.js b/UIMod/onboard_bundled/twoboxform/twoboxform.js similarity index 98% rename from UIMod/twoboxform/twoboxform.js rename to UIMod/onboard_bundled/twoboxform/twoboxform.js index f4fc50e9..1de9baa1 100644 --- a/UIMod/twoboxform/twoboxform.js +++ b/UIMod/onboard_bundled/twoboxform/twoboxform.js @@ -85,7 +85,7 @@ document.addEventListener('DOMContentLoaded', () => { const configField = document.getElementById('config-field').value; let nextStep = document.getElementById('next-step').value; - if (step === "welcome") { + if (step === "welcome" || step === "pls_read") { window.location.href = `/setup?step=${nextStep}`; return; } @@ -131,7 +131,7 @@ document.addEventListener('DOMContentLoaded', () => { // Handle boolean conversion for yes/no fields if (configField === "IsDiscordEnabled" || configField === "UPNPEnabled" || - configField === "ServerVisible" || configField === "UseSteamP2P" || configField === "IsSSCMEnabled") { + configField === "ServerVisible" || configField === "UseSteamP2P" || configField === "IsSSCMEnabled" || configField === "IsNewTerrainAndSaveSystem") { body = JSON.stringify({ [configField]: booleanToConfig(document.getElementById('primary-field').value) }); diff --git a/UIMod/ui/config.html b/UIMod/onboard_bundled/ui/config.html similarity index 51% rename from UIMod/ui/config.html rename to UIMod/onboard_bundled/ui/config.html index 8f350593..66726b04 100644 --- a/UIMod/ui/config.html +++ b/UIMod/onboard_bundled/ui/config.html @@ -4,7 +4,7 @@ - Server Configuration + {{.UIText_ServerConfig}} @@ -21,18 +21,18 @@
-

Server Configuration

+

{{.UIText_ServerConfig}}

@@ -42,183 +42,234 @@

Server Configuration

- - - + + + +
-

Please select a configuration section above

-

Alternatively, use the Configuration Wizard to configure the server.

+

{{.UIText_PleaseSelectSection}}

+

{{.UIText_UseWizardAlternative}}

-

Basic Server Settings

+

{{.UIText_BasicServerSettings}}

- - -
Name displayed in server list
+ + +
{{.UIText_ServerNameInfo}}
- - {{.UIText_SaveFileName}}: + -
Name of save folder. Must be capitalized. To create a new world, - provide the - World type to generate. (MyMoonMap Moon)
+
{{.UIText_SaveFileNameInfo}}
- +
- + -
Maximum number of players allowed
+ value="{{.ServerMaxPlayers}}" pattern="^\S*$" required> +
{{.UIText_MaxPlayersInfo}}
- + -
Leave empty for no password
+ value="{{.ServerPassword}}" pattern="^\S*$"> +
{{.UIText_ServerPasswordInfo}}
-
- - -
Server Admin Password
+ +
+ + +
{{.UIText_ServerAuthSecretInfo}}
- + -
Set to TRUE to enable automatic saving
+
{{.UIText_AutoSaveInfo}}
- - {{.UIText_SaveInterval}}: + -
Time in seconds between saves
+
{{.UIText_SaveIntervalInfo}}
- + -
Automatically pause server when no players are connected -
+
{{.UIText_AutoPauseServerInfo}}
-

Network Configuration

+

{{.UIText_NetworkConfiguration}}

- - {{.UIText_GamePort}}: + -
Default: 27016
+
{{.UIText_GamePortInfo}}
- - {{.UIText_UpdatePort}}: + -
Default: 27015
+
{{.UIText_UpdatePortInfo}}
- + -
Enable automatic UPNP port forwarding
+
{{.UIText_UPNPEnabledInfo}}
- + -
IP address to bind to
+ value="{{.LocalIpAddress}}" pattern="^\S*$" required> +
{{.UIText_LocalIpAddressInfo}}
- + -
Set to TRUE to listen only on local network
+
{{.UIText_StartLocalHostInfo}}
- + -
Set to TRUE to list server publicly
+
{{.UIText_ServerVisibleInfo}}
- + -
Enable Steam Peer-to-Peer networking
+
{{.UIText_UseSteamP2PInfo}}
-

Advanced Configuration

+

{{.UIText_AdvancedConfiguration}}

-
- - -
Authentication secret for the server (optional)
+ +
+ + +
{{.UIText_AdminPasswordInfo}}
- - -
System path to server executable
-
Not editable from the UI for security reasons, but you can - edit it - manually in the config.json file.
+ + +
{{.UIText_ServerExePathInfo}}
+
{{.UIText_ServerExePathInfo2}}
- + -
Format: CustomParam1 Value1 CustomParam2 Value2
+ value="{{.AdditionalParams}}"> +
{{.UIText_AdditionalParamsInfo}}
- + -
Timeframe in minutes to schedule an automatic gameserver restart. 0 = disabled, 1440 = 24 hours, etc.. If SSCM is enabled, you will see "Attention, server is restarting in 30/20/10/5 seconds!" messages ingame before restarting.
+ value="{{.AutoRestartServerTimer}}"> +
{{.UIText_AutoRestartServerTimerInfo}}
+
+ +
+ + +
{{.UIText_GameBranchInfo}}
+
+ +
+ + +
{{.UIText_AutoStartServerOnStartupInfo}}
+
+ +
+
+ +
+

{{.UIText_BetaOnlySettings}}

+
{{.UIText_BetaWarning}}
+
+
+ + +
{{.UIText_UseNewTerrainAndSaveInfo}}
+
+ +
+ + +
{{.UIText_DifficultyInfo}}
+
+ +
+ + +
{{.UIText_StartConditionInfo}}
+
+ +
+ + +
{{.UIText_StartLocationInfo}}
@@ -236,74 +287,74 @@

Advanced Configuration

- +
RECOMMENDED

- - -
Your Discord bot's authentication token
+ + +
{{.UIText_DiscordBotTokenInfo}}
-

Channel Configuration

+

{{.UIText_ChannelConfiguration}}

- + -
Channel for admin commands
+ value="{{.ControlChannelID}}"> +
{{.UIText_AdminCommandChannelInfo}}
- + -
Channel for control panel
+ value="{{.ControlPanelChannelID}}"> +
{{.UIText_ControlPanelChannelInfo}}
- - -
Server status updates
+ + +
{{.UIText_StatusChannelInfo}}
- - -
Player connection tracking
+ + +
{{.UIText_ConnectionListChannelInfo}}
- - -
Server log output
+ + +
{{.UIText_LogChannelInfo}}
- - -
Save file information
+ + +
{{.UIText_SaveInfoChannelInfo}}
- - -
Server error messages
+ + +
{{.UIText_ErrorChannelInfo}}
- + -
File path to banned players list
+ value="{{.BlackListFilePath}}"> +
{{.UIText_BannedPlayersListPathInfo}}
@@ -313,22 +364,22 @@

Channel Configuration

-

Discord Integration Benefits

+

{{.UIText_DiscordIntegrationBenefits}}

    -
  • Monitor server status in real-time
  • -
  • Manage restarts and restores remotely
  • -
  • Track player connections
  • -
  • Community management options
  • -
  • Real-time error notifications
  • +
  • {{.UIText_DiscordBenefit1}}
  • +
  • {{.UIText_DiscordBenefit2}}
  • +
  • {{.UIText_DiscordBenefit3}}
  • +
  • {{.UIText_DiscordBenefit4}}
  • +
  • {{.UIText_DiscordBenefit5}}
-

For setup instructions, visit the {{.UIText_DiscordSetupInstructions}} GitHub repository

+ diff --git a/go.mod b/go.mod index 0085bd67..9e92a894 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.2 require ( github.com/bwmarrin/discordgo v0.28.1 - github.com/fsnotify/fsnotify v1.9.0 + github.com/fsnotify/fsnotify v1.7.0 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 golang.org/x/crypto v0.37.0 diff --git a/go.sum b/go.sum index a5090fb4..2ed19ffb 100644 --- a/go.sum +++ b/go.sum @@ -2,25 +2,18 @@ github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/server.go b/server.go index b1ab7dab..613e66f3 100644 --- a/server.go +++ b/server.go @@ -21,14 +21,19 @@ package main import ( + "embed" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/loader" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/cli" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/web" ) +//go:embed UIMod/onboard_bundled +var v1uiFS embed.FS + func main() { var wg sync.WaitGroup logger.Main.Install("Starting setup...") @@ -41,8 +46,13 @@ func main() { wg.Wait() // Load config,discordbot, backupmgr and detectionmgr using the loader package - loader.ReloadAll() + loader.InitVirtFS(v1uiFS) + loader.InitBackend() loader.InitDetector() + loader.AfterStartComplete() + + cli.StartConsole(&wg) + web.StartWebServer(&wg) } diff --git a/src/backupmgr/restore.go b/src/backupmgr/restore.go deleted file mode 100644 index 089bea2f..00000000 --- a/src/backupmgr/restore.go +++ /dev/null @@ -1,57 +0,0 @@ -package backupmgr - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" -) - -// RestoreBackup restores a backup with the given index -func (m *BackupManager) RestoreBackup(index int) error { - m.mu.Lock() - defer m.mu.Unlock() - logger.Backup.Info("Restoring backup with index " + fmt.Sprintf("%d", index)) - - files := []struct { - backupName string - backupNameAlt string - destName string - }{ - {fmt.Sprintf("world_meta(%d).xml", index), fmt.Sprintf("world_meta(%d)_AutoSave.xml", index), "world_meta.xml"}, - {fmt.Sprintf("world(%d).xml", index), fmt.Sprintf("world(%d)_AutoSave.xml", index), "world.xml"}, - {fmt.Sprintf("world(%d).bin", index), fmt.Sprintf("world(%d)_AutoSave.bin", index), "world.bin"}, - } - - restoredFiles := make(map[string]string) - - for _, file := range files { - backupFile := filepath.Join(m.config.SafeBackupDir, file.backupName) - destFile := filepath.Join("./saves/"+config.WorldName, file.destName) - - if err := copyFile(backupFile, destFile); err != nil { - // Try alternative name - backupFileAlt := filepath.Join(m.config.SafeBackupDir, file.backupNameAlt) - if err := copyFile(backupFileAlt, destFile); err != nil { - m.revertRestore(restoredFiles) - return fmt.Errorf("failed to restore %s: %w", file.backupName, err) - } - backupFile = backupFileAlt - } - restoredFiles[destFile] = backupFile - } - logger.Backup.Debug(fmt.Sprintf("%v", restoredFiles)) - - return nil -} - -// revertRestore undoes a failed restore operation -func (m *BackupManager) revertRestore(restoredFiles map[string]string) { - for destFile, backupFile := range restoredFiles { - if err := os.Remove(destFile); err == nil { - _ = copyFile(backupFile, destFile) - } - } -} diff --git a/src/backupmgr/utils.go b/src/backupmgr/utils.go deleted file mode 100644 index 489b3c34..00000000 --- a/src/backupmgr/utils.go +++ /dev/null @@ -1,52 +0,0 @@ -package backupmgr - -import ( - "io" - "os" - "regexp" - "strconv" - "strings" -) - -// copyFile copies a file from src to dst -func copyFile(src, dst string) error { - source, err := os.Open(src) - if err != nil { - return err - } - defer source.Close() - - destination, err := os.Create(dst) - if err != nil { - return err - } - defer destination.Close() - - if _, err := io.Copy(destination, source); err != nil { - return err - } - - return destination.Sync() -} - -// parseBackupIndex extracts the backup index from a filename -func parseBackupIndex(filename string) int { - re := regexp.MustCompile(`\((\d+)\)`) - matches := re.FindStringSubmatch(filename) - if len(matches) < 2 { - return -1 - } - - index, err := strconv.Atoi(matches[1]) - if err != nil { - return -1 - } - - return index -} - -func isValidBackupFile(filename string) bool { - return strings.Contains(filename, "world") && - (strings.HasSuffix(filename, ".bin") || - strings.HasSuffix(filename, ".xml")) -} diff --git a/src/cli/runtimecommands.go b/src/cli/runtimecommands.go new file mode 100644 index 00000000..d501127d --- /dev/null +++ b/src/cli/runtimecommands.go @@ -0,0 +1,197 @@ +// Package misc provides a non-blocking command-line interface for entering commands +// while allowing the application to continue its operations normally. +package cli + +import ( + "bufio" + "errors" + "fmt" + "os" + "sort" + "strings" + "sync" + "time" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" +) + +// ANSI escape codes for green text and reset +const ( + cliPrompt = "\033[32m" + "SSUICLI" + " » " + "\033[0m" +) + +// CommandFunc defines the signature for command handler functions. +type CommandFunc func(args []string) error + +// commandRegistry holds the map of command names to their handler functions. +var commandRegistry = make(map[string]CommandFunc) +var mu sync.Mutex + +var commandAliases = make(map[string][]string) + +// RegisterCommand adds a new command and its handler to the registry. +func RegisterCommand(name string, handler CommandFunc, aliases ...string) { + mu.Lock() + defer mu.Unlock() + commandRegistry[name] = handler + if len(aliases) > 0 { + commandAliases[name] = append(commandAliases[name], aliases...) + for _, alias := range aliases { + commandRegistry[alias] = handler + } + } +} + +// StartConsole starts a non-blocking console input loop in a separate goroutine. +func StartConsole(wg *sync.WaitGroup) { + if !config.IsConsoleEnabled { + logger.Core.Info("SSUICLI runtime console is disabled in config, skipping...") + return + } + wg.Add(1) + go func() { + defer wg.Done() + scanner := bufio.NewScanner(os.Stdin) + logger.Core.Info("SSUICLI runtime console started. Type 'help' for commands.") + time.Sleep(10 * time.Millisecond) + + for { + fmt.Print(cliPrompt) + os.Stdout.Sync() // Force flush the output buffer + if !scanner.Scan() { + break + } + input := strings.TrimSpace(scanner.Text()) + if input == "" { + continue + } + ProcessCommand(input) + } + + if err := scanner.Err(); err != nil { + logger.Core.Error("SSUICLI input error:" + err.Error()) + } + logger.Core.Info("SSUICLI runtime console stopped.") + }() +} + +// ProcessCommand parses and executes a command from the input string. +func ProcessCommand(input string) { + args := strings.Fields(input) + if len(args) == 0 { + return + } + + commandName := strings.ToLower(args[0]) + args = args[1:] // Remove command name from args + + mu.Lock() + handler, exists := commandRegistry[commandName] + mu.Unlock() + + if !exists { + logger.Core.Error("Unknown command:" + commandName + ". Type 'help' for available commands.") + return + } + + if err := handler(args); err != nil { + logger.Core.Error("Command " + commandName + " failed:" + err.Error()) + } +} + +// WrapNoReturn wraps a function with no return value to match CommandFunc. +func WrapNoReturn(fn func()) CommandFunc { + return func(args []string) error { + if len(args) > 0 { + return errors.New("command does not accept arguments") + } + fn() + logger.Core.Info("Runtime CLI Command executed successfully") + return nil + } +} + +// helpCommand displays available commands along with their aliases. +func helpCommand(args []string) error { + mu.Lock() + defer mu.Unlock() + logger.Core.Info("Available commands:") + // Collect primary commands (those in commandAliases keys) + primaryCommands := make([]string, 0, len(commandAliases)) + for cmd := range commandAliases { + primaryCommands = append(primaryCommands, cmd) + } + sort.Strings(primaryCommands) + for _, cmd := range primaryCommands { + aliases := commandAliases[cmd] + if len(aliases) > 0 { + logger.Core.Info("- " + cmd + " (aliases: " + strings.Join(aliases, ", ") + ")") + } else { + logger.Core.Info("- %s" + cmd) + } + } + return nil +} + +// init registers default cli commands and their aliases. +func init() { + RegisterCommand("help", helpCommand, "h") + 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") +} + +func startServer() { + err := gamemgr.InternalStartServer() + if err != nil { + logger.Core.Error("Error starting server:" + err.Error()) + } +} +func stopServer() { + err := gamemgr.InternalStopServer() + if err != nil { + logger.Core.Error("Error stopping server:" + err.Error()) + } +} + +func exitfromcli() { + // send signal to the main process to exit + logger.Core.Info("I have to go...") + os.Exit(0) +} + +func deleteConfig() { + //remove file at config.ConfigPath + if err := os.Remove(config.ConfigPath); err != nil { + logger.Core.Error("Error deleting config file: " + err.Error()) + return + } + logger.Core.Info("Config file deleted successfully") +} + +func runSteamCMD() { + if gamemgr.InternalIsServerRunning() { + logger.Core.Warn("Server is running, stopping server first...") + gamemgr.InternalStopServer() + time.Sleep(10000 * time.Millisecond) + } + logger.Core.Info("Running SteamCMD") + setup.InstallAndRunSteamCMD() +} + +func testLocalization() { + currentLanguageSetting := config.LanguageSetting + s := localization.GetString("UIText_StartButton") + logger.Core.Info(s + " (current language: " + currentLanguageSetting + ")") +} diff --git a/src/cli/terminalmsg.go b/src/cli/terminalmsg.go new file mode 100644 index 00000000..9c89873d --- /dev/null +++ b/src/cli/terminalmsg.go @@ -0,0 +1,58 @@ +package cli + +import ( + "fmt" + "runtime" + "time" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" +) + +// PrintStartupMessage prints a stylish startup message to the terminal +func PrintStartupMessage() { + // Clear some space + fmt.Println() + fmt.Println() + + // Main ASCII art logo + fmt.Println(" ███████╗████████╗ █████╗ ████████╗██╗ ██████╗ ███╗ ██╗███████╗███████╗██████╗ ███████╗ ███████╗██╗ ██╗██╗") + fmt.Println(" ██╔════╝╚══██╔══╝██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║██╔════╝██╔════╝██╔══██╗██╔════╝ ██╔════╝██║ ██║██║") + fmt.Println(" ███████╗ ██║ ███████║ ██║ ██║██║ ██║██╔██╗ ██║█████╗ █████╗ ██████╔╝███████╗█████╗███████╗██║ ██║██║") + fmt.Println(" ╚════██║ ██║ ██╔══██║ ██║ ██║██║ ██║██║╚██╗██║██╔══╝ ██╔══╝ ██╔══██╗╚════██║╚════╝╚════██║██║ ██║██║") + fmt.Println(" ███████║ ██║ ██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║███████╗███████╗██║ ██║███████║ ███████║╚██████╔╝██║") + fmt.Println(" ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚══════╝╚═╝ ╚═╝╚══════╝ ╚══════╝ ╚═════╝ ╚═╝") + + // Decorative line + fmt.Println(" ╔═══════════════════════════════════════════════════════════════════════════════════════════════════╗") + // Tagline + fmt.Println(" ║ 🎮 YOUR ONE-STOP SHOP FOR RUNNING A STATIONEERS SERVER 🎮 ║") + // System info + fmt.Printf(" ║ 🚀 Version: %s 📅 %s 💻 Runtime: %.3s/%s ║\n", + config.Version, + time.Now().Format("2006-01-02 15:04:05"), + runtime.GOOS, + runtime.GOARCH) + // Decorative line + fmt.Println(" ╚═══════════════════════════════════════════════════════════════════════════════════════════════════╝") + + // Web UI info + fmt.Println("\n 🌐 Web UI available at: https://localhost:8443 (default) or https://:8443") + fmt.Println("\n 🌐 Support available at: https://discord.gg/8n3vN92MyJ") + + // Quote + fmt.Println("\n JacksonTheMaster: \"Managing game servers shouldn't be rocket science... unless it's a rocket game!\"") +} + +func PrintFirstTimeSetupMessage() { + // Setup guide + fmt.Println(" 📋 GETTING STARTED:") + fmt.Println(" ┌─────────────────────────────────────────────────────────────────────────────────────────────┐") + fmt.Println(" │ • Ready, set, go! Welcome to StationeersServerUI, new User! │") + fmt.Println(" │ • The good news: you made it here, which means you are likely ready to run your server! │") + fmt.Println(" │ • If this is your first time here, no worries: SSUI is made to be easy to use. │") + fmt.Println(" │ • Configure your server by visiting the WebUI! │") + fmt.Println(" │ • Support is provided at https://discord.gg/8n3vN92MyJ │") + fmt.Println(" │ • For more details, check the GitHub Wiki: │") + fmt.Println(" │ • https://github.com/JacksonTheMaster/StationeersServerUI/v5/wiki │") + fmt.Println(" └─────────────────────────────────────────────────────────────────────────────────────────────┘") +} diff --git a/src/config/config.go b/src/config/config.go index 4a7bc5d4..f7c8aee1 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -11,61 +11,69 @@ import ( var ( // All configuration variables can be found in vars.go - Version = "5.4.34" - Branch = "release" + Version = "5.5.8" + Branch = "indev-no-steamcmd" ) type JsonConfig struct { - DiscordToken string `json:"discordToken"` - ControlChannelID string `json:"controlChannelID"` - StatusChannelID string `json:"statusChannelID"` - ConnectionListChannelID string `json:"connectionListChannelID"` - LogChannelID string `json:"logChannelID"` - SaveChannelID string `json:"saveChannelID"` - ControlPanelChannelID string `json:"controlPanelChannelID"` - DiscordCharBufferSize int `json:"DiscordCharBufferSize"` - BlackListFilePath string `json:"blackListFilePath"` - IsDiscordEnabled *bool `json:"isDiscordEnabled"` - ErrorChannelID string `json:"errorChannelID"` - BackupKeepLastN int `json:"backupKeepLastN"` - IsCleanupEnabled *bool `json:"isCleanupEnabled"` - BackupKeepDailyFor int `json:"backupKeepDailyFor"` - BackupKeepWeeklyFor int `json:"backupKeepWeeklyFor"` - BackupKeepMonthlyFor int `json:"backupKeepMonthlyFor"` - BackupCleanupInterval int `json:"backupCleanupInterval"` - BackupWaitTime int `json:"backupWaitTime"` - GameBranch string `json:"gameBranch"` - ServerName string `json:"ServerName"` - SaveInfo string `json:"SaveInfo"` - ServerMaxPlayers string `json:"ServerMaxPlayers"` - ServerPassword string `json:"ServerPassword"` - ServerAuthSecret string `json:"ServerAuthSecret"` - AdminPassword string `json:"AdminPassword"` - GamePort string `json:"GamePort"` - UpdatePort string `json:"UpdatePort"` - UPNPEnabled *bool `json:"UPNPEnabled"` - AutoSave *bool `json:"AutoSave"` - SaveInterval string `json:"SaveInterval"` - AutoPauseServer *bool `json:"AutoPauseServer"` - LocalIpAddress string `json:"LocalIpAddress"` - StartLocalHost *bool `json:"StartLocalHost"` - ServerVisible *bool `json:"ServerVisible"` - UseSteamP2P *bool `json:"UseSteamP2P"` - ExePath string `json:"ExePath"` - AdditionalParams string `json:"AdditionalParams"` - Users map[string]string `json:"users"` // Map of username to hashed password - AuthEnabled *bool `json:"authEnabled"` // Toggle for enabling/disabling auth - JwtKey string `json:"JwtKey"` - AuthTokenLifetime int `json:"AuthTokenLifetime"` - Debug *bool `json:"Debug"` - CreateSSUILogFile *bool `json:"CreateSSUILogFile"` - LogLevel int `json:"LogLevel"` - SubsystemFilters []string `json:"subsystemFilters"` - IsUpdateEnabled *bool `json:"IsUpdateEnabled"` - IsSSCMEnabled *bool `json:"IsSSCMEnabled"` - AutoRestartServerTimer string `json:"AutoRestartServerTimer"` - AllowPrereleaseUpdates *bool `json:"AllowPrereleaseUpdates"` - AllowMajorUpdates *bool `json:"AllowMajorUpdates"` + DiscordToken string `json:"discordToken"` + ControlChannelID string `json:"controlChannelID"` + StatusChannelID string `json:"statusChannelID"` + ConnectionListChannelID string `json:"connectionListChannelID"` + LogChannelID string `json:"logChannelID"` + SaveChannelID string `json:"saveChannelID"` + ControlPanelChannelID string `json:"controlPanelChannelID"` + DiscordCharBufferSize int `json:"DiscordCharBufferSize"` + BlackListFilePath string `json:"blackListFilePath"` + IsDiscordEnabled *bool `json:"isDiscordEnabled"` + ErrorChannelID string `json:"errorChannelID"` + BackupKeepLastN int `json:"backupKeepLastN"` + IsCleanupEnabled *bool `json:"isCleanupEnabled"` + BackupKeepDailyFor int `json:"backupKeepDailyFor"` + BackupKeepWeeklyFor int `json:"backupKeepWeeklyFor"` + BackupKeepMonthlyFor int `json:"backupKeepMonthlyFor"` + BackupCleanupInterval int `json:"backupCleanupInterval"` + BackupWaitTime int `json:"backupWaitTime"` + IsNewTerrainAndSaveSystem *bool `json:"IsNewTerrainAndSaveSystem"` + GameBranch string `json:"gameBranch"` + Difficulty string `json:"Difficulty"` + StartCondition string `json:"StartCondition"` + StartLocation string `json:"StartLocation"` + ServerName string `json:"ServerName"` + SaveInfo string `json:"SaveInfo"` + ServerMaxPlayers string `json:"ServerMaxPlayers"` + ServerPassword string `json:"ServerPassword"` + ServerAuthSecret string `json:"ServerAuthSecret"` + AdminPassword string `json:"AdminPassword"` + GamePort string `json:"GamePort"` + UpdatePort string `json:"UpdatePort"` + UPNPEnabled *bool `json:"UPNPEnabled"` + AutoSave *bool `json:"AutoSave"` + SaveInterval string `json:"SaveInterval"` + AutoPauseServer *bool `json:"AutoPauseServer"` + LocalIpAddress string `json:"LocalIpAddress"` + StartLocalHost *bool `json:"StartLocalHost"` + ServerVisible *bool `json:"ServerVisible"` + UseSteamP2P *bool `json:"UseSteamP2P"` + ExePath string `json:"ExePath"` + AdditionalParams string `json:"AdditionalParams"` + Users map[string]string `json:"users"` // Map of username to hashed password + AuthEnabled *bool `json:"authEnabled"` // Toggle for enabling/disabling auth + JwtKey string `json:"JwtKey"` + AuthTokenLifetime int `json:"AuthTokenLifetime"` + Debug *bool `json:"Debug"` + CreateSSUILogFile *bool `json:"CreateSSUILogFile"` + LogLevel int `json:"LogLevel"` + LogClutterToConsole *bool `json:"LogClutterToConsole"` + SubsystemFilters []string `json:"subsystemFilters"` + IsUpdateEnabled *bool `json:"IsUpdateEnabled"` + IsSSCMEnabled *bool `json:"IsSSCMEnabled"` + AutoRestartServerTimer string `json:"AutoRestartServerTimer"` + AllowPrereleaseUpdates *bool `json:"AllowPrereleaseUpdates"` + AllowMajorUpdates *bool `json:"AllowMajorUpdates"` + IsConsoleEnabled *bool `json:"IsConsoleEnabled"` + LanguageSetting string `json:"LanguageSetting"` + AutoStartServerOnStartup *bool `json:"AutoStartServerOnStartup"` } type CustomDetection struct { @@ -78,6 +86,8 @@ type CustomDetection struct { // LoadConfig loads and initializes the configuration func LoadConfig() (*JsonConfig, error) { + ConfigMu.Lock() + var jsonConfig JsonConfig file, err := os.Open(ConfigPath) if err == nil { @@ -88,13 +98,14 @@ func LoadConfig() (*JsonConfig, error) { return nil, fmt.Errorf("failed to decode config: %v", err) } } else if os.IsNotExist(err) { - // File is missing, log it and proceed with defaults - fmt.Println("Config file does not exist. Using defaults and environment variables.") + // File is missing, log it and proceed with defaults (probably first time setup) + fmt.Println("config file was not found, proceeding with defaults.") } else { // Other errors (e.g., permissions), fail immediately return nil, fmt.Errorf("failed to open config file: %v", err) } - // Apply configuration with hierarchy + ConfigMu.Unlock() + // Apply configuration applyConfig(&jsonConfig) return &jsonConfig, nil @@ -131,15 +142,24 @@ func applyConfig(cfg *JsonConfig) { BackupKeepMonthlyFor = time.Duration(getInt(cfg.BackupKeepMonthlyFor, "BACKUP_KEEP_MONTHLY_FOR", 730)) * time.Hour BackupCleanupInterval = time.Duration(getInt(cfg.BackupCleanupInterval, "BACKUP_CLEANUP_INTERVAL", 730)) * time.Hour BackupWaitTime = time.Duration(getInt(cfg.BackupWaitTime, "BACKUP_WAIT_TIME", 30)) * time.Second + + isNewTerrainAndSaveSystemVal := getBool(cfg.IsNewTerrainAndSaveSystem, "ENABLE_DOT_SAVES", false) + IsNewTerrainAndSaveSystem = isNewTerrainAndSaveSystemVal + cfg.IsNewTerrainAndSaveSystem = &isNewTerrainAndSaveSystemVal + GameBranch = getString(cfg.GameBranch, "GAME_BRANCH", "public") + Difficulty = getString(cfg.Difficulty, "DIFFICULTY", "") + StartCondition = getString(cfg.StartCondition, "START_CONDITION", "") + StartLocation = getString(cfg.StartLocation, "START_LOCATION", "") ServerName = getString(cfg.ServerName, "SERVER_NAME", "Stationeers Server UI") - SaveInfo = getString(cfg.SaveInfo, "SAVE_INFO", "Moon Moon") + SaveInfo = getString(cfg.SaveInfo, "SAVE_INFO", "Vulcan Vulcan") ServerMaxPlayers = getString(cfg.ServerMaxPlayers, "SERVER_MAX_PLAYERS", "6") ServerPassword = getString(cfg.ServerPassword, "SERVER_PASSWORD", "") ServerAuthSecret = getString(cfg.ServerAuthSecret, "SERVER_AUTH_SECRET", "") AdminPassword = getString(cfg.AdminPassword, "ADMIN_PASSWORD", "") GamePort = getString(cfg.GamePort, "GAME_PORT", "27016") UpdatePort = getString(cfg.UpdatePort, "UPDATE_PORT", "27015") + LanguageSetting = getString(cfg.LanguageSetting, "LANGUAGE_SETTING", "en-US") upnpEnabledVal := getBool(cfg.UPNPEnabled, "UPNP_ENABLED", false) UPNPEnabled = upnpEnabledVal @@ -204,10 +224,22 @@ func applyConfig(cfg *JsonConfig) { SubsystemFilters = getStringSlice(cfg.SubsystemFilters, "SUBSYSTEM_FILTERS", []string{}) AutoRestartServerTimer = getString(cfg.AutoRestartServerTimer, "AUTO_RESTART_SERVER_TIMER", "0") - isSSCMEnabledVal := getBool(cfg.IsSSCMEnabled, "IS_SSCM_ENABLED", false) + isSSCMEnabledVal := getBool(cfg.IsSSCMEnabled, "IS_SSCM_ENABLED", true) IsSSCMEnabled = isSSCMEnabledVal cfg.IsSSCMEnabled = &isSSCMEnabledVal + isConsoleEnabledVal := getBool(cfg.IsConsoleEnabled, "IS_CONSOLE_ENABLED", true) + IsConsoleEnabled = isConsoleEnabledVal + cfg.IsConsoleEnabled = &isConsoleEnabledVal + + logClutterToConsoleVal := getBool(cfg.LogClutterToConsole, "LOG_CLUTTER_TO_CONSOLE", false) + LogClutterToConsole = logClutterToConsoleVal + cfg.LogClutterToConsole = &logClutterToConsoleVal + + autoStartServerOnStartupVal := getBool(cfg.AutoStartServerOnStartup, "AUTO_START_SERVER_ON_STARTUP", false) + AutoStartServerOnStartup = autoStartServerOnStartupVal + cfg.AutoStartServerOnStartup = &autoStartServerOnStartupVal + // Process SaveInfo parts := strings.Split(SaveInfo, " ") if len(parts) > 0 { @@ -217,7 +249,31 @@ func applyConfig(cfg *JsonConfig) { BackupWorldName = parts[1] } - // Set backup paths - ConfiguredBackupDir = filepath.Join("./saves/", WorldName, "Backup") + // Set backup paths for old or new style saves + if IsNewTerrainAndSaveSystem { + // use new new style autosave folder + ConfiguredBackupDir = filepath.Join("./saves/", WorldName, "autosave") + } else { + // use old style Backups folder + ConfiguredBackupDir = filepath.Join("./saves/", WorldName, "Backup") + } + // use Safebackups folder either way. ConfiguredSafeBackupDir = filepath.Join("./saves/", WorldName, "Safebackups") } + +// use SaveConfig EXCLUSIVELY though loader.SaveConfig to trigger a reload afterwards! +func SaveConfig(cfg *JsonConfig) error { + file, err := os.Create(ConfigPath) + if err != nil { + return fmt.Errorf("error creating config.json: %v", err) + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(cfg); err != nil { + return fmt.Errorf("error encoding config.json: %v", err) + } + + return nil +} diff --git a/src/configchanger/configuration.go b/src/config/configchanger/configuration.go similarity index 86% rename from src/configchanger/configuration.go rename to src/config/configchanger/configuration.go index e0037da4..5cd6d666 100644 --- a/src/configchanger/configuration.go +++ b/src/config/configchanger/configuration.go @@ -5,33 +5,13 @@ import ( "fmt" "io" "net/http" - "os" "reflect" "strconv" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/loader" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" ) -// SaveConfig writes the given config to file and reloads it -func SaveConfig(cfg *config.JsonConfig) error { - file, err := os.Create(config.ConfigPath) - if err != nil { - return fmt.Errorf("error creating config.json: %v", err) - } - defer file.Close() - - encoder := json.NewEncoder(file) - encoder.SetIndent("", " ") - if err := encoder.Encode(cfg); err != nil { - return fmt.Errorf("error encoding config.json: %v", err) - } - - // Reload using the loader package - loader.ReloadAll() - return nil -} - func SaveConfigForm(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) @@ -84,12 +64,12 @@ func SaveConfigForm(w http.ResponseWriter, r *http.Request) { } // Save the updated config - if err := SaveConfig(existingConfig); err != nil { + if err := loader.SaveConfig(existingConfig); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - http.Redirect(w, r, "/", http.StatusSeeOther) + http.Redirect(w, r, "/config", http.StatusSeeOther) } func SaveConfigRestful(w http.ResponseWriter, r *http.Request) { @@ -176,7 +156,7 @@ func SaveConfigRestful(w http.ResponseWriter, r *http.Request) { } // Save the updated config - if err := SaveConfig(existingConfig); err != nil { + if err := loader.SaveConfig(existingConfig); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/src/config/helpers.go b/src/config/helpers.go index e8e0da91..5aaa4d68 100644 --- a/src/config/helpers.go +++ b/src/config/helpers.go @@ -4,6 +4,7 @@ package config import ( "crypto/rand" + "embed" "encoding/base64" "fmt" "os" @@ -109,3 +110,11 @@ func generateJwtKey() string { } return base64.RawURLEncoding.EncodeToString(key) } + +func SetV1UIFS(v1uiFS embed.FS) { + V1UIFS = v1uiFS +} + +func GetV1UIFS() embed.FS { + return V1UIFS +} diff --git a/src/config/vars.go b/src/config/vars.go index 2a6a2f97..995d45e7 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -1,6 +1,7 @@ package config import ( + "embed" "sync" "time" @@ -43,24 +44,31 @@ var ( SaveInterval string AutoPauseServer bool AutoSave bool + Difficulty string + StartCondition string + StartLocation string ) // Logging, debugging and misc var ( - IsDebugMode bool //only used for pprof server, keep it like this and check the log level instead. Debug = 10 - CreateSSUILogFile bool - LogLevel int - LogMessageBuffer string - IsFirstTimeSetup bool - BufferFlushTicker *time.Ticker - SSEMessageBufferSize = 2000 - MaxSSEConnections = 20 - GameServerAppID = "600760" - ExePath string - GameBranch string - SubsystemFilters []string - GameServerUUID uuid.UUID // Assined at startup to the current instance of the server we are managing. Currently unused. - AutoRestartServerTimer string + IsDebugMode bool //only used for pprof server, keep it like this and check the log level instead. Debug = 10 + CreateSSUILogFile bool + LogLevel int + LogMessageBuffer string + IsFirstTimeSetup bool + BufferFlushTicker *time.Ticker + SSEMessageBufferSize = 2000 + MaxSSEConnections = 20 + GameServerAppID = "600760" + ExePath string + GameBranch string + SubsystemFilters []string + GameServerUUID uuid.UUID // Assined at startup to the current instance of the server we are managing. Currently unused. + AutoRestartServerTimer string + IsConsoleEnabled bool + LogClutterToConsole bool // surpresses clutter mono logs from the gameserver + LanguageSetting string + AutoStartServerOnStartup bool ) // Discord integration @@ -83,15 +91,16 @@ var ( // Backup and cleanup settings var ( - IsCleanupEnabled bool - BackupKeepLastN int - BackupKeepDailyFor time.Duration - BackupKeepWeeklyFor time.Duration - BackupKeepMonthlyFor time.Duration - BackupCleanupInterval time.Duration - ConfiguredBackupDir string - ConfiguredSafeBackupDir string - BackupWaitTime time.Duration + IsCleanupEnabled bool + BackupKeepLastN int + BackupKeepDailyFor time.Duration + BackupKeepWeeklyFor time.Duration + BackupKeepMonthlyFor time.Duration + BackupCleanupInterval time.Duration + ConfiguredBackupDir string + ConfiguredSafeBackupDir string + BackupWaitTime time.Duration + IsNewTerrainAndSaveSystem bool ) // Authentication and security @@ -120,7 +129,7 @@ var ( TLSCertPath = "./UIMod/tls/cert.pem" TLSKeyPath = "./UIMod/tls/key.pem" ConfigPath = "./UIMod/config/config.json" - CustomDetectionsFilePath = "./UIMod/detectionmanager/customdetections.json" + CustomDetectionsFilePath = "./UIMod/config/customdetections.json" LogFolder = "./UIMod/logs/" UIModFolder = "./UIMod/" TwoBoxFormFolder = "./UIMod/twoboxform/" @@ -132,3 +141,7 @@ var ( SSCMFilePath = "./BepInEx/plugins/SSCM/SSCM.socket" SSCMPluginDir = "./BepInEx/plugins/SSCM/" ) + +// Bundled Assets + +var V1UIFS embed.FS diff --git a/src/core/loader/afterstart.go b/src/core/loader/afterstart.go new file mode 100644 index 00000000..9f1c3027 --- /dev/null +++ b/src/core/loader/afterstart.go @@ -0,0 +1,32 @@ +package loader + +import ( + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" +) + +func AfterStartComplete() { + existingConfig, err := config.LoadConfig() + if err != nil { + logger.Core.Error("AfterStartComplete: Failed to Load config: " + err.Error()) + } + err = SaveConfig(existingConfig, false) // save config, but explicitly DONT reload backend since config is already loaded + if err != nil { + logger.Core.Error("AfterStartComplete: Failed to save config: " + err.Error()) + } + err = setup.CleanUpOldUIModFolderFiles() + if err != nil { + logger.Core.Error("AfterStartComplete: Failed to clean up old pre-v5.5 UI mod folder files: " + err.Error()) + } + err = setup.CleanUpOldExecutables() + if err != nil { + logger.Core.Error("AfterStartComplete: Failed to clean up old executables: " + err.Error()) + } + if config.AutoStartServerOnStartup { + logger.Core.Info("AutoStartServerOnStartup is enabled, starting server...") + gamemgr.InternalStartServer() + } + setup.SetupAutostartScripts() +} diff --git a/src/loader/loader.go b/src/core/loader/helpers.go similarity index 71% rename from src/loader/loader.go rename to src/core/loader/helpers.go index 96765ed4..94ac68d8 100644 --- a/src/loader/loader.go +++ b/src/core/loader/helpers.go @@ -1,61 +1,25 @@ -// loader.go package loader import ( "fmt" "strconv" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/backupmgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/detectionmgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discordbot" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" ) -func ReloadAll() { - ReloadConfig() - ReloadBackupManager() - ReloadDiscordBot() -} - -func ReloadConfig() { - if _, err := config.LoadConfig(); err != nil { - logger.Core.Error("Failed to load config: " + err.Error()) - return +// this is a Hack, but it works for now. Ideally, move the getter setter logic from SteamServerUI to StationeersServerUI, but not feasible at the moment. +func SaveConfig(cfg *config.JsonConfig, reloadBackend ...bool) error { + err := config.SaveConfig(cfg) + if err != nil { + logger.Core.Error("Failed to save config: " + err.Error()) + return err } - - logger.Core.Info("Config reloaded successfully") - - if config.IsSSCMEnabled { - setup.InstallSSCM() + // Call ReloadBackend by default, unless reloadBackend is explicitly false + if len(reloadBackend) == 0 || reloadBackend[0] { + ReloadBackend() } - - PrintConfigDetails() -} - -func ReloadBackupManager() { - if err := backupmgr.ReloadBackupManagerFromConfig(); err != nil { - logger.Backup.Error("Failed to reload backup manager: " + err.Error()) - return - } - logger.Backup.Info("Backup manager reloaded successfully") -} - -func ReloadDiscordBot() { - if config.IsDiscordEnabled { - go discordbot.InitializeDiscordBot() - logger.Discord.Info("Discord bot reloaded successfully") - } -} - -// The detector should NOT be reloaded, as it is a singleton. Instead, dynamic changes come in via the custom detections manager. -func InitDetector() { - detector := detectionmgr.Start() - detectionmgr.RegisterDefaultHandlers(detector) - detectionmgr.InitCustomDetectionsManager(detector) - go detectionmgr.StreamLogs(detector) - logger.Detection.Info("Detector loaded successfully") + return nil } func PrintConfigDetails() { @@ -100,6 +64,7 @@ func PrintConfigDetails() { logger.Config.Debug(fmt.Sprintf("Branch: %s", config.Branch)) logger.Config.Debug(fmt.Sprintf("GameServerAppID: %s", config.GameServerAppID)) logger.Config.Debug(fmt.Sprintf("Version: %s", config.Version)) + logger.Config.Debug(fmt.Sprintf("IsNewTerrainAndSaveSystem: %v", config.IsNewTerrainAndSaveSystem)) logger.Config.Debug("---- UPDATER CONFIG VARS ----") logger.Config.Debug(fmt.Sprintf("AllowPrereleaseUpdates: %v", config.AllowPrereleaseUpdates)) diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go new file mode 100644 index 00000000..b70c5448 --- /dev/null +++ b/src/core/loader/loader.go @@ -0,0 +1,88 @@ +// loader.go +package loader + +import ( + "embed" + + "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/managers/backupmgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/detectionmgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup/update" +) + +// only call this once at startup +func InitBackend() { + ReloadConfig() + ReloadSSCM() + ReloadBackupManager() + ReloadLocalizer() + ReloadDiscordBot() +} + +// use this to reload backend at runtime +func ReloadBackend() { + + logger.Core.Info("Reloading backend...") + ReloadConfig() + ReloadSSCM() + ReloadBackupManager() + ReloadLocalizer() + PrintConfigDetails() +} + +// should ideally not be called standalone, if feasable, call ReloadBackend instead +func ReloadConfig() { + if _, err := config.LoadConfig(); err != nil { + logger.Core.Error("Failed to load config: " + err.Error()) + return + } + logger.Core.Info("Config loaded successfully") + +} + +func ReloadSSCM() { + if config.IsSSCMEnabled { + setup.InstallSSCM() + } +} + +func ReloadBackupManager() { + if err := backupmgr.ReloadBackupManagerFromConfig(); err != nil { + logger.Backup.Error("Failed to reload backup manager: " + err.Error()) + return + } + logger.Backup.Info("Backup manager reloaded successfully") +} + +func ReloadDiscordBot() { + if config.IsDiscordEnabled { + go discordbot.InitializeDiscordBot() + logger.Discord.Info("Discord bot reloaded successfully") + } +} + +// The detector should NOT be reloaded, as it is a singleton. Instead, dynamic changes come in via the custom detections manager. +func InitDetector() { + detector := detectionmgr.Start() + detectionmgr.RegisterDefaultHandlers(detector) + detectionmgr.InitCustomDetectionsManager(detector) + go detectionmgr.StreamLogs(detector) + logger.Detection.Info("Detector loaded successfully") +} + +func RestartBackend() { + update.RestartMySelf() +} + +func ReloadLocalizer() { + localization.ReloadLocalizer() +} + +// InitBundler initialized the onboard bundled assets for the web UI +func InitVirtFS(v1uiFS embed.FS) { + config.SetV1UIFS(v1uiFS) +} diff --git a/src/security/auth.go b/src/core/security/auth.go similarity index 100% rename from src/security/auth.go rename to src/core/security/auth.go diff --git a/src/security/tls.go b/src/core/security/tls.go similarity index 100% rename from src/security/tls.go rename to src/core/security/tls.go diff --git a/src/ssestream/ssemanager.go b/src/core/ssestream/ssemanager.go similarity index 72% rename from src/ssestream/ssemanager.go rename to src/core/ssestream/ssemanager.go index 33471ccf..17203976 100644 --- a/src/ssestream/ssemanager.go +++ b/src/core/ssestream/ssemanager.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" ) @@ -122,33 +123,51 @@ func (m *SSEManager) streamMessages( } } -// dropKinematicMessage checks if a message should be dropped due to kinematic warnings. This is a workaround "fix" for a bug in the gameserver. -func (m *SSEManager) dropKinematicMessage(message string) bool { - if !strings.Contains(message, "Setting linear velocity of a kinematic body is not supported") && - !strings.Contains(message, "Setting angular velocity of a kinematic body is not supported") { +// excludeClutterLogs checks if a message should be dropped due to kinematic warnings. This is a workaround "fix" for a bug in the gameserver. +func (m *SSEManager) excludeClutterLogs(message string) bool { + if config.LogClutterToConsole { return false } + dropMessages := map[string]bool{ + "Setting linear velocity of a kinematic body is not supported": true, + "Setting angular velocity of a kinematic body is not supported": true, + "WARNING: Shader": true, + "ERROR: Shader": true, + "No mesh data available": true, + "The image effect Main Camera": true, + "Unsupported shader": true, + "The shader": true, + "memorysetup": true, + "Microsoft Media Foundation video decoding": true, + "The referenced script on this Behaviour": true, + } - m.dropMu.Lock() - defer m.dropMu.Unlock() + // Check if message contains any of the drop messages + for dropMsg := range dropMessages { + if strings.Contains(message, dropMsg) { + m.dropMu.Lock() + defer m.dropMu.Unlock() - m.kinematicDropCount++ - now := time.Now() + m.kinematicDropCount++ + now := time.Now() - // Log only if it's been more than a minute since last log and we have messages to report - if m.kinematicDropCount > 0 && now.Sub(m.lastKinematicLog) >= time.Minute { - logger.SSE.Info(fmt.Sprintf("🗑️ Detected and Dropped %d kinematic body warning messages. (Gameserver Bug)", m.kinematicDropCount)) - m.lastKinematicLog = now - m.kinematicDropCount = 0 // Reset count after logging + // Log only if it's been more than a minute since last log and we have messages to report + if m.kinematicDropCount > 0 && now.Sub(m.lastKinematicLog) >= time.Minute { + logger.SSE.Info(fmt.Sprintf("🗑️ Detected and Dropped %d unhelpful game server log messages. (Workaround for Gameserver Bug)", m.kinematicDropCount)) + m.lastKinematicLog = now + m.kinematicDropCount = 0 // Reset count after logging + } + return true + } } - return true + return false } // Broadcast sends a message to all clients with a non-blocking approach func (m *SSEManager) Broadcast(message string) { // Check if message should be dropped - if m.dropKinematicMessage(message) { + if m.excludeClutterLogs(message) { return } diff --git a/src/ssestream/sseutils.go b/src/core/ssestream/sseutils.go similarity index 100% rename from src/ssestream/sseutils.go rename to src/core/ssestream/sseutils.go diff --git a/src/discordbot/handleDeprecated.go b/src/discordbot/handleDeprecated.go index 0b52145d..dce24b8c 100644 --- a/src/discordbot/handleDeprecated.go +++ b/src/discordbot/handleDeprecated.go @@ -4,8 +4,8 @@ import ( "strings" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/gamemgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" "github.com/bwmarrin/discordgo" ) diff --git a/src/discordbot/handleReactions.go b/src/discordbot/handleReactions.go index 30313958..fe479187 100644 --- a/src/discordbot/handleReactions.go +++ b/src/discordbot/handleReactions.go @@ -5,8 +5,8 @@ import ( "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/gamemgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" "github.com/bwmarrin/discordgo" ) diff --git a/src/discordbot/handleSlashcommands.go b/src/discordbot/handleSlashcommands.go index b12a5a4a..56d0a72f 100644 --- a/src/discordbot/handleSlashcommands.go +++ b/src/discordbot/handleSlashcommands.go @@ -7,10 +7,10 @@ import ( "strings" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/backupmgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/gamemgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/backupmgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" "github.com/bwmarrin/discordgo" ) diff --git a/src/gamemgr/args.go b/src/gamemgr/args.go deleted file mode 100644 index 157ee987..00000000 --- a/src/gamemgr/args.go +++ /dev/null @@ -1,78 +0,0 @@ -package gamemgr - -import ( - "runtime" - "strconv" - "strings" - - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" -) - -type Arg struct { - Flag string - Value string - RequiresValue bool - Condition func() bool - NoQuote bool -} - -func buildCommandArgs() []string { - var argOrder = []Arg{ - {Flag: "-nographics", RequiresValue: false}, - {Flag: "-batchmode", RequiresValue: false}, - {Flag: "-LOAD", Value: config.SaveInfo, RequiresValue: true, NoQuote: true}, // LOAD has special handling because the gameserver expects 2 parameters - {Flag: "-logFile", Value: "./debug.log", Condition: func() bool { return runtime.GOOS == "linux" }, RequiresValue: true}, - {Flag: "-settings", RequiresValue: false}, - {Flag: "StartLocalHost", Value: strconv.FormatBool(config.StartLocalHost), RequiresValue: true}, - {Flag: "ServerVisible", Value: strconv.FormatBool(config.ServerVisible), RequiresValue: true}, - {Flag: "GamePort", Value: config.GamePort, RequiresValue: true}, - {Flag: "UPNPEnabled", Value: strconv.FormatBool(config.UPNPEnabled), RequiresValue: true}, - {Flag: "ServerName", Value: config.ServerName, RequiresValue: true}, - {Flag: "ServerPassword", Value: config.ServerPassword, Condition: func() bool { return config.ServerPassword != "" }, RequiresValue: true}, - {Flag: "ServerMaxPlayers", Value: config.ServerMaxPlayers, RequiresValue: true}, - {Flag: "AutoSave", Value: strconv.FormatBool(config.AutoSave), RequiresValue: true}, - {Flag: "SaveInterval", Value: config.SaveInterval, RequiresValue: true}, - {Flag: "ServerAuthSecret", Value: config.ServerAuthSecret, Condition: func() bool { return config.ServerAuthSecret != "" }, RequiresValue: true}, - {Flag: "UpdatePort", Value: config.UpdatePort, RequiresValue: true}, - {Flag: "AutoPauseServer", Value: strconv.FormatBool(config.AutoPauseServer), RequiresValue: true}, - {Flag: "UseSteamP2P", Value: strconv.FormatBool(config.UseSteamP2P), RequiresValue: true}, - {Flag: "AdminPassword", Value: config.AdminPassword, Condition: func() bool { return config.AdminPassword != "" }, RequiresValue: true}, - } - - var args []string - for _, arg := range argOrder { - if arg.Condition != nil && !arg.Condition() { - continue - } - if arg.RequiresValue && arg.Value == "" { - continue - } - - args = append(args, arg.Flag) - - if arg.Flag == "-LOAD" && arg.Value != "" { - parts := strings.SplitN(arg.Value, " ", 2) - for _, part := range parts { - if part != "" { - args = append(args, part) - } - } - continue - } - - if arg.Value != "" { - args = append(args, arg.Value) - } - } - - if config.AdditionalParams != "" { - args = append(args, strings.Fields(config.AdditionalParams)...) - } - - if config.LocalIpAddress != "" { - args = append(args, "LocalIpAddress") - args = append(args, config.LocalIpAddress) - } - - return args -} diff --git a/src/localization/localization.go b/src/localization/localization.go new file mode 100644 index 00000000..49baa043 --- /dev/null +++ b/src/localization/localization.go @@ -0,0 +1,125 @@ +package localization + +import ( + "encoding/json" + "io/fs" + "strings" + "sync" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" +) + +// translations stores language code to key-value pairs +var translations = make(map[string]map[string]string) +var mu sync.RWMutex +var currentLanguage string + +const fallbackLanguage = "en-us" + +// reloads all translations and resets to the current language +func ReloadLocalizer() { + logger.Localization.Info("Reloading localization data") + currentLanguage = strings.ToLower(config.LanguageSetting) + loadTranslations() +} + +// loadTranslations reads all JSON files from virtFS +func loadTranslations() { + mu.Lock() + defer mu.Unlock() + + // Clear existing translations + translations = make(map[string]map[string]string) + + virtFS, err := fs.Sub(config.V1UIFS, "UIMod/onboard_bundled/localization") + if err != nil { + logger.Localization.Error("Failed to access virtual filesystem: " + err.Error()) + return + } + + entries, err := fs.ReadDir(virtFS, ".") + if err != nil { + logger.Localization.Error("Failed to read virtual filesystem directory: " + err.Error()) + return + } + + // Process each JSON file + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(strings.ToLower(entry.Name()), ".json") { + continue // Skip directories and non-JSON files + } + + langCode := strings.ToLower(strings.TrimSuffix(entry.Name(), ".json")) + file, err := virtFS.Open(entry.Name()) + if err != nil { + logger.Localization.Error("Failed to open file " + entry.Name() + ": " + err.Error()) + continue + } + + data, err := fs.ReadFile(virtFS, entry.Name()) + if err != nil { + logger.Localization.Error("Failed to read file " + entry.Name() + ": " + err.Error()) + file.Close() + continue + } + file.Close() + + var rawData map[string]any + if err := json.Unmarshal(data, &rawData); err != nil { + logger.Localization.Error("Failed to parse JSON in " + entry.Name() + ": " + err.Error()) + continue + } + + langMap := make(map[string]string) + flattenJSON(rawData, langMap) + + translations[langCode] = langMap + logger.Localization.Debug("Loaded translations for language: " + langCode) + } + + if _, exists := translations[fallbackLanguage]; !exists { + logger.Localization.Warn("Fallback language en-us not found in localization files") + } +} + +func flattenJSON(data map[string]interface{}, output map[string]string) { + for key, value := range data { + switch v := value.(type) { + case map[string]interface{}: + flattenJSON(v, output) + case string: + // stor str vals directly + output[key] = v + default: + logger.Localization.Warn("Ignoring non-string value for key: " + key) + } + } +} + +// GetString returns the localized string for the given key +func GetString(key string) string { + mu.RLock() + defer mu.RUnlock() + + //logger.Localization.Warn("Looking up key: " + key + " in language: " + currentLanguage) + + // Try current language + if langMap, exists := translations[currentLanguage]; exists { + if translation, exists := langMap[key]; exists { + return translation + } + } + + // Fall back to en-us + if langMap, exists := translations[fallbackLanguage]; exists { + if translation, exists := langMap[key]; exists { + logger.Localization.Debug("Falling back to en-us for key: " + key) + return translation + } + } + + // Return key as final fallback + logger.Localization.Warn("Translation not found for key: " + key) + return key +} diff --git a/src/logger/logger.go b/src/logger/logger.go index b05d718a..832a7a1f 100644 --- a/src/logger/logger.go +++ b/src/logger/logger.go @@ -13,16 +13,17 @@ import ( // Logger instances var ( - Main = &Logger{prefix: SYS_MAIN} - Web = &Logger{prefix: SYS_WEB} - Discord = &Logger{prefix: SYS_DISCORD} - Backup = &Logger{prefix: SYS_BACKUP} - Detection = &Logger{prefix: SYS_DETECT} - Core = &Logger{prefix: SYS_CORE} - Config = &Logger{prefix: SYS_CONFIG} - Install = &Logger{prefix: SYS_INSTALL} - SSE = &Logger{prefix: SYS_SSE} - Security = &Logger{prefix: SYS_SECURITY} + Main = &Logger{prefix: SYS_MAIN} + Web = &Logger{prefix: SYS_WEB} + Discord = &Logger{prefix: SYS_DISCORD} + Backup = &Logger{prefix: SYS_BACKUP} + Detection = &Logger{prefix: SYS_DETECT} + Core = &Logger{prefix: SYS_CORE} + Config = &Logger{prefix: SYS_CONFIG} + Install = &Logger{prefix: SYS_INSTALL} + SSE = &Logger{prefix: SYS_SSE} + Security = &Logger{prefix: SYS_SECURITY} + Localization = &Logger{prefix: SYS_LOCALIZATION} ) // Severity Levels @@ -35,16 +36,17 @@ const ( // Subsystems const ( - SYS_MAIN = "MAIN" - SYS_WEB = "WEB" - SYS_DISCORD = "DISCORD" - SYS_BACKUP = "BACKUP" - SYS_DETECT = "DETECT" - SYS_CORE = "CORE" - SYS_CONFIG = "CONFIG" - SYS_INSTALL = "INSTALL" - SYS_SSE = "SSE" - SYS_SECURITY = "SECURITY" + SYS_MAIN = "MAIN" + SYS_WEB = "WEB" + SYS_DISCORD = "DISCORD" + SYS_BACKUP = "BACKUP" + SYS_DETECT = "DETECT" + SYS_CORE = "CORE" + SYS_CONFIG = "CONFIG" + SYS_INSTALL = "INSTALL" + SYS_SSE = "SSE" + SYS_SECURITY = "SECURITY" + SYS_LOCALIZATION = "LOCALIZATION" ) const ( @@ -59,16 +61,17 @@ const ( // Subsystem color map (distinct colors, cohesive vibe) var subsystemColors = map[string]string{ - SYS_MAIN: colorBlue, // Calm, default system - SYS_WEB: colorCyan, // Clean, UI-related - SYS_DISCORD: colorMagenta, // Flashy, chatty subsystem - SYS_BACKUP: colorGreen, // Safe, reliable vibe - SYS_DETECT: colorYellow, // Attention-grabbing for detection - SYS_CORE: colorMagenta, // Critical, stands out - SYS_CONFIG: colorYellow, // Warning-like, config tweaks - SYS_INSTALL: colorBlue, // Matches MAIN, setup-related - SYS_SSE: colorCyan, // Matches WEB, streaming vibe - SYS_SECURITY: colorRed, // Screams "pay attention" + SYS_MAIN: colorBlue, // Calm, default system + SYS_WEB: colorCyan, // Clean, UI-related + SYS_DISCORD: colorMagenta, // Flashy, chatty subsystem + SYS_BACKUP: colorGreen, // Safe, reliable vibe + SYS_DETECT: colorYellow, // Attention-grabbing for detection + SYS_CORE: colorMagenta, // Critical, stands out + SYS_CONFIG: colorYellow, // Warning-like, config tweaks + SYS_INSTALL: colorBlue, // Matches MAIN, setup-related + SYS_SSE: colorCyan, // Matches WEB, streaming vibe + SYS_SECURITY: colorRed, // Screams "pay attention" + SYS_LOCALIZATION: colorCyan, // Matches WEB, localization-related } type Logger struct { @@ -238,3 +241,7 @@ func (l *Logger) SSE(message string) { func (l *Logger) Security(message string) { l.log(logEntry{ERROR, "SECURITY", colorReset, message}) // Red via subsystem } + +func (l *Logger) Localization(message string) { + l.log(logEntry{INFO, "LOCALIZATION", colorReset, message}) // Cyan via subsystem +} diff --git a/src/backupmgr/backuphttp.go b/src/managers/backupmgr/backuphttp.go similarity index 100% rename from src/backupmgr/backuphttp.go rename to src/managers/backupmgr/backuphttp.go diff --git a/src/backupmgr/backupinterface.go b/src/managers/backupmgr/backupinterface.go similarity index 83% rename from src/backupmgr/backupinterface.go rename to src/managers/backupmgr/backupinterface.go index 1c040fcb..b5b7f526 100644 --- a/src/backupmgr/backupinterface.go +++ b/src/managers/backupmgr/backupinterface.go @@ -5,6 +5,7 @@ import ( "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" ) // GlobalBackupManager is the singleton instance of the backup manager @@ -20,16 +21,21 @@ func InitGlobalBackupManager(config BackupConfig) error { } GlobalBackupManager = NewBackupManager(config) - if err := GlobalBackupManager.Initialize(); err != nil { - return err - } // Update all active HTTP handlers with the new manager for _, handler := range activeHTTPHandlers { handler.manager = GlobalBackupManager } - return GlobalBackupManager.Start() + // Start the backup manager in a goroutine to avoid blocking + go func() { + if err := GlobalBackupManager.Start(); err != nil { + logger.Backup.Error("Failed to start global backup manager: " + err.Error()) + } + }() + + // Return immediately, initialization will complete in the background + return nil } // RegisterHTTPHandler registers an HTTP handler to be updated when the manager changes diff --git a/src/backupmgr/cleanup.go b/src/managers/backupmgr/cleanup.go similarity index 76% rename from src/backupmgr/cleanup.go rename to src/managers/backupmgr/cleanup.go index e95653b6..340e4e3f 100644 --- a/src/backupmgr/cleanup.go +++ b/src/managers/backupmgr/cleanup.go @@ -124,26 +124,37 @@ func (m *BackupManager) cleanSafeBackupDir() error { // getBackupGroups collects and groups backup files func (m *BackupManager) getBackupGroups() ([]BackupGroup, error) { - files, err := os.ReadDir(m.config.SafeBackupDir) + var files []os.DirEntry + err := filepath.WalkDir(m.config.SafeBackupDir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() { + files = append(files, d) + } + return nil + }) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to walk safe backup dir: %w", err) } groups := make(map[int]BackupGroup) for _, file := range files { - if file.IsDir() { + filename := file.Name() + if !isValidBackupFile(filename) { continue } - index := parseBackupIndex(file.Name()) - if index == -1 { + fullPath := filepath.Join(m.config.SafeBackupDir, filename) + info, err := file.Info() + if err != nil { continue } - fullPath := filepath.Join(m.config.SafeBackupDir, file.Name()) - info, err := os.Stat(fullPath) - if err != nil { + // Parse index or assign synthetic index for .save files + index := parseBackupIndex(filename, info.ModTime(), files) + if index == -1 { continue } @@ -151,13 +162,17 @@ func (m *BackupManager) getBackupGroups() ([]BackupGroup, error) { group.Index = index group.ModTime = info.ModTime() - switch { - case strings.HasSuffix(file.Name(), ".bin"): + if strings.HasSuffix(filename, ".save") { group.BinFile = fullPath - case strings.Contains(file.Name(), "world(") && strings.HasSuffix(file.Name(), ".xml"): - group.XMLFile = fullPath - case strings.Contains(file.Name(), "world_meta(") && strings.HasSuffix(file.Name(), ".xml"): - group.MetaFile = fullPath + } else { + switch { + case strings.HasSuffix(filename, ".bin"): + group.BinFile = fullPath + case strings.Contains(filename, "world(") && strings.HasSuffix(filename, ".xml"): + group.XMLFile = fullPath + case strings.Contains(filename, "world_meta(") && strings.HasSuffix(filename, ".xml"): + group.MetaFile = fullPath + } } groups[index] = group @@ -165,7 +180,8 @@ func (m *BackupManager) getBackupGroups() ([]BackupGroup, error) { var result []BackupGroup for _, group := range groups { - if group.BinFile != "" && group.XMLFile != "" && group.MetaFile != "" { + // Include both old-style groups (all three files) and .save-based groups (just BinFile) + if (group.BinFile != "" && group.XMLFile != "" && group.MetaFile != "") || (group.BinFile != "" && strings.HasSuffix(group.BinFile, ".save")) { result = append(result, group) } } diff --git a/src/backupmgr/manager.go b/src/managers/backupmgr/manager.go similarity index 60% rename from src/backupmgr/manager.go rename to src/managers/backupmgr/manager.go index 1927ee3e..390daf87 100644 --- a/src/backupmgr/manager.go +++ b/src/managers/backupmgr/manager.go @@ -10,6 +10,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/commandmgr" "github.com/fsnotify/fsnotify" ) @@ -20,30 +21,69 @@ Background routines (file watching and cleanup) only start when Start() is calle can coexist but may conflict if configured with overlapping directories. */ -// Initialize sets up required directories -func (m *BackupManager) Initialize() error { +// Initialize checks for BackupDir and waits until it exists, then ensures SafeBackupDir exists. +// It returns a channel that signals when initialization is complete or an error occurs. +func (m *BackupManager) Initialize() <-chan error { m.mu.Lock() defer m.mu.Unlock() - if err := os.MkdirAll(m.config.BackupDir, os.ModePerm); err != nil { - return err - } - return os.MkdirAll(m.config.SafeBackupDir, os.ModePerm) + result := make(chan error, 1) + + go func() { + const timeout = 90 * time.Minute + const pollInterval = 2500 * time.Millisecond + deadline := time.Now().Add(timeout) + + // Wait for BackupDir to exist + for { + if _, err := os.Stat(m.config.BackupDir); err == nil { + // Directory exists, proceed + break + } else if !os.IsNotExist(err) { + // An error other than "not exists" occurred + result <- fmt.Errorf("error checking backup directory %s: %v", m.config.BackupDir, err) + return + } + + if time.Now().After(deadline) { + result <- fmt.Errorf("timeout waiting for backup directory %s to be created", m.config.BackupDir) + return + } + logger.Backup.Debug("Backup manager waiting for save folder " + m.config.BackupDir + " to be created by Stationeers...") + + // Wait before checking again + time.Sleep(pollInterval) + } + + // Ensure SafeBackupDir exists, create it if it doesn't + if err := os.MkdirAll(m.config.SafeBackupDir, os.ModePerm); err != nil { + result <- fmt.Errorf("error creating safe backup directory %s: %v", m.config.SafeBackupDir, err) + return + } + logger.Backup.Debug("Backup manager created safebackups dir successfully") + + result <- nil + }() + + return result } // Start begins the backup monitoring and cleanup routines func (m *BackupManager) Start() error { - if err := m.Initialize(); err != nil { - return fmt.Errorf("failed to initialize backup directories: %w", err) + // Wait for initialization to complete + logger.Backup.Debug("Backup manager is waiting for save folder initialization...") + initResult := <-m.Initialize() + if initResult != nil { + return fmt.Errorf("failed to initialize backup manager: %w", initResult) } + logger.Backup.Info("Backup manager started") // Start file watcher watcher, err := newFsWatcher(m.config.BackupDir) if err != nil { - return fmt.Errorf("failed to create file watcher: %w", err) + return fmt.Errorf("failed to create autosave watcher: %w", err) } m.watcher = watcher - go m.watchBackups() if config.IsCleanupEnabled { @@ -88,6 +128,13 @@ func (m *BackupManager) handleNewBackup(filePath string) { return } + if config.IsSSCMEnabled && config.IsNewTerrainAndSaveSystem { + commandmgr.WriteCommand("SAVE") + logger.Backup.Info("HEAD Save triggered via SSCM") + } else { + logger.Backup.Info("HEAD Save NOT refreshed via SSCM") + } + m.wg.Add(1) go func() { defer m.wg.Done() @@ -98,7 +145,17 @@ func (m *BackupManager) handleNewBackup(filePath string) { defer m.mu.Unlock() fileName := filepath.Base(filePath) - dstPath := filepath.Join(m.config.SafeBackupDir, fileName) + relativePath, err := filepath.Rel(m.config.BackupDir, filePath) + if err != nil { + logger.Backup.Error("Error getting relative path for " + filePath + ": " + err.Error()) + return + } + dstPath := filepath.Join(m.config.SafeBackupDir, relativePath) + + if err := os.MkdirAll(filepath.Dir(dstPath), os.ModePerm); err != nil { + logger.Backup.Error("Error creating destination dir for " + dstPath + ": " + err.Error()) + return + } if err := copyFile(filePath, dstPath); err != nil { logger.Backup.Error("Error copying backup " + fileName + ": " + err.Error()) diff --git a/src/managers/backupmgr/restore.go b/src/managers/backupmgr/restore.go new file mode 100644 index 00000000..ed0fed23 --- /dev/null +++ b/src/managers/backupmgr/restore.go @@ -0,0 +1,113 @@ +package backupmgr + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" +) + +// RestoreBackup restores a backup with the given index +func (m *BackupManager) RestoreBackup(index int) error { + m.mu.Lock() + defer m.mu.Unlock() + logger.Backup.Info("Restoring backup with index " + fmt.Sprintf("%d", index)) + + groups, err := m.getBackupGroups() + if err != nil { + return fmt.Errorf("failed to get backup groups: %w", err) + } + + var targetGroup BackupGroup + for _, group := range groups { + if group.Index == index { + targetGroup = group + break + } + } + + if targetGroup.Index == 0 { + return fmt.Errorf("no backup found with index %d", index) + } + + restoredFiles := make(map[string]string) + + // Handle .save file or old-style trio + if targetGroup.BinFile != "" && strings.HasSuffix(targetGroup.BinFile, ".save") { + // .save file case + backupFile := targetGroup.BinFile + destFile := filepath.Join("./saves/"+m.config.WorldName, m.config.WorldName+".save") + + // Before restore, check if we have existing .save files in the root saves/WorldName dir + saveDir := filepath.Join("./saves/", m.config.WorldName) + files, err := os.ReadDir(saveDir) + if err != nil { + return fmt.Errorf("failed to read save directory %s: %w", saveDir, err) + } + + for _, file := range files { + if file.IsDir() { + continue + } + if strings.HasSuffix(file.Name(), ".save") { + existingFile := filepath.Join(saveDir, file.Name()) + // Move existing .save file to SafeBackupDir with timestamp to avoid overwrites + timestamp := time.Now().Format("2006-01-02_15-04-05") + savedPreviousHeadSaveFilePath := filepath.Join(m.config.SafeBackupDir, fmt.Sprintf("%s_%s_%s", "oldHeadSaveBackup", timestamp, file.Name())) + if err := os.Rename(existingFile, savedPreviousHeadSaveFilePath); err != nil { + return fmt.Errorf("failed to move existing HEAD .save file %s to %s: %w", existingFile, savedPreviousHeadSaveFilePath, err) + } + logger.Backup.Info("Moved previous HEAD .save file to: " + savedPreviousHeadSaveFilePath) + } + } + + // Now copy the new .save file + if err := copyFile(backupFile, destFile); err != nil { + m.revertRestore(restoredFiles) + return fmt.Errorf("failed to restore .save file %s: %w", backupFile, err) + } + restoredFiles[destFile] = backupFile + } else { + // Old-style trio (world_meta.xml, world.xml, world.bin) + files := []struct { + backupName string + backupNameAlt string + destName string + }{ + {fmt.Sprintf("world_meta(%d).xml", index), fmt.Sprintf("world_meta(%d)_AutoSave.xml", index), "world_meta.xml"}, + {fmt.Sprintf("world(%d).xml", index), fmt.Sprintf("world(%d)_AutoSave.xml", index), "world.xml"}, + {fmt.Sprintf("world(%d).bin", index), fmt.Sprintf("world(%d)_AutoSave.bin", index), "world.bin"}, + } + + for _, file := range files { + backupFile := filepath.Join(m.config.SafeBackupDir, file.backupName) + destFile := filepath.Join("./saves/"+m.config.WorldName, file.destName) + + if err := copyFile(backupFile, destFile); err != nil { + // Try alternative name + backupFileAlt := filepath.Join(m.config.SafeBackupDir, file.backupNameAlt) + if err := copyFile(backupFileAlt, destFile); err != nil { + m.revertRestore(restoredFiles) + return fmt.Errorf("failed to restore %s: %w", file.backupName, err) + } + backupFile = backupFileAlt + } + restoredFiles[destFile] = backupFile + } + } + logger.Backup.Debug(fmt.Sprintf("%v", restoredFiles)) + + return nil +} + +// revertRestore undoes a failed restore operation +func (m *BackupManager) revertRestore(restoredFiles map[string]string) { + for destFile, backupFile := range restoredFiles { + if err := os.Remove(destFile); err == nil { + _ = copyFile(backupFile, destFile) + } + } +} diff --git a/src/backupmgr/types.go b/src/managers/backupmgr/types.go similarity index 100% rename from src/backupmgr/types.go rename to src/managers/backupmgr/types.go diff --git a/src/managers/backupmgr/utils.go b/src/managers/backupmgr/utils.go new file mode 100644 index 00000000..15c00eee --- /dev/null +++ b/src/managers/backupmgr/utils.go @@ -0,0 +1,90 @@ +package backupmgr + +import ( + "io" + "os" + "regexp" + "sort" + "strconv" + "strings" + "time" +) + +// copyFile copies a file from src to dst +func copyFile(src, dst string) error { + source, err := os.Open(src) + if err != nil { + return err + } + defer source.Close() + + destination, err := os.Create(dst) + if err != nil { + return err + } + defer destination.Close() + + if _, err := io.Copy(destination, source); err != nil { + return err + } + + return destination.Sync() +} + +// parseBackupIndex extracts the backup index from a filename or assigns a synthetic index +func parseBackupIndex(filename string, modTime time.Time, files []os.DirEntry) int { + // Try to extract index from old format (e.g., world(1).xml) + re := regexp.MustCompile(`\((\d+)\)`) + matches := re.FindStringSubmatch(filename) + if len(matches) >= 2 { + index, err := strconv.Atoi(matches[1]) + if err == nil { + return index + } + } + + // For .save files, assign synthetic index based on mod time (newest eq highest) + if strings.HasSuffix(filename, ".save") { + // Sort files by mod time to assign indexes + var sortedFiles []struct { + name string + modTime time.Time + } + for _, file := range files { + if !strings.HasSuffix(file.Name(), ".save") { + continue + } + info, err := file.Info() + if err != nil { + continue + } + sortedFiles = append(sortedFiles, struct { + name string + modTime time.Time + }{file.Name(), info.ModTime()}) + } + + // Sort newest first + sort.Slice(sortedFiles, func(i, j int) bool { + return sortedFiles[i].modTime.After(sortedFiles[j].modTime) + }) + + // Find the position of the current file + for i, f := range sortedFiles { + if f.name == filename { + // Assign index starting from max possible index downwards + return len(sortedFiles) - i + } + } + } + + return -1 +} + +// isValidBackupFile checks if a filename is a valid backup file +func isValidBackupFile(filename string) bool { + return (strings.Contains(filename, "world") && + (strings.HasSuffix(filename, ".bin") || + strings.HasSuffix(filename, ".xml"))) || + strings.HasSuffix(filename, ".save") +} diff --git a/src/backupmgr/watcher.go b/src/managers/backupmgr/watcher.go similarity index 75% rename from src/backupmgr/watcher.go rename to src/managers/backupmgr/watcher.go index e2e48413..4cc00c9f 100644 --- a/src/backupmgr/watcher.go +++ b/src/managers/backupmgr/watcher.go @@ -2,6 +2,7 @@ package backupmgr import ( "fmt" + "os" "path/filepath" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" @@ -29,11 +30,24 @@ func newFsWatcher(path string) (*fsWatcher, error) { } logger.Backup.Debug("Watcher created successfully") - if err := watcher.Add(normalizedPath); err != nil { + // Watch the root save path and all subdirectories + err = filepath.WalkDir(normalizedPath, func(subPath string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + if err := watcher.Add(subPath); err != nil { + logger.Backup.Error("Failed to add subdir to watcher: " + subPath + ": " + err.Error()) + } else { + logger.Backup.Debug("Successfully watching subdir: " + subPath) + } + } + return nil + }) + if err != nil { watcher.Close() - return nil, fmt.Errorf("failed to add path %s to watcher: %w", normalizedPath, err) + return nil, fmt.Errorf("failed to add paths to watcher: %w", err) } - logger.Backup.Debug("Successfully watching path: " + normalizedPath) w := &fsWatcher{ watcher: watcher, diff --git a/src/commandmgr/commandmgr.go b/src/managers/commandmgr/commandmgr.go similarity index 100% rename from src/commandmgr/commandmgr.go rename to src/managers/commandmgr/commandmgr.go diff --git a/src/detectionmgr/customdetections.go b/src/managers/detectionmgr/customdetections.go similarity index 100% rename from src/detectionmgr/customdetections.go rename to src/managers/detectionmgr/customdetections.go diff --git a/src/detectionmgr/detector.go b/src/managers/detectionmgr/detector.go similarity index 100% rename from src/detectionmgr/detector.go rename to src/managers/detectionmgr/detector.go diff --git a/src/detectionmgr/handlers.go b/src/managers/detectionmgr/handlers.go similarity index 98% rename from src/detectionmgr/handlers.go rename to src/managers/detectionmgr/handlers.go index 86bc01e8..b30e61ce 100644 --- a/src/detectionmgr/handlers.go +++ b/src/managers/detectionmgr/handlers.go @@ -6,9 +6,9 @@ import ( "strings" "time" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/ssestream" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discordbot" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/ssestream" ) /* diff --git a/src/detectionmgr/http.go b/src/managers/detectionmgr/http.go similarity index 100% rename from src/detectionmgr/http.go rename to src/managers/detectionmgr/http.go diff --git a/src/detectionmgr/interface.go b/src/managers/detectionmgr/interface.go similarity index 100% rename from src/detectionmgr/interface.go rename to src/managers/detectionmgr/interface.go diff --git a/src/detectionmgr/logstream.go b/src/managers/detectionmgr/logstream.go similarity index 86% rename from src/detectionmgr/logstream.go rename to src/managers/detectionmgr/logstream.go index 59b43467..865de822 100644 --- a/src/detectionmgr/logstream.go +++ b/src/managers/detectionmgr/logstream.go @@ -3,9 +3,9 @@ package detectionmgr import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/ssestream" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discordbot" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/ssestream" ) /* @@ -21,7 +21,7 @@ func StreamLogs(detector *Detector) { logChan := ssestream.ConsoleStreamManager.AddInternalSubscriber() go func() { - logger.Detection.Info("Connected to internal log stream.") + logger.Detection.Debug("Connected to internal log stream.") for logMessage := range logChan { if config.IsDiscordEnabled { discordbot.PassLogStreamToDiscordLogBuffer(logMessage) diff --git a/src/detectionmgr/types.go b/src/managers/detectionmgr/types.go similarity index 87% rename from src/detectionmgr/types.go rename to src/managers/detectionmgr/types.go index 58e1dccc..c24773f6 100644 --- a/src/detectionmgr/types.go +++ b/src/managers/detectionmgr/types.go @@ -3,17 +3,6 @@ package detectionmgr import "regexp" -const ( - // ANSI color codes for styling terminal output - colorReset = "\033[0m" - colorRed = "\033[31m" - colorGreen = "\033[32m" - colorYellow = "\033[33m" - colorBlue = "\033[34m" - colorMagenta = "\033[35m" - colorCyan = "\033[36m" -) - // EventType defines the type of event detected type EventType string diff --git a/src/managers/gamemgr/args.go b/src/managers/gamemgr/args.go new file mode 100644 index 00000000..08eaf448 --- /dev/null +++ b/src/managers/gamemgr/args.go @@ -0,0 +1,117 @@ +package gamemgr + +import ( + "runtime" + "strconv" + "strings" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" +) + +type Arg struct { + Flag string + Value string + RequiresValue bool + Condition func() bool + NoQuote bool +} + +func buildCommandArgs() []string { + var argOrder []Arg + + if config.IsNewTerrainAndSaveSystem { + argOrder = []Arg{ + {Flag: "-nographics", RequiresValue: false}, + {Flag: "-batchmode", RequiresValue: false}, + /* file start: (expects up to four optional args for: + -worldid (Optional to LOAD, required to CREATE save, if start value is not found, tries to create map with worldid) -> config.BackupWorldName for legacy reasons + -difficulty (Optional, defaults to "Normal" if not provided) + -startcondition (Optional, defaults to the default start condition for the world setting if not provided.) + -startlocation (Optional, defaults to "DefaultStartLocation" if not provided.) + */ + {Flag: "-file", RequiresValue: false}, + {Flag: "start", Value: config.WorldName, RequiresValue: true}, + {Flag: config.BackupWorldName, RequiresValue: false}, + {Flag: config.Difficulty, RequiresValue: false, Condition: func() bool { return config.Difficulty != "" }}, + {Flag: config.StartCondition, RequiresValue: false, Condition: func() bool { return config.StartCondition != "" }}, + {Flag: config.StartLocation, RequiresValue: false, Condition: func() bool { return config.StartLocation != "" }}, + // file start end + {Flag: "-logFile", Value: "./debug.log", Condition: func() bool { return runtime.GOOS == "linux" }, RequiresValue: true}, + {Flag: "-settings", RequiresValue: false}, + {Flag: "StartLocalHost", Value: strconv.FormatBool(config.StartLocalHost), RequiresValue: true}, + {Flag: "ServerVisible", Value: strconv.FormatBool(config.ServerVisible), RequiresValue: true}, + {Flag: "GamePort", Value: config.GamePort, RequiresValue: true}, + {Flag: "UPNPEnabled", Value: strconv.FormatBool(config.UPNPEnabled), RequiresValue: true}, + {Flag: "ServerName", Value: config.ServerName, RequiresValue: true}, + {Flag: "ServerPassword", Value: config.ServerPassword, Condition: func() bool { return config.ServerPassword != "" }, RequiresValue: true}, + {Flag: "ServerMaxPlayers", Value: config.ServerMaxPlayers, RequiresValue: true}, + {Flag: "AutoSave", Value: strconv.FormatBool(config.AutoSave), RequiresValue: true}, + {Flag: "SaveInterval", Value: config.SaveInterval, RequiresValue: true}, + {Flag: "ServerAuthSecret", Value: config.ServerAuthSecret, Condition: func() bool { return config.ServerAuthSecret != "" }, RequiresValue: true}, + {Flag: "UpdatePort", Value: config.UpdatePort, RequiresValue: true}, + {Flag: "AutoPauseServer", Value: strconv.FormatBool(config.AutoPauseServer), RequiresValue: true}, + {Flag: "UseSteamP2P", Value: strconv.FormatBool(config.UseSteamP2P), RequiresValue: true}, + {Flag: "AdminPassword", Value: config.AdminPassword, Condition: func() bool { return config.AdminPassword != "" }, RequiresValue: true}, + } + } + if !config.IsNewTerrainAndSaveSystem { + argOrder = []Arg{ + {Flag: "-nographics", RequiresValue: false}, + {Flag: "-batchmode", RequiresValue: false}, + {Flag: "-LOAD", Value: config.SaveInfo, RequiresValue: true, NoQuote: true}, // LOAD has special handling because the gameserver expects 2 parameters + {Flag: "-logFile", Value: "./debug.log", Condition: func() bool { return runtime.GOOS == "linux" }, RequiresValue: true}, + {Flag: "-settings", RequiresValue: false}, + {Flag: "StartLocalHost", Value: strconv.FormatBool(config.StartLocalHost), RequiresValue: true}, + {Flag: "ServerVisible", Value: strconv.FormatBool(config.ServerVisible), RequiresValue: true}, + {Flag: "GamePort", Value: config.GamePort, RequiresValue: true}, + {Flag: "UPNPEnabled", Value: strconv.FormatBool(config.UPNPEnabled), RequiresValue: true}, + {Flag: "ServerName", Value: config.ServerName, RequiresValue: true}, + {Flag: "ServerPassword", Value: config.ServerPassword, Condition: func() bool { return config.ServerPassword != "" }, RequiresValue: true}, + {Flag: "ServerMaxPlayers", Value: config.ServerMaxPlayers, RequiresValue: true}, + {Flag: "AutoSave", Value: strconv.FormatBool(config.AutoSave), RequiresValue: true}, + {Flag: "SaveInterval", Value: config.SaveInterval, RequiresValue: true}, + {Flag: "ServerAuthSecret", Value: config.ServerAuthSecret, Condition: func() bool { return config.ServerAuthSecret != "" }, RequiresValue: true}, + {Flag: "UpdatePort", Value: config.UpdatePort, RequiresValue: true}, + {Flag: "AutoPauseServer", Value: strconv.FormatBool(config.AutoPauseServer), RequiresValue: true}, + {Flag: "UseSteamP2P", Value: strconv.FormatBool(config.UseSteamP2P), RequiresValue: true}, + {Flag: "AdminPassword", Value: config.AdminPassword, Condition: func() bool { return config.AdminPassword != "" }, RequiresValue: true}, + } + } + + var args []string + for _, arg := range argOrder { + if arg.Condition != nil && !arg.Condition() { + continue + } + if arg.RequiresValue && arg.Value == "" { + continue + } + + args = append(args, arg.Flag) + + if arg.Flag == "-LOAD" && arg.Value != "" { + parts := strings.SplitN(arg.Value, " ", 2) + for _, part := range parts { + if part != "" { + args = append(args, part) + } + } + continue + } + + if arg.Value != "" { + args = append(args, arg.Value) + } + } + + if config.AdditionalParams != "" { + args = append(args, strings.Fields(config.AdditionalParams)...) + } + + if config.LocalIpAddress != "" { + args = append(args, "LocalIpAddress") + args = append(args, config.LocalIpAddress) + } + + return args +} diff --git a/src/gamemgr/processmanagement.go b/src/managers/gamemgr/processmanagement.go similarity index 88% rename from src/gamemgr/processmanagement.go rename to src/managers/gamemgr/processmanagement.go index 63efc119..cdac8785 100644 --- a/src/gamemgr/processmanagement.go +++ b/src/managers/gamemgr/processmanagement.go @@ -12,9 +12,9 @@ import ( "syscall" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/commandmgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/commandmgr" "github.com/google/uuid" ) @@ -24,6 +24,7 @@ var ( logDone chan struct{} err error autoRestartDone chan struct{} + processExited chan struct{} ) // InternalIsServerRunning checks if the server process is running. @@ -35,34 +36,25 @@ func InternalIsServerRunning() bool { } // internalIsServerRunningNoLock checks if the server process is running. -// Caller must hold mu.Lock(). +// Caller M U S T hold mu.Lock(). func internalIsServerRunningNoLock() bool { if cmd == nil || cmd.Process == nil { return false } if runtime.GOOS == "windows" { - done := make(chan error, 1) - go func() { done <- cmd.Wait() }() select { - case err := <-done: - // process has likely exited - if err != nil { - logger.Core.Debug("Wait failed: " + err.Error()) - if strings.Contains(err.Error(), "The handle is invalid") { - cmd = nil - clearGameServerUUID() - return false - } - } + case <-processExited: cmd = nil clearGameServerUUID() return false - case <-time.After(50 * time.Millisecond): + default: // Process is still running return true } - } else { + } + + if runtime.GOOS == "linux" { // On Unix-like systems, use Signal(0) if err := cmd.Process.Signal(syscall.Signal(0)); err != nil { logger.Core.Debug("Signal(0) failed, assuming process is dead: " + err.Error()) @@ -72,6 +64,9 @@ func internalIsServerRunningNoLock() bool { } return true } + + logger.Core.Warn("Failed to check if server is running, assuming it's dead") + return false } func InternalStartServer() error { @@ -102,12 +97,14 @@ func InternalStartServer() error { logger.Core.Info("BepInEx/Doorstop environment configured for server process") } logger.Core.Info("• Executable: " + config.ExePath + " (with SSCM)") + logger.Core.Info("• Arguments: " + strings.Join(args, " ")) } if !config.IsSSCMEnabled && runtime.GOOS == "linux" { // Use ExePath directly as the command cmd = exec.Command(config.ExePath, args...) logger.Core.Info("• Executable: " + config.ExePath) + logger.Core.Info("• Arguments: " + strings.Join(args, " ")) } if runtime.GOOS == "windows" { @@ -137,6 +134,18 @@ func InternalStartServer() error { // Start reading stdout and stderr pipes on Windows go readPipe(stdout) go readPipe(stderr) + + // Monitor process exit + processExited = make(chan struct{}) + go func() { + err := cmd.Wait() + if err != nil { + logger.Core.Debug("Process exited with error: " + err.Error()) + } else { + logger.Core.Debug("Process exited successfully") + } + close(processExited) + }() } else { logger.Core.Debug("Switching to log file for logs as we are on Linux! Hail the Penguin!") @@ -191,7 +200,7 @@ func InternalStopServer() error { if autoRestartDone != nil { close(autoRestartDone) autoRestartDone = nil - logger.Core.Info("Auto-restart cycle interrupted due to manaual stop") + logger.Core.Info("Auto-restart cycle interrupted due to manual stop") } // Process is running, stop it @@ -201,6 +210,15 @@ func InternalStopServer() error { if isWindows { // On Windows, terminate the process (no graceful shutdown) killErr = cmd.Process.Kill() + // Wait for the processExited channel to confirm exit + if processExited != nil { + select { + case <-processExited: + logger.Core.Debug("processExited channel confirmed server shutdown") + case <-time.After(2 * time.Second): + logger.Core.Warn("Timeout waiting for processExited confirmation") + } + } } else { // On Linux/Unix, send SIGTERM for graceful shutdown if termErr := cmd.Process.Signal(syscall.SIGTERM); termErr != nil { @@ -239,24 +257,6 @@ func InternalStopServer() error { } } - // For Windows, wait briefly after Kill to ensure process is gone - if isWindows { - waitErrChan := make(chan error, 1) - go func() { - waitErrChan <- cmd.Wait() - }() - - select { - case waitErr := <-waitErrChan: - if waitErr != nil && !strings.Contains(waitErr.Error(), "exit status") && - !strings.Contains(waitErr.Error(), "The handle is invalid") { - return fmt.Errorf("error during server shutdown: %v", waitErr) - } - case <-time.After(1 * time.Second): - return fmt.Errorf("timeout waiting for process to exit") - } - } - if killErr != nil { return fmt.Errorf("error stopping server: %v", killErr) } diff --git a/src/gamemgr/serverlog.go b/src/managers/gamemgr/serverlog.go similarity index 98% rename from src/gamemgr/serverlog.go rename to src/managers/gamemgr/serverlog.go index bd92666d..fc950cff 100644 --- a/src/gamemgr/serverlog.go +++ b/src/managers/gamemgr/serverlog.go @@ -10,8 +10,8 @@ import ( "strconv" "time" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/ssestream" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/ssestream" ) // readPipe for Windows diff --git a/src/gamemgr/sscm.go b/src/managers/gamemgr/sscm.go similarity index 100% rename from src/gamemgr/sscm.go rename to src/managers/gamemgr/sscm.go diff --git a/src/setup/autostartscripts.go b/src/setup/autostartscripts.go new file mode 100644 index 00000000..4fe653d4 --- /dev/null +++ b/src/setup/autostartscripts.go @@ -0,0 +1,48 @@ +package setup + +import ( + "io" + "io/fs" + "os" + "runtime" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" +) + +func SetupAutostartScripts() { + scriptFS, err := fs.Sub(config.V1UIFS, "UIMod/onboard_bundled/scripts") + if err != nil { + return + } + + if runtime.GOOS == "windows" { + script, err := scriptFS.Open("autostart.ps1") + if err != nil { + return + } + defer script.Close() + data, err := io.ReadAll(script) + if err != nil { + return + } + err = os.WriteFile("autostart.ps1", data, 0755) + if err != nil { + return + } + } + if runtime.GOOS == "linux" { + script, err := scriptFS.Open("autostart.sh") + if err != nil { + return + } + defer script.Close() + data, err := io.ReadAll(script) + if err != nil { + return + } + err = os.WriteFile("autostart.service", data, 0755) + if err != nil { + return + } + } +} diff --git a/src/setup/cleanup.go b/src/setup/cleanup.go new file mode 100644 index 00000000..dde1e310 --- /dev/null +++ b/src/setup/cleanup.go @@ -0,0 +1,129 @@ +package setup + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" +) + +func CleanUpOldUIModFolderFiles() error { + uiModFolder := config.UIModFolder + customdetectionsSourceFile := filepath.Join(uiModFolder, "detectionmanager", "customdetections.json") + customdetectionsDestinationFile := config.CustomDetectionsFilePath + oldUiFolder := filepath.Join(uiModFolder, "ui") // used to test if we need clean up from a structure before v5.5 (since we now have embedded assets) + + //if uiModFolder doesn't contain a folder called UI, return early as there is nothing to clean up + if _, err := os.Stat(oldUiFolder); os.IsNotExist(err) { + return nil + } + + // Copy customdetections.json to the destination path + if _, err := os.Stat(customdetectionsSourceFile); err == nil { + // Ensure destination directory exists + destDir := filepath.Dir(customdetectionsDestinationFile) + if err := os.MkdirAll(destDir, 0755); err != nil { + return fmt.Errorf("failed to create destination directory: %w", err) + } + + // Read source file + data, err := os.ReadFile(customdetectionsSourceFile) + if err != nil { + return fmt.Errorf("failed to read source file: %w", err) + } + + // Write to destination file + if err := os.WriteFile(customdetectionsDestinationFile, data, 0644); err != nil { + return fmt.Errorf("failed to write destination file: %w", err) + } + } else if !os.IsNotExist(err) { + logger.Core.Error("Error moving customdetections.json file to new location: " + err.Error()) + } + + // List of folders to remove + foldersToRemove := []string{ + filepath.Join(uiModFolder, "detectionmanager"), + filepath.Join(uiModFolder, "ui"), + filepath.Join(uiModFolder, "twoboxform"), + filepath.Join(uiModFolder, "assets"), + } + + // Remove specified folders if they exist + for _, folder := range foldersToRemove { + if _, err := os.Stat(folder); err == nil { + if err := os.RemoveAll(folder); err != nil { + return fmt.Errorf("failed to remove folder %s: %w", folder, err) + } + } else if !os.IsNotExist(err) { + return fmt.Errorf("error checking folder %s: %w", folder, err) + } + } + + return nil +} + +func CleanUpOldExecutables() error { + // Exit early if update is disabled to allow running old versions if needed + if !config.IsUpdateEnabled { + return nil + } + currentBackendVersion := config.Version + pattern := `StationeersServerControlv(\d+\.\d+\.\d+)(?:\.exe|\.x86_64)$` + re, err := regexp.Compile(pattern) + if err != nil { + return fmt.Errorf("failed to compile regex: %w", err) + } + + // Get current directory + dir, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current directory: %w", err) + } + + // Walk through the directory + err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Skip directories, non-matching files, and files with _old prefix + if info.IsDir() || !re.MatchString(info.Name()) || strings.HasPrefix(info.Name(), "_old") { + return nil + } + + // Extract version from filename + matches := re.FindStringSubmatch(info.Name()) + if len(matches) < 2 { + return nil + } + fileVersion := matches[1] + + // Skip if the version matches the current backend version + if fileVersion == currentBackendVersion { + return nil + } + + // Generate new filename with _old prefix + newName := "_old" + info.Name() + newPath := filepath.Join(filepath.Dir(path), newName) + + // Rename the file + err = os.Rename(path, newPath) + if err != nil { + return fmt.Errorf("failed to rename %s to %s: %w", path, newName, err) + } + logger.Install.Info(fmt.Sprintf("Old Executable cleanup: Renamed %s to %s", path, newName)) + + return nil + }) + + if err != nil { + return fmt.Errorf("error walking directory: %w", err) + } + + return nil +} diff --git a/src/setup/install.go b/src/setup/install.go index 59bcec06..bc567c20 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -16,6 +16,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup/update" ) var downloadBranch string // Holds the branch to download from @@ -25,38 +26,30 @@ func Install(wg *sync.WaitGroup) { defer wg.Done() // Signal that installation is complete // Step 0: Check for updates - if err := UpdateExecutable(); err != nil { + if err := update.UpdateExecutable(); err != nil { logger.Install.Error("❌Update check went sideways: " + err.Error()) } // Step 1: Check and download the UIMod folder contents - logger.Install.Info("🔄Checking UIMod folder contents...") + logger.Install.Info("🔄Checking UIMod folder...") CheckAndDownloadUIMod() logger.Install.Info("✅UIMod folder setup complete.") // Step 2: Check for Blacklist.txt and create it if it doesn't exist logger.Install.Info("🔄Checking for Blacklist.txt...") checkAndCreateBlacklist() logger.Install.Info("✅Blacklist.txt verified or created.") - time.Sleep(2 * time.Second) // Small pause to let the user read potential errors // Step 3: Install and run SteamCMD logger.Install.Info("🔄Installing and running SteamCMD...") InstallAndRunSteamCMD() - logger.Install.Warn("🙏Thank you for using StationeersServerUI!") logger.Install.Info("✅Setup complete!") } func CheckAndDownloadUIMod() { uiModDir := config.UIModFolder - twoBoxFormDir := config.UIModFolder + "twoboxform/" - detectionmanagerDir := config.UIModFolder + "detectionmanager/" - assetDir := config.UIModFolder + "assets/" - cssAssetDIr := config.UIModFolder + "assets/css/" - uiDir := config.UIModFolder + "ui/" configDir := config.UIModFolder + "config/" tlsDir := config.UIModFolder + "tls/" - jsAssetDir := config.UIModFolder + "assets/js/" - requiredDirs := []string{uiModDir, uiDir, assetDir, cssAssetDIr, twoBoxFormDir, detectionmanagerDir, configDir, jsAssetDir} + requiredDirs := []string{uiModDir, configDir} // Set branch if config.Branch == "release" || config.Branch == "Release" { @@ -64,39 +57,23 @@ func CheckAndDownloadUIMod() { } else { downloadBranch = config.Branch } - logger.Install.Info("Using branch: " + downloadBranch) + logger.Install.Debug("Using branch: " + downloadBranch) // Define file mappings files := map[string]string{ - uiDir + "config.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/ui/config.html", downloadBranch), - uiDir + "index.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/ui/index.html", downloadBranch), - uiDir + "detectionmanager.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/ui/detectionmanager.html", downloadBranch), - assetDir + "stationeers.png": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/stationeers.png", downloadBranch), - assetDir + "favicon.ico": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/favicon.ico", downloadBranch), - assetDir + "apiinfo.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/apiinfo.html", downloadBranch), - twoBoxFormDir + "twoboxform.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.css", downloadBranch), - twoBoxFormDir + "twoboxform.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.js", downloadBranch), - twoBoxFormDir + "twoboxform.html": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/twoboxform/twoboxform.html", downloadBranch), - cssAssetDIr + "apiinfo.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/apiinfo.css", downloadBranch), - cssAssetDIr + "background.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/background.css", downloadBranch), - cssAssetDIr + "base.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/base.css", downloadBranch), - cssAssetDIr + "components.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/components.css", downloadBranch), - cssAssetDIr + "config.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/config.css", downloadBranch), - cssAssetDIr + "detectionmanager.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/detectionmanager.css", downloadBranch), - cssAssetDIr + "home.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/home.css", downloadBranch), - cssAssetDIr + "mobile.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/mobile.css", downloadBranch), - cssAssetDIr + "style.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/style.css", downloadBranch), - cssAssetDIr + "tabs.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/tabs.css", downloadBranch), - cssAssetDIr + "variables.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/variables.css", downloadBranch), - jsAssetDir + "main.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/main.js", downloadBranch), - jsAssetDir + "detectionmanager.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/detectionmanager.js", downloadBranch), - jsAssetDir + "console-manager.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/console-manager.js", downloadBranch), - jsAssetDir + "server-api.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/server-api.js", downloadBranch), - jsAssetDir + "ui-utils.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/js/ui-utils.js", downloadBranch), + // NOTE: Now empty as files are now embedded in the executable. Kept this structure for future use. + + // UI - commented out since files are embedded, left here for reference in case we need this funcitonality again + // "ui/config.html": "https://raw.githubusercontent.com/SteamServerUI/SteamServerUI/{branch}/UIMod/ui/config.html", } createRequiredDirs(requiredDirs) + if len(files) == 0 { + logger.Install.Debug("📁 File mappings empty - no additional files to download available") + return + } + // Check if the directory exists if _, err := os.Stat(uiModDir); os.IsNotExist(err) { // Initial download @@ -382,6 +359,7 @@ func createRequiredDirs(requiredDirs []string) { // Create directories for _, dir := range requiredDirs { if _, err := os.Stat(dir); os.IsNotExist(err) { + config.IsFirstTimeSetup = true err := os.MkdirAll(dir, os.ModePerm) if err != nil { logger.Install.Error("❌Error creating folder: " + err.Error()) diff --git a/src/setup/sscm.go b/src/setup/sscm.go index 9108922e..28fb4866 100644 --- a/src/setup/sscm.go +++ b/src/setup/sscm.go @@ -18,7 +18,6 @@ var installMutex sync.Mutex func CheckAndDownloadSSCM() { SSCMPluginDir := config.SSCMPluginDir sscmDir := config.SSCMWebDir - cssAssetDIr := config.UIModFolder + "assets/css/" requiredDirs := []string{SSCMPluginDir, sscmDir} @@ -33,9 +32,6 @@ func CheckAndDownloadSSCM() { // Define file mappings files := map[string]string{ SSCMPluginDir + "SSCM.dll": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/sscm/SSCM.dll", downloadBranch), - SSCMPluginDir + "SSCM.pdb": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/sscm/SSCM.pdb", downloadBranch), - sscmDir + "sscm.js": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/sscm/sscm.js", downloadBranch), - cssAssetDIr + "sscm.css": fmt.Sprintf("https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/%s/UIMod/assets/css/sscm.css", downloadBranch), } // Check if the directory exists diff --git a/src/setup/steamcmd.go b/src/setup/steamcmd.go index 335027e1..7db0d37a 100644 --- a/src/setup/steamcmd.go +++ b/src/setup/steamcmd.go @@ -6,6 +6,7 @@ import ( "os/exec" "path/filepath" "runtime" + "strings" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" @@ -27,6 +28,11 @@ const ( // InstallAndRunSteamCMD installs and runs SteamCMD based on the platform (Windows/Linux). // It automatically detects the OS and calls the appropriate installation function. func InstallAndRunSteamCMD() { + if config.Branch == "indev-no-steamcmd" { + logger.Install.Info("🔍 Detected indev-no-steamcmd branch, skipping SteamCMD installation") + return + } + if runtime.GOOS == "windows" { installSteamCMDWindows() } else if runtime.GOOS == "linux" { @@ -86,7 +92,7 @@ func installSteamCMD(platform string, steamCMDDir string, downloadURL string, ex logger.Install.Info("✅ SteamCMD installed successfully.\n") } else { - logger.Install.Info("✅ SteamCMD is already installed.\n") + logger.Install.Info("✅ SteamCMD is already installed.") } // Run SteamCMD @@ -128,7 +134,12 @@ func runSteamCMD(steamCMDDir string) { cmd.Stderr = os.Stderr // Run the command - logger.Install.Info("🕑 Running SteamCMD...\n") + if config.LogLevel == 10 { + cmdString := strings.Join(cmd.Args, " ") + logger.Install.Info("🕑 Running SteamCMD: " + cmdString) + } else { + logger.Install.Info("🕑 Running SteamCMD...") + } err = cmd.Run() if err != nil { logger.Install.Error("❌ Error running SteamCMD: " + err.Error() + "\n") @@ -140,15 +151,11 @@ func runSteamCMD(steamCMDDir string) { // buildSteamCMDCommand constructs the SteamCMD command based on the OS. func buildSteamCMDCommand(steamCMDDir, currentDir string) *exec.Cmd { //print the config.GameBranch and config.GameServerAppID - logger.Install.Info("🔍 Game Branch: " + config.GameBranch + "\n") - logger.Install.Debug("🔍 Game Server App ID: " + config.GameServerAppID + "\n") + logger.Install.Info("🔍 Game Branch: " + config.GameBranch) + logger.Install.Debug("🔍 Game Server App ID: " + config.GameServerAppID) if runtime.GOOS == "windows" { return exec.Command(filepath.Join(steamCMDDir, "steamcmd.exe"), "+force_install_dir", currentDir, "+login", "anonymous", "+app_update", config.GameServerAppID, "-beta", config.GameBranch, "validate", "+quit") } - - if config.GameBranch == "public" { - return exec.Command(filepath.Join(steamCMDDir, "steamcmd.sh"), "+force_install_dir", currentDir, "+login", "anonymous", "+app_update", config.GameServerAppID, "validate", "+quit") - } return exec.Command(filepath.Join(steamCMDDir, "steamcmd.sh"), "+force_install_dir", currentDir, "+login", "anonymous", "+app_update", config.GameServerAppID, "-beta", config.GameBranch, "validate", "+quit") } diff --git a/src/setup/update-helper-linux.go b/src/setup/update/update-helper-linux.go similarity index 84% rename from src/setup/update-helper-linux.go rename to src/setup/update/update-helper-linux.go index d2c7f64e..1d515dd3 100644 --- a/src/setup/update-helper-linux.go +++ b/src/setup/update/update-helper-linux.go @@ -1,6 +1,6 @@ //go:build linux -package setup +package update import ( "os/exec" diff --git a/src/setup/update-helper-windows.go b/src/setup/update/update-helper-windows.go similarity index 90% rename from src/setup/update-helper-windows.go rename to src/setup/update/update-helper-windows.go index 37d63df2..e99961aa 100644 --- a/src/setup/update-helper-windows.go +++ b/src/setup/update/update-helper-windows.go @@ -1,6 +1,6 @@ //go:build windows -package setup +package update import ( "os/exec" diff --git a/src/setup/updater.go b/src/setup/update/updater.go similarity index 65% rename from src/setup/updater.go rename to src/setup/update/updater.go index 0c5e9f61..cdd8b456 100644 --- a/src/setup/updater.go +++ b/src/setup/update/updater.go @@ -1,4 +1,4 @@ -package setup +package update import ( "encoding/json" @@ -38,7 +38,14 @@ type Version struct { // UpdateExecutable checks for and applies the latest release from GitHub func UpdateExecutable() error { if !config.IsUpdateEnabled { - logger.Install.Warn("⚠️ Update check is disabled. Skipping update check.") + logger.Install.Warn("⚠️ Update check is disabled. Skipping update check. Change 'IsUpdateEnabled' in config.json to true to re-enable update checks.") + time.Sleep(1000 * time.Millisecond) + logger.Install.Info("⚠️ Continuing in 3 seconds...") + time.Sleep(1000 * time.Millisecond) + logger.Install.Info("⚠️ Continuing in 2 seconds...") + time.Sleep(1000 * time.Millisecond) + logger.Install.Info("⚠️ Continuing in 1 seconds...") + time.Sleep(1000 * time.Millisecond) return nil } @@ -47,7 +54,11 @@ func UpdateExecutable() error { return nil } - logger.Install.Info("🕵️ Querying GitHub API for the latest release...") + if config.AllowPrereleaseUpdates { + logger.Install.Info("🕵️ Querying GitHub API for the latest (pre)release...") + } else { + logger.Install.Info("🕵️ Querying GitHub API for the latest stable release...") + } latestRelease, err := getLatestRelease() if err != nil { return fmt.Errorf("❌ Failed to fetch latest release: %v", err) @@ -65,12 +76,6 @@ func UpdateExecutable() error { logger.Install.Info(fmt.Sprintf("Current version: %s, Latest version: %s", config.Version, latestRelease.TagName)) - // Check pre-release status - if latestRelease.Prerelease && !config.AllowPrereleaseUpdates { - logger.Install.Warn(fmt.Sprintf("⚠️ Latest version %s is a pre-release. Enable 'AllowPrerelease' in config to update.", latestRelease.TagName)) - return nil - } - // Check if we should update updateReason, shouldUpdate := shouldUpdate(currentVer, latestVer) if !shouldUpdate { @@ -78,7 +83,14 @@ func UpdateExecutable() error { case "up-to-date": logger.Install.Info("🎉 No update needed: you’re already on the latest version.") case "major-update": - logger.Install.Warn(fmt.Sprintf("⚠️ Latest version %s is a major update from %s. Major Updates include Breaking changes in this project. Read the release notes and backup your Server folder before updating. Enable 'AllowMajorUpdates' in config to proceed.", latestRelease.TagName, config.Version)) + logger.Install.Warn(fmt.Sprintf("⚠️ Update found: Latest version %s is a major update from %s. Major Updates include Breaking changes in this project. Read the release notes and backup your Server folder before updating. Enable 'AllowMajorUpdates' in config to proceed.", latestRelease.TagName, config.Version)) + time.Sleep(1000 * time.Millisecond) + logger.Install.Info("⚠️ Continuing in 3 seconds...") + time.Sleep(1000 * time.Millisecond) + logger.Install.Info("⚠️ Continuing in 2 seconds...") + time.Sleep(1000 * time.Millisecond) + logger.Install.Info("⚠️ Continuing in 1 seconds...") + time.Sleep(1000 * time.Millisecond) } return nil } @@ -135,6 +147,27 @@ func UpdateExecutable() error { return nil } +func RestartMySelf() { + currentExe, err := os.Executable() + if err != nil { + logger.Install.Warn(fmt.Sprintf("⚠️ Restart failed: couldn’t get current executable path: %v. Keeping version %s.", err, config.Version)) + return + } + + if runtime.GOOS == "windows" { + if err := runAndExit(currentExe); err != nil { + logger.Install.Warn(fmt.Sprintf("⚠️ Restart failed: couldn’t launch %s: %v. Keeping version %s.", currentExe, err, config.Version)) + return + } + } + if runtime.GOOS == "linux" { + if err := runAndExitLinux(currentExe); err != nil { + logger.Install.Warn(fmt.Sprintf("⚠️ Restart failed: couldn’t launch %s: %v. Keeping version %s.", currentExe, err, config.Version)) + return + } + } +} + // parseVersion parses a version string (e.g., "4.6.10") into a Version struct and tries to handle a few culprits too func parseVersion(v string) (Version, error) { v = strings.TrimPrefix(v, "v") @@ -167,9 +200,10 @@ func shouldUpdate(current, latest Version) (string, bool) { return "", true } -// getLatestRelease fetches the latest release info from GitHub API +// getLatestRelease fetches the most recent release (or prerelease) from GitHub API func getLatestRelease() (*githubRelease, error) { - resp, err := http.Get("https://api.github.com/repos/JacksonTheMaster/StationeersServerUI/releases/latest") + url := "https://api.github.com/repos/JacksonTheMaster/StationeersServerUI/releases" + resp, err := http.Get(url) if err != nil { return nil, err } @@ -179,11 +213,93 @@ func getLatestRelease() (*githubRelease, error) { return nil, fmt.Errorf("bad response from GitHub API: %s", resp.Status) } - var release githubRelease - if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + var releases []githubRelease + if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil { return nil, fmt.Errorf("failed to parse GitHub API response: %v", err) } - return &release, nil + + if len(releases) == 0 { + return nil, fmt.Errorf("no releases found") + } + + // Find the most recent release + var latestRelease *githubRelease + var latestVersion Version + for i, release := range releases { + version, err := parseVersion(release.TagName) + if err != nil { + logger.Install.Warn(fmt.Sprintf("Skipping invalid version tag %s: %v", release.TagName, err)) + continue + } + if i == 0 || isReleaseNewerVersion(version, latestVersion) { + currentVersion, err := parseVersion(config.Version) + if err == nil && isReleaseNewerVersion(currentVersion, version) { + if release.Prerelease { + logger.Install.Warn("Found a prerelease, but it is older than the running version. Skipping...") + continue + } + continue + } + latestVersion = version + latestRelease = &releases[i] + } + } + + if latestRelease == nil { + return nil, fmt.Errorf("no suitable releases found") + } + + // Log warning if the latest release is a prerelease + if latestRelease.Prerelease && !config.AllowPrereleaseUpdates { + logger.Install.Warn(fmt.Sprintf("⚠️ Pre-release Update found: Latest version %s is a pre-release. Enable 'AllowPrereleaseUpdates' in config.json to update to it.", latestRelease.TagName)) + time.Sleep(1000 * time.Millisecond) + logger.Install.Info("⚠️ Continuing in 3 seconds...") + time.Sleep(1000 * time.Millisecond) + logger.Install.Info("⚠️ Continuing in 2 seconds...") + time.Sleep(1000 * time.Millisecond) + logger.Install.Info("⚠️ Continuing in 1 seconds...") + time.Sleep(1000 * time.Millisecond) + } + + // If prerelease and AllowPrereleaseUpdates is false, find the latest stable release + if latestRelease.Prerelease && !config.AllowPrereleaseUpdates { + var stableRelease *githubRelease + var stableVersion Version + for i, release := range releases { + if release.Prerelease { + continue + } + version, err := parseVersion(release.TagName) + if err != nil { + logger.Install.Warn(fmt.Sprintf("Skipping invalid version tag %s: %v", release.TagName, err)) + continue + } + if i == 0 || isReleaseNewerVersion(version, stableVersion) { + stableVersion = version + stableRelease = &releases[i] + } + } + if stableRelease == nil { + return nil, fmt.Errorf("no stable releases found") + } + return stableRelease, nil + } + + return latestRelease, nil +} + +// isNewerVersion compares two versions to determine if the first is newer +func isReleaseNewerVersion(v1, v2 Version) bool { + if v1.Major != v2.Major { + return v1.Major > v2.Major + } + if v1.Minor != v2.Minor { + return v1.Minor > v2.Minor + } + if v1.Patch == v2.Patch { + return false + } + return v1.Patch > v2.Patch } // downloadNewExecutable downloads the new executable with a progress bar diff --git a/src/web/TwoBoxForm.go b/src/web/TwoBoxForm.go index 3153e57d..4fc7efe7 100644 --- a/src/web/TwoBoxForm.go +++ b/src/web/TwoBoxForm.go @@ -1,10 +1,12 @@ package web import ( + "io/fs" "net/http" "text/template" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" ) @@ -46,9 +48,16 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { SecondaryPlaceholderText string } - tmpl, err := template.ParseFiles(config.TwoBoxFormHtmlPath) + twoboxformAssetsFS, err := fs.Sub(config.GetV1UIFS(), "UIMod/onboard_bundled/twoboxform") if err != nil { - logger.Web.Error("Failed to parse 2BoxForm template: %v" + err.Error()) + logger.Web.Error("Failed to get bundled FS") + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + tmpl, err := template.ParseFS(twoboxformAssetsFS, "twoboxform.html") + if err != nil { + logger.Web.Error("Failed to parse 2BoxForm template") http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } @@ -63,219 +72,236 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { steps := map[string]Step{ "welcome": { ID: "welcome", - Title: "Stationeers Server UI", + Title: localization.GetString("UIText_Welcome_Title"), HeaderTitle: "", StepMessage: "", PrimaryLabel: "", SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Start Setup", - SkipButtonText: "Skip Setup", + SubmitButtonText: localization.GetString("UIText_Welcome_SubmitButton"), + SkipButtonText: localization.GetString("UIText_Welcome_SkipButton"), + NextStep: "pls_read", + }, + "pls_read": { + ID: "pls_read", + Title: localization.GetString("UIText_PlsRead_Title"), + HeaderTitle: localization.GetString("UIText_PlsRead_HeaderTitle"), + StepMessage: localization.GetString("UIText_PlsRead_StepMessage"), + PrimaryLabel: "", + SecondaryLabel: "", + SecondaryLabelType: "hidden", + SubmitButtonText: localization.GetString("UIText_PlsRead_SubmitButton"), + SkipButtonText: localization.GetString("UIText_PlsRead_SkipButton"), NextStep: "server_name", }, "server_name": { ID: "server_name", - Title: "Stationeers Server UI", - HeaderTitle: "Server Name Setup", - StepMessage: "Give your server a name like 'Space Station 13'", - PrimaryPlaceholderText: "My Stationeers Server with UI", - PrimaryLabel: "Server Name", + Title: localization.GetString("UIText_ServerName_Title"), + HeaderTitle: localization.GetString("UIText_ServerName_HeaderTitle"), + StepMessage: localization.GetString("UIText_ServerName_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_ServerName_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_ServerName_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", + SubmitButtonText: localization.GetString("UIText_ServerName_SubmitButton"), + SkipButtonText: localization.GetString("UIText_ServerName_SkipButton"), ConfigField: "ServerName", NextStep: "save_identifier", }, "save_identifier": { ID: "save_identifier", - Title: "Stationeers Server UI", - HeaderTitle: "Save Identifier Setup", - StepMessage: "Set a save identifier like 'SpaceStation13 Moon'. Capitalize the first letter of each word. Possible World types can be found in the Stationeers Wiki or the Stationeers Server UI GitHub Wiki.", - PrimaryPlaceholderText: "Requires a SaveName and WorldType for first start!", - PrimaryLabel: "Save Identifier", + Title: localization.GetString("UIText_SaveIdentifier_Title"), + HeaderTitle: localization.GetString("UIText_SaveIdentifier_HeaderTitle"), + StepMessage: localization.GetString("UIText_SaveIdentifier_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_SaveIdentifier_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_SaveIdentifier_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", + SubmitButtonText: localization.GetString("UIText_SaveIdentifier_SubmitButton"), + SkipButtonText: localization.GetString("UIText_SaveIdentifier_SkipButton"), ConfigField: "SaveInfo", NextStep: "max_players", }, "max_players": { ID: "max_players", - Title: "Stationeers Server UI", - HeaderTitle: "Player Limit Setup", - StepMessage: "Choose the maximum number of players that can connect to the server.", - PrimaryPlaceholderText: "8", - PrimaryLabel: "Max Players", + Title: localization.GetString("UIText_MaxPlayers_Title"), + HeaderTitle: localization.GetString("UIText_MaxPlayers_HeaderTitle"), + StepMessage: localization.GetString("UIText_MaxPlayers_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_MaxPlayers_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_MaxPlayers_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", + SubmitButtonText: localization.GetString("UIText_MaxPlayers_SubmitButton"), + SkipButtonText: localization.GetString("UIText_MaxPlayers_SkipButton"), ConfigField: "ServerMaxPlayers", NextStep: "server_password", }, "server_password": { ID: "server_password", - Title: "Stationeers Server UI", - HeaderTitle: "Server Password Setup", - StepMessage: "Set a gameserver password or skip this step.", - PrimaryPlaceholderText: "Server Password", - PrimaryLabel: "Server Password", + Title: localization.GetString("UIText_ServerPassword_Title"), + HeaderTitle: localization.GetString("UIText_ServerPassword_HeaderTitle"), + StepMessage: localization.GetString("UIText_ServerPassword_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_ServerPassword_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_ServerPassword_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", + SubmitButtonText: localization.GetString("UIText_ServerPassword_SubmitButton"), + SkipButtonText: localization.GetString("UIText_ServerPassword_SkipButton"), ConfigField: "ServerPassword", NextStep: "game_branch", }, "game_branch": { ID: "game_branch", - Title: "Stationeers Server UI", - HeaderTitle: "Game Branch Setup", - StepMessage: "Enter a beta branch or skip this to use the release version. If switching branches, make sure to restart SSUI after completing this wizzard.", - PrimaryPlaceholderText: "beta", - PrimaryLabel: "Game Branch", + Title: localization.GetString("UIText_GameBranch_Title"), + HeaderTitle: localization.GetString("UIText_GameBranch_HeaderTitle"), + StepMessage: localization.GetString("UIText_GameBranch_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_GameBranch_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_GameBranch_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Use Release Version", - ConfigField: "GameBranch", + SubmitButtonText: localization.GetString("UIText_GameBranch_SubmitButton"), + SkipButtonText: localization.GetString("UIText_GameBranch_SkipButton"), + ConfigField: "gameBranch", + NextStep: "newterrain_and_savesystem", + }, + "newterrain_and_savesystem": { + ID: "newterrain_and_savesystem", + Title: localization.GetString("UIText_NewTerrainAndSaveSystem_Title"), + HeaderTitle: localization.GetString("UIText_NewTerrainAndSaveSystem_HeaderTitle"), + StepMessage: localization.GetString("UIText_NewTerrainAndSaveSystem_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_NewTerrainAndSaveSystem_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_NewTerrainAndSaveSystem_PrimaryLabel"), + SecondaryLabel: "", + SecondaryLabelType: "hidden", + SubmitButtonText: localization.GetString("UIText_NewTerrainAndSaveSystem_SubmitButton"), + SkipButtonText: localization.GetString("UIText_NewTerrainAndSaveSystem_SkipButton"), + ConfigField: "IsNewTerrainAndSaveSystem", NextStep: "network_config_choice", }, - "discord_enabled": { ID: "discord_enabled", - Title: "Stationeers Server UI", - HeaderTitle: "Discord Integration", - StepMessage: "Do you want to enable Discord integration? Enter 'yes' to enable or Skip to disable.", - PrimaryPlaceholderText: "yes", - PrimaryLabel: "Enable Discord", + Title: localization.GetString("UIText_DiscordEnabled_Title"), + HeaderTitle: localization.GetString("UIText_DiscordEnabled_HeaderTitle"), + StepMessage: localization.GetString("UIText_DiscordEnabled_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_DiscordEnabled_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_DiscordEnabled_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip (Disable Discord)", + SubmitButtonText: localization.GetString("UIText_DiscordEnabled_SubmitButton"), + SkipButtonText: localization.GetString("UIText_DiscordEnabled_SkipButton"), ConfigField: "isDiscordEnabled", // We'll handle the boolean conversion in JS NextStep: "discord_token", // Default next step if enabled // The actual next step will be determined by JS based on the answer }, - "discord_token": { ID: "discord_token", - Title: "Stationeers Server UI", - HeaderTitle: "Discord Bot Token", - StepMessage: "Enter your Discord bot token for server integration", - PrimaryPlaceholderText: "Discord Bot Token", - PrimaryLabel: "Discord Token", + Title: localization.GetString("UIText_DiscordToken_Title"), + HeaderTitle: localization.GetString("UIText_DiscordToken_HeaderTitle"), + StepMessage: localization.GetString("UIText_DiscordToken_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_DiscordToken_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_DiscordToken_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", + SubmitButtonText: localization.GetString("UIText_DiscordToken_SubmitButton"), + SkipButtonText: localization.GetString("UIText_DiscordToken_SkipButton"), ConfigField: "discordToken", NextStep: "control_panel_channel", }, - "control_panel_channel": { ID: "control_panel_channel", - Title: "Stationeers Server UI", - HeaderTitle: "Discord Channel Setup (1/6)", - StepMessage: "Enter Discord Control Panel Channel ID", - PrimaryPlaceholderText: "Channel ID", - PrimaryLabel: "Control Panel Channel ID", + Title: localization.GetString("UIText_ControlPanelChannel_Title"), + HeaderTitle: localization.GetString("UIText_ControlPanelChannel_HeaderTitle"), + StepMessage: localization.GetString("UIText_ControlPanelChannel_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_ControlPanelChannel_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_ControlPanelChannel_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", + SubmitButtonText: localization.GetString("UIText_ControlPanelChannel_SubmitButton"), + SkipButtonText: localization.GetString("UIText_ControlPanelChannel_SkipButton"), ConfigField: "controlPanelChannelID", NextStep: "save_channel", }, - "save_channel": { ID: "save_channel", - Title: "Stationeers Server UI", - HeaderTitle: "Discord Channel Setup (2/6)", - StepMessage: "Enter Discord Save Channel ID", - PrimaryPlaceholderText: "Channel ID", - PrimaryLabel: "Save Channel ID", + Title: localization.GetString("UIText_SaveChannel_Title"), + HeaderTitle: localization.GetString("UIText_SaveChannel_HeaderTitle"), + StepMessage: localization.GetString("UIText_SaveChannel_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_SaveChannel_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_SaveChannel_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", + SubmitButtonText: localization.GetString("UIText_SaveChannel_SubmitButton"), + SkipButtonText: localization.GetString("UIText_SaveChannel_SkipButton"), ConfigField: "saveChannelID", NextStep: "log_channel", }, - "log_channel": { ID: "log_channel", - Title: "Stationeers Server UI", - HeaderTitle: "Discord Channel Setup (3/6)", - StepMessage: "Enter Discord Log Channel ID", - PrimaryPlaceholderText: "Channel ID", - PrimaryLabel: "Log Channel ID", + Title: localization.GetString("UIText_LogChannel_Title"), + HeaderTitle: localization.GetString("UIText_LogChannel_HeaderTitle"), + StepMessage: localization.GetString("UIText_LogChannel_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_LogChannel_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_LogChannel_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", + SubmitButtonText: localization.GetString("UIText_LogChannel_SubmitButton"), + SkipButtonText: localization.GetString("UIText_LogChannel_SkipButton"), ConfigField: "logChannelID", NextStep: "connection_list_channel", }, - "connection_list_channel": { ID: "connection_list_channel", - Title: "Stationeers Server UI", - HeaderTitle: "Discord Channel Setup (4/6)", - StepMessage: "Enter Discord Connection List Channel ID", - PrimaryPlaceholderText: "Channel ID", - PrimaryLabel: "Connection List Channel ID", + Title: localization.GetString("UIText_ConnectionListChannel_Title"), + HeaderTitle: localization.GetString("UIText_ConnectionListChannel_HeaderTitle"), + StepMessage: localization.GetString("UIText_ConnectionListChannel_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_ConnectionListChannel_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_ConnectionListChannel_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", + SubmitButtonText: localization.GetString("UIText_ConnectionListChannel_SubmitButton"), + SkipButtonText: localization.GetString("UIText_ConnectionListChannel_SkipButton"), ConfigField: "connectionListChannelID", NextStep: "status_channel", }, - "status_channel": { ID: "status_channel", - Title: "Stationeers Server UI", - HeaderTitle: "Discord Channel Setup (5/6)", - StepMessage: "Enter Discord Status Channel ID", - PrimaryPlaceholderText: "Channel ID", - PrimaryLabel: "Status Channel ID", + Title: localization.GetString("UIText_StatusChannel_Title"), + HeaderTitle: localization.GetString("UIText_StatusChannel_HeaderTitle"), + StepMessage: localization.GetString("UIText_StatusChannel_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_StatusChannel_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_StatusChannel_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", + SubmitButtonText: localization.GetString("UIText_StatusChannel_SubmitButton"), + SkipButtonText: localization.GetString("UIText_StatusChannel_SkipButton"), ConfigField: "statusChannelID", NextStep: "control_channel", }, - "control_channel": { ID: "control_channel", - Title: "Stationeers Server UI", - HeaderTitle: "Discord Channel Setup (6/6)", - StepMessage: "Enter Discord Control Channel ID", - PrimaryPlaceholderText: "Channel ID", - PrimaryLabel: "Control Channel ID", + Title: localization.GetString("UIText_ControlChannel_Title"), + HeaderTitle: localization.GetString("UIText_ControlChannel_HeaderTitle"), + StepMessage: localization.GetString("UIText_ControlChannel_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_ControlChannel_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_ControlChannel_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", + SubmitButtonText: localization.GetString("UIText_ControlChannel_SubmitButton"), + SkipButtonText: localization.GetString("UIText_ControlChannel_SkipButton"), ConfigField: "controlChannelID", NextStep: "network_config_choice", }, - "network_config_choice": { ID: "network_config_choice", - Title: "Stationeers Server UI", - HeaderTitle: "Network Configuration", - StepMessage: "Do you want to configure network settings? Enter 'yes' to configure or Skip to use defaults. Note: Network configuration is especially important on Linux servers.", - PrimaryPlaceholderText: "yes", - PrimaryLabel: "Configure Network", + Title: localization.GetString("UIText_NetworkConfigChoice_Title"), + HeaderTitle: localization.GetString("UIText_NetworkConfigChoice_HeaderTitle"), + StepMessage: localization.GetString("UIText_NetworkConfigChoice_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_NetworkConfigChoice_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_NetworkConfigChoice_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Continue", - SkipButtonText: "Skip (Use Defaults)", + SubmitButtonText: localization.GetString("UIText_NetworkConfigChoice_SubmitButton"), + SkipButtonText: localization.GetString("UIText_NetworkConfigChoice_SkipButton"), ConfigField: "", // No config field, just for branching NextStep: "game_port", // Default next step if they choose to configure // The actual next step will be determined by JS based on the answer @@ -283,133 +309,99 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { "game_port": { ID: "game_port", - Title: "Stationeers Server UI", - HeaderTitle: "Network Setup (1/6)", - StepMessage: "Enter the port number for game connections", - PrimaryPlaceholderText: "27016", - PrimaryLabel: "Game Port", + Title: localization.GetString("UIText_GamePort_Title"), + HeaderTitle: localization.GetString("UIText_GamePort_HeaderTitle"), + StepMessage: localization.GetString("UIText_GamePort_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_GamePort_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_GamePort_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", + SubmitButtonText: localization.GetString("UIText_GamePort_SubmitButton"), + SkipButtonText: localization.GetString("UIText_GamePort_SkipButton"), ConfigField: "GamePort", NextStep: "update_port", }, - "update_port": { ID: "update_port", - Title: "Stationeers Server UI", - HeaderTitle: "Network Setup (2/6)", - StepMessage: "Enter the port number for update connections", - PrimaryPlaceholderText: "27015", - PrimaryLabel: "Update Port", + Title: localization.GetString("UIText_UpdatePort_Title"), + HeaderTitle: localization.GetString("UIText_UpdatePort_HeaderTitle"), + StepMessage: localization.GetString("UIText_UpdatePort_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_UpdatePort_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_UpdatePort_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", + SubmitButtonText: localization.GetString("UIText_UpdatePort_SubmitButton"), + SkipButtonText: localization.GetString("UIText_UpdatePort_SkipButton"), ConfigField: "UpdatePort", NextStep: "upnp_enabled", }, - "upnp_enabled": { ID: "upnp_enabled", - Title: "Stationeers Server UI", - HeaderTitle: "Network Setup (3/6)", - StepMessage: "Enable UPnP? Enter 'yes' to enable or 'no' to disable.", - PrimaryPlaceholderText: "yes/no", - PrimaryLabel: "Enable UPnP", - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", - ConfigField: "UPNPEnabled", // We'll handle the boolean conversion in JS - NextStep: "server_visible", - }, - - "server_visible": { - ID: "server_visible", - Title: "Stationeers Server UI", - HeaderTitle: "Network Setup (4/6)", - StepMessage: "Make server visible in the Server list? Enter 'yes' to make visible or 'no' to hide.", - PrimaryPlaceholderText: "yes/no", - PrimaryLabel: "Server Visible", + Title: localization.GetString("UIText_UPnPEnabled_Title"), + HeaderTitle: localization.GetString("UIText_UPnPEnabled_HeaderTitle"), + StepMessage: localization.GetString("UIText_UPnPEnabled_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_UPnPEnabled_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_UPnPEnabled_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", - ConfigField: "ServerVisible", // We'll handle the boolean conversion in JS - NextStep: "use_steam_p2p", - }, - - "use_steam_p2p": { - ID: "use_steam_p2p", - Title: "Stationeers Server UI", - HeaderTitle: "Network Setup (5/6)", - StepMessage: "Use Steam P2P networking? Enter 'yes' to enable or 'no' to disable.", - PrimaryPlaceholderText: "yes/no", - PrimaryLabel: "Use Steam P2P", - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", - ConfigField: "UseSteamP2P", // We'll handle the boolean conversion in JS + SubmitButtonText: localization.GetString("UIText_UPnPEnabled_SubmitButton"), + SkipButtonText: localization.GetString("UIText_UPnPEnabled_SkipButton"), + ConfigField: "UPNPEnabled", NextStep: "local_ip_address", }, - "local_ip_address": { ID: "local_ip_address", - Title: "Stationeers Server UI", - HeaderTitle: "Network Setup (6/6)", - StepMessage: "Enter server's local IP address in format 0.0.0.0 (no CIDR notation)", - PrimaryPlaceholderText: "0.0.0.0", - PrimaryLabel: "Local IP Address", + Title: localization.GetString("UIText_LocalIPAddress_Title"), + HeaderTitle: localization.GetString("UIText_LocalIPAddress_HeaderTitle"), + StepMessage: localization.GetString("UIText_LocalIPAddress_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_LocalIPAddress_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_LocalIPAddress_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip", + SubmitButtonText: localization.GetString("UIText_LocalIPAddress_SubmitButton"), + SkipButtonText: localization.GetString("UIText_LocalIPAddress_SkipButton"), ConfigField: "LocalIpAddress", - NextStep: "admin_account", // Continue to admin account setup after network config + NextStep: "admin_account", }, "admin_account": { ID: "admin_account", - Title: "Stationeers Server UI", - HeaderTitle: "Admin Account Setup", - StepMessage: "Set up your admin account.", - PrimaryPlaceholderText: "Username", - PrimaryLabel: "Username", - SecondaryLabel: "Password", - SecondaryPlaceholderText: "Password", + Title: localization.GetString("UIText_AdminAccount_Title"), + HeaderTitle: localization.GetString("UIText_AdminAccount_HeaderTitle"), + StepMessage: localization.GetString("UIText_AdminAccount_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_AdminAccount_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_AdminAccount_PrimaryLabel"), + SecondaryLabel: localization.GetString("UIText_AdminAccount_SecondaryLabel"), + SecondaryPlaceholderText: localization.GetString("UIText_AdminAccount_SecondaryPlaceholder"), SecondaryLabelType: "password", - SubmitButtonText: "Save & Continue", - SkipButtonText: "Skip Authentication", - ConfigField: "", // Special handling for admin account - NextStep: "sscm_opt_in", + SubmitButtonText: localization.GetString("UIText_AdminAccount_SubmitButton"), + SkipButtonText: localization.GetString("UIText_AdminAccount_SkipButton"), + ConfigField: "", + NextStep: "finalize", }, - "sscm_opt_in": { - ID: "sscm_opt_in", - Title: "Stationeers Command Manager", - HeaderTitle: "Preview Feature", - StepMessage: "SSCM is a custom server plugin that allows you to execute commands directly from SSUI. It doesn't affect vanilla server functionality while giving you the ability to run commands from the SSUI console.", - PrimaryPlaceholderText: "yes", - PrimaryLabel: "Enable SSCM", + "sscm": { + ID: "sscm", + Title: localization.GetString("UIText_SSCM_Title"), + HeaderTitle: localization.GetString("UIText_SSCM_HeaderTitle"), + StepMessage: localization.GetString("UIText_SSCM_StepMessage"), + PrimaryPlaceholderText: localization.GetString("UIText_SSCM_PrimaryPlaceholder"), + PrimaryLabel: localization.GetString("UIText_SSCM_PrimaryLabel"), SecondaryLabel: "", SecondaryLabelType: "hidden", - SubmitButtonText: "Enable & Continue", - SkipButtonText: "Skip", + SubmitButtonText: localization.GetString("UIText_SSCM_SubmitButton"), + SkipButtonText: localization.GetString("UIText_SSCM_SkipButton"), ConfigField: "IsSSCMEnabled", NextStep: "finalize", }, "finalize": { - ID: "finalize", - Title: "Finalize Setup", - HeaderTitle: "", - StepMessage: "Ready to finalize? Your configuration has already been saved while you completed this setup. If you want to change any of the settings, you may click Return to Start and skip whatever you want to keep. Most options can also be changed on the config Tab in the UI.", - PrimaryLabel: "", - SecondaryLabel: "", - SecondaryLabelType: "hidden", - SubmitButtonText: "Return to Start", - SkipButtonText: "Skip Authentication", - NextStep: "welcome", // Return to first step if "Return to Setup" is clicked + ID: "finalize", + Title: localization.GetString("UIText_Finalize_Title"), + HeaderTitle: "", + StepMessage: localization.GetString("UIText_Finalize_StepMessage"), + PrimaryLabel: "", + SecondaryLabel: "", + SubmitButtonText: localization.GetString("UIText_Finalize_SubmitButton"), + SkipButtonText: localization.GetString("UIText_Finalize_SkipButton"), + NextStep: "welcome", // Return to first step if "Return to Setup" is clicked }, } @@ -417,7 +409,7 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { IsFirstTimeSetup: config.IsFirstTimeSetup, Path: path, Step: stepID, - FooterText: "Need help? Check the Stationeers Server UI Github Wiki.", + FooterText: localization.GetString("UIText_FooterText"), } switch { @@ -444,8 +436,8 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { data.NextStep = step.NextStep data.PrimaryPlaceholderText = step.PrimaryPlaceholderText data.SecondaryPlaceholderText = step.SecondaryPlaceholderText - if stepID == "sscm_opt_in" { - data.FooterText = "Opt in to SSCM for the most powerful Stationeers server management! This license protects this unique feature, ensuring it stays exclusive to SSUI users. Check the terms in the SSUI GitHub Wiki. Don’t be worried, the license simply protects SSCM’s integrity and its integration with SSUI." + if stepID == "sscm" { + data.FooterText = localization.GetString("UIText_SSCM_FooterText") } } else { // Default to welcome page if step is invalid @@ -464,25 +456,25 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { } case path == "/changeuser": - data.Title = "Stationeers Server UI" - data.HeaderTitle = "Manage Users" - data.PrimaryLabel = "Username to Add/Update" - data.SecondaryLabel = "New Password" - data.SecondaryPlaceholderText = "Password" + data.Title = localization.GetString("UIText_ChangeUser_Title") + data.HeaderTitle = localization.GetString("UIText_ChangeUser_HeaderTitle") + data.PrimaryLabel = localization.GetString("UIText_ChangeUser_PrimaryLabel") + data.SecondaryLabel = localization.GetString("UIText_ChangeUser_SecondaryLabel") + data.SecondaryPlaceholderText = localization.GetString("UIText_ChangeUser_SecondaryPlaceholder") data.SecondaryLabelType = "password" - data.SubmitButtonText = "Add/Update User" + data.SubmitButtonText = localization.GetString("UIText_ChangeUser_SubmitButton") data.Mode = "changeuser" data.ShowExtraButtons = false default: - data.Title = "Stationeers Server UI" - data.HeaderTitle = "" - data.PrimaryLabel = "Username" - data.SecondaryLabel = "Password" - data.PrimaryPlaceholderText = "Enter Username" - data.SecondaryPlaceholderText = "Enter Password" + data.Title = localization.GetString("UIText_Login_Title") + data.HeaderTitle = localization.GetString("UIText_Login_HeaderTitle") + data.PrimaryLabel = localization.GetString("UIText_Login_PrimaryLabel") + data.SecondaryLabel = localization.GetString("UIText_Login_SecondaryLabel") + data.PrimaryPlaceholderText = localization.GetString("UIText_Login_PrimaryPlaceholder") + data.SecondaryPlaceholderText = localization.GetString("UIText_Login_SecondaryPlaceholder") data.SecondaryLabelType = "password" - data.SubmitButtonText = "Login" + data.SubmitButtonText = localization.GetString("UIText_Login_SubmitButton") data.Mode = "login" data.ShowExtraButtons = false } diff --git a/src/web/commands.go b/src/web/commands.go index 87baa27b..30523f8b 100644 --- a/src/web/commands.go +++ b/src/web/commands.go @@ -4,8 +4,8 @@ import ( "encoding/json" "net/http" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/commandmgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/commandmgr" ) type CommandRequest struct { diff --git a/src/web/configpage.go b/src/web/configpage.go new file mode 100644 index 00000000..f66e65ef --- /dev/null +++ b/src/web/configpage.go @@ -0,0 +1,265 @@ +package web + +import ( + "fmt" + "io/fs" + "net/http" + "text/template" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" +) + +func ServeConfigPage(w http.ResponseWriter, r *http.Request) { + + htmlFS, err := fs.Sub(config.V1UIFS, "UIMod/onboard_bundled/ui") + if err != nil { + http.Error(w, "Error accessing Virt FS: "+err.Error(), http.StatusInternalServerError) + return + } + + tmpl, err := template.ParseFS(htmlFS, "config.html") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Core.Error("failed to serve config.html") + return + } + + // Determine selected attributes for boolean fields + upnpTrueSelected := "" + upnpFalseSelected := "" + if config.UPNPEnabled { + upnpTrueSelected = "selected" + } else { + upnpFalseSelected = "selected" + } + + discordTrueSelected := "" + discordFalseSelected := "" + if config.IsDiscordEnabled { + discordTrueSelected = "selected" + } else { + discordFalseSelected = "selected" + } + + autoSaveTrueSelected := "" + autoSaveFalseSelected := "" + if config.AutoSave { + autoSaveTrueSelected = "selected" + } else { + autoSaveFalseSelected = "selected" + } + + autoPauseTrueSelected := "" + autoPauseFalseSelected := "" + if config.AutoPauseServer { + autoPauseTrueSelected = "selected" + } else { + autoPauseFalseSelected = "selected" + } + + startLocalTrueSelected := "" + startLocalFalseSelected := "" + if config.StartLocalHost { + startLocalTrueSelected = "selected" + } else { + startLocalFalseSelected = "selected" + } + + serverVisibleTrueSelected := "" + serverVisibleFalseSelected := "" + if config.ServerVisible { + serverVisibleTrueSelected = "selected" + } else { + serverVisibleFalseSelected = "selected" + } + + isNewTerrainAndSaveSystemTrueSelected := "" + isNewTerrainAndSaveSystemFalseSelected := "" + + if config.IsNewTerrainAndSaveSystem { + isNewTerrainAndSaveSystemTrueSelected = "selected" + } else { + isNewTerrainAndSaveSystemFalseSelected = "selected" + } + + autoStartServerTrueSelected := "" + autoStartServerFalseSelected := "" + if config.AutoStartServerOnStartup { + autoStartServerTrueSelected = "selected" + } else { + autoStartServerFalseSelected = "selected" + } + + steamP2PTrueSelected := "" + steamP2PFalseSelected := "" + if config.UseSteamP2P { + steamP2PTrueSelected = "selected" + } else { + steamP2PFalseSelected = "selected" + } + + data := ConfigTemplateData{ + // Config values + DiscordToken: config.DiscordToken, + ControlChannelID: config.ControlChannelID, + StatusChannelID: config.StatusChannelID, + ConnectionListChannelID: config.ConnectionListChannelID, + LogChannelID: config.LogChannelID, + SaveChannelID: config.SaveChannelID, + ControlPanelChannelID: config.ControlPanelChannelID, + BlackListFilePath: config.BlackListFilePath, + ErrorChannelID: config.ErrorChannelID, + IsDiscordEnabled: fmt.Sprintf("%v", config.IsDiscordEnabled), + IsDiscordEnabledTrueSelected: discordTrueSelected, + IsDiscordEnabledFalseSelected: discordFalseSelected, + GameBranch: config.GameBranch, + Difficulty: config.Difficulty, + StartCondition: config.StartCondition, + StartLocation: config.StartLocation, + ServerName: config.ServerName, + SaveInfo: config.SaveInfo, + ServerMaxPlayers: config.ServerMaxPlayers, + ServerPassword: config.ServerPassword, + ServerAuthSecret: config.ServerAuthSecret, + AdminPassword: config.AdminPassword, + GamePort: config.GamePort, + UpdatePort: config.UpdatePort, + UPNPEnabled: fmt.Sprintf("%v", config.UPNPEnabled), + UPNPEnabledTrueSelected: upnpTrueSelected, + UPNPEnabledFalseSelected: upnpFalseSelected, + AutoSave: fmt.Sprintf("%v", config.AutoSave), + AutoSaveTrueSelected: autoSaveTrueSelected, + AutoSaveFalseSelected: autoSaveFalseSelected, + SaveInterval: config.SaveInterval, + AutoPauseServer: fmt.Sprintf("%v", config.AutoPauseServer), + AutoPauseServerTrueSelected: autoPauseTrueSelected, + AutoPauseServerFalseSelected: autoPauseFalseSelected, + LocalIpAddress: config.LocalIpAddress, + StartLocalHost: fmt.Sprintf("%v", config.StartLocalHost), + StartLocalHostTrueSelected: startLocalTrueSelected, + StartLocalHostFalseSelected: startLocalFalseSelected, + ServerVisible: fmt.Sprintf("%v", config.ServerVisible), + ServerVisibleTrueSelected: serverVisibleTrueSelected, + ServerVisibleFalseSelected: serverVisibleFalseSelected, + UseSteamP2P: fmt.Sprintf("%v", config.UseSteamP2P), + UseSteamP2PTrueSelected: steamP2PTrueSelected, + UseSteamP2PFalseSelected: steamP2PFalseSelected, + ExePath: config.ExePath, + AdditionalParams: config.AdditionalParams, + AutoRestartServerTimer: config.AutoRestartServerTimer, + IsNewTerrainAndSaveSystem: fmt.Sprintf("%v", config.IsNewTerrainAndSaveSystem), + IsNewTerrainAndSaveSystemTrueSelected: isNewTerrainAndSaveSystemTrueSelected, + IsNewTerrainAndSaveSystemFalseSelected: isNewTerrainAndSaveSystemFalseSelected, + AutoStartServerOnStartup: fmt.Sprintf("%v", config.AutoStartServerOnStartup), + AutoStartServerOnStartupTrueSelected: autoStartServerTrueSelected, + AutoStartServerOnStartupFalseSelected: autoStartServerFalseSelected, + + // Localized UI text + UIText_ServerConfig: localization.GetString("UIText_ServerConfig"), + UIText_DiscordIntegration: localization.GetString("UIText_DiscordIntegration"), + UIText_DetectionManager: localization.GetString("UIText_DetectionManager"), + UIText_ConfigurationWizard: localization.GetString("UIText_ConfigurationWizard"), + UIText_PleaseSelectSection: localization.GetString("UIText_PleaseSelectSection"), + UIText_UseWizardAlternative: localization.GetString("UIText_UseWizardAlternative"), + UIText_BasicSettings: localization.GetString("UIText_BasicSettings"), + UIText_NetworkSettings: localization.GetString("UIText_NetworkSettings"), + UIText_AdvancedSettings: localization.GetString("UIText_AdvancedSettings"), + UIText_BetaSettings: localization.GetString("UIText_BetaSettings"), + UIText_BasicServerSettings: localization.GetString("UIText_BasicServerSettings"), + + UIText_ServerName: localization.GetString("UIText_ServerName"), + UIText_ServerNameInfo: localization.GetString("UIText_ServerNameInfo"), + UIText_SaveFileName: localization.GetString("UIText_SaveFileName"), + UIText_SaveFileNameInfo: localization.GetString("UIText_SaveFileNameInfo"), + UIText_MaxPlayers: localization.GetString("UIText_MaxPlayers"), + UIText_MaxPlayersInfo: localization.GetString("UIText_MaxPlayersInfo"), + UIText_ServerPassword: localization.GetString("UIText_ServerPassword"), + UIText_ServerPasswordInfo: localization.GetString("UIText_ServerPasswordInfo"), + UIText_AdminPassword: localization.GetString("UIText_AdminPassword"), + UIText_AdminPasswordInfo: localization.GetString("UIText_AdminPasswordInfo"), + UIText_AutoSave: localization.GetString("UIText_AutoSave"), + UIText_AutoSaveInfo: localization.GetString("UIText_AutoSaveInfo"), + UIText_SaveInterval: localization.GetString("UIText_SaveInterval"), + UIText_SaveIntervalInfo: localization.GetString("UIText_SaveIntervalInfo"), + UIText_AutoPauseServer: localization.GetString("UIText_AutoPauseServer"), + UIText_AutoPauseServerInfo: localization.GetString("UIText_AutoPauseServerInfo"), + UIText_NetworkConfiguration: localization.GetString("UIText_NetworkConfiguration"), + UIText_GamePort: localization.GetString("UIText_GamePort"), + UIText_GamePortInfo: localization.GetString("UIText_GamePortInfo"), + UIText_UpdatePort: localization.GetString("UIText_UpdatePort"), + UIText_UpdatePortInfo: localization.GetString("UIText_UpdatePortInfo"), + UIText_UPNPEnabled: localization.GetString("UIText_UPNPEnabled"), + UIText_UPNPEnabledInfo: localization.GetString("UIText_UPNPEnabledInfo"), + UIText_LocalIpAddress: localization.GetString("UIText_LocalIpAddress"), + UIText_LocalIpAddressInfo: localization.GetString("UIText_LocalIpAddressInfo"), + UIText_StartLocalHost: localization.GetString("UIText_StartLocalHost"), + UIText_StartLocalHostInfo: localization.GetString("UIText_StartLocalHostInfo"), + UIText_ServerVisible: localization.GetString("UIText_ServerVisible"), + UIText_ServerVisibleInfo: localization.GetString("UIText_ServerVisibleInfo"), + UIText_UseSteamP2P: localization.GetString("UIText_UseSteamP2P"), + UIText_UseSteamP2PInfo: localization.GetString("UIText_UseSteamP2PInfo"), + UIText_AdvancedConfiguration: localization.GetString("UIText_AdvancedConfiguration"), + UIText_ServerAuthSecret: localization.GetString("UIText_ServerAuthSecret"), + UIText_ServerAuthSecretInfo: localization.GetString("UIText_ServerAuthSecretInfo"), + UIText_ServerExePath: localization.GetString("UIText_ServerExePath"), + UIText_ServerExePathInfo: localization.GetString("UIText_ServerExePathInfo"), + UIText_ServerExePathInfo2: localization.GetString("UIText_ServerExePathInfo2"), + UIText_AdditionalParams: localization.GetString("UIText_AdditionalParams"), + UIText_AdditionalParamsInfo: localization.GetString("UIText_AdditionalParamsInfo"), + UIText_AutoRestartServerTimer: localization.GetString("UIText_AutoRestartServerTimer"), + UIText_AutoRestartServerTimerInfo: localization.GetString("UIText_AutoRestartServerTimerInfo"), + UIText_GameBranch: localization.GetString("UIText_GameBranch"), + UIText_GameBranchInfo: localization.GetString("UIText_GameBranchInfo"), + UIText_BetaOnlySettings: localization.GetString("UIText_BetaOnlySettings"), + UIText_BetaWarning: localization.GetString("UIText_BetaWarning"), + UIText_UseNewTerrainAndSave: localization.GetString("UIText_UseNewTerrainAndSave"), + UIText_UseNewTerrainAndSaveInfo: localization.GetString("UIText_UseNewTerrainAndSaveInfo"), + UIText_Difficulty: localization.GetString("UIText_Difficulty"), + UIText_DifficultyInfo: localization.GetString("UIText_DifficultyInfo"), + UIText_StartCondition: localization.GetString("UIText_StartCondition"), + UIText_StartConditionInfo: localization.GetString("UIText_StartConditionInfo"), + UIText_StartLocation: localization.GetString("UIText_StartLocation"), + UIText_StartLocationInfo: localization.GetString("UIText_StartLocationInfo"), + UIText_AutoStartServerOnStartup: localization.GetString("UIText_AutoStartServerOnStartup"), + UIText_AutoStartServerOnStartupInfo: localization.GetString("UIText_AutoStartServerOnStartupInfo"), + + UIText_DiscordIntegrationTitle: localization.GetString("UIText_DiscordIntegrationTitle"), + UIText_DiscordBotToken: localization.GetString("UIText_DiscordBotToken"), + UIText_DiscordBotTokenInfo: localization.GetString("UIText_DiscordBotTokenInfo"), + UIText_ChannelConfiguration: localization.GetString("UIText_ChannelConfiguration"), + UIText_AdminCommandChannel: localization.GetString("UIText_AdminCommandChannel"), + UIText_AdminCommandChannelInfo: localization.GetString("UIText_AdminCommandChannelInfo"), + UIText_ControlPanelChannel: localization.GetString("UIText_ControlPanelChannel"), + UIText_ControlPanelChannelInfo: localization.GetString("UIText_ControlPanelChannelInfo"), + 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_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"), + UIText_BannedPlayersListPathInfo: localization.GetString("UIText_BannedPlayersListPathInfo"), + UIText_DiscordIntegrationBenefits: localization.GetString("UIText_DiscordIntegrationBenefits"), + UIText_DiscordBenefit1: localization.GetString("UIText_DiscordBenefit1"), + UIText_DiscordBenefit2: localization.GetString("UIText_DiscordBenefit2"), + UIText_DiscordBenefit3: localization.GetString("UIText_DiscordBenefit3"), + UIText_DiscordBenefit4: localization.GetString("UIText_DiscordBenefit4"), + UIText_DiscordBenefit5: localization.GetString("UIText_DiscordBenefit5"), + UIText_DiscordSetupInstructions: localization.GetString("UIText_DiscordSetupInstructions"), + + UIText_CopyrightConfig1: localization.GetString("UIText_Copyright1"), + UIText_CopyrightConfig2: localization.GetString("UIText_Copyright2"), + } + + err = tmpl.Execute(w, data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/src/web/detectionmanagerpage.go b/src/web/detectionmanagerpage.go new file mode 100644 index 00000000..74f8a642 --- /dev/null +++ b/src/web/detectionmanagerpage.go @@ -0,0 +1,34 @@ +package web + +import ( + "fmt" + "io" + "io/fs" + "net/http" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" +) + +func ServeDetectionManager(w http.ResponseWriter, r *http.Request) { + detectionmanagerFS, err := fs.Sub(config.V1UIFS, "UIMod/onboard_bundled/detectionmanager") + if err != nil { + http.Error(w, "Error accessing Virt FS: "+err.Error(), http.StatusInternalServerError) + return + } + + htmlFile, err := detectionmanagerFS.Open("detectionmanager.html") + if err != nil { + http.Error(w, fmt.Sprintf("Error reading detectionmanager.html: %v", err), http.StatusInternalServerError) + return + } + defer htmlFile.Close() + + htmlContent, err := io.ReadAll(htmlFile) + if err != nil { + http.Error(w, fmt.Sprintf("Error reading detectionmanager.html content: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write(htmlContent) +} diff --git a/src/web/http.go b/src/web/http.go index a075fcb6..38f5f4ee 100644 --- a/src/web/http.go +++ b/src/web/http.go @@ -7,181 +7,17 @@ import ( "net/http" "os" "strings" - "text/template" + "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/commandmgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/gamemgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/ssestream" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/ssestream" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/commandmgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" ) -// TemplateData holds data to be passed to templates -type TemplateData struct { - Version string - Branch string -} - -func ServeIndex(w http.ResponseWriter, r *http.Request) { - tmpl, err := template.ParseFiles(config.IndexHtmlPath) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - data := TemplateData{ - Version: config.Version, - Branch: config.Branch, - } - if data.Version == "" { - data.Version = "unknown" - } - if data.Branch == "" { - data.Branch = "unknown" - } - - err = tmpl.Execute(w, data) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -func ServeDetectionManager(w http.ResponseWriter, r *http.Request) { - - htmlFile, err := os.ReadFile(config.DetectionManagerHtmlPath) - if err != nil { - http.Error(w, fmt.Sprintf("Error reading detectionmanager.html: %v", err), http.StatusInternalServerError) - return - } - - htmlContent := string(htmlFile) - - fmt.Fprint(w, htmlContent) -} - -func ServeConfigPage(w http.ResponseWriter, r *http.Request) { - - htmlFile, err := os.ReadFile(config.ConfigHtmlPath) - if err != nil { - http.Error(w, fmt.Sprintf("Error reading config.html: %v", err), http.StatusInternalServerError) - return - } - - htmlContent := string(htmlFile) - - // Determine selected attributes for boolean fields - upnpTrueSelected := "" - upnpFalseSelected := "" - if config.UPNPEnabled { - upnpTrueSelected = "selected" - } else { - upnpFalseSelected = "selected" - } - - discordTrueSelected := "" - discordFalseSelected := "" - if config.IsDiscordEnabled { - discordTrueSelected = "selected" - } else { - discordFalseSelected = "selected" - } - - autoSaveTrueSelected := "" - autoSaveFalseSelected := "" - if config.AutoSave { - autoSaveTrueSelected = "selected" - } else { - autoSaveFalseSelected = "selected" - } - - autoPauseTrueSelected := "" - autoPauseFalseSelected := "" - if config.AutoPauseServer { - autoPauseTrueSelected = "selected" - } else { - autoPauseFalseSelected = "selected" - } - - startLocalTrueSelected := "" - startLocalFalseSelected := "" - if config.StartLocalHost { - startLocalTrueSelected = "selected" - } else { - startLocalFalseSelected = "selected" - } - - serverVisibleTrueSelected := "" - serverVisibleFalseSelected := "" - if config.ServerVisible { - serverVisibleTrueSelected = "selected" - } else { - serverVisibleFalseSelected = "selected" - } - - steamP2PTrueSelected := "" - steamP2PFalseSelected := "" - if config.UseSteamP2P { - steamP2PTrueSelected = "selected" - } else { - steamP2PFalseSelected = "selected" - } - - // Replace placeholders in the HTML with actual config values - replacements := map[string]string{ - "{{discordToken}}": config.DiscordToken, - "{{controlChannelID}}": config.ControlChannelID, - "{{statusChannelID}}": config.StatusChannelID, - "{{connectionListChannelID}}": config.ConnectionListChannelID, - "{{logChannelID}}": config.LogChannelID, - "{{saveChannelID}}": config.SaveChannelID, - "{{controlPanelChannelID}}": config.ControlPanelChannelID, - "{{blackListFilePath}}": config.BlackListFilePath, - "{{errorChannelID}}": config.ErrorChannelID, - "{{isDiscordEnabled}}": fmt.Sprintf("%v", config.IsDiscordEnabled), - "{{IsDiscordEnabledTrueSelected}}": discordTrueSelected, - "{{IsDiscordEnabledFalseSelected}}": discordFalseSelected, - "{{gameBranch}}": config.GameBranch, - "{{ServerName}}": config.ServerName, - "{{SaveInfo}}": config.SaveInfo, - "{{ServerMaxPlayers}}": config.ServerMaxPlayers, - "{{ServerPassword}}": config.ServerPassword, - "{{ServerAuthSecret}}": config.ServerAuthSecret, - "{{AdminPassword}}": config.AdminPassword, - "{{GamePort}}": config.GamePort, - "{{UpdatePort}}": config.UpdatePort, - "{{UPNPEnabled}}": fmt.Sprintf("%v", config.UPNPEnabled), - "{{UPNPEnabledTrueSelected}}": upnpTrueSelected, - "{{UPNPEnabledFalseSelected}}": upnpFalseSelected, - "{{AutoSave}}": fmt.Sprintf("%v", config.AutoSave), - "{{AutoSaveTrueSelected}}": autoSaveTrueSelected, - "{{AutoSaveFalseSelected}}": autoSaveFalseSelected, - "{{SaveInterval}}": config.SaveInterval, - "{{AutoPauseServer}}": fmt.Sprintf("%v", config.AutoPauseServer), - "{{AutoPauseServerTrueSelected}}": autoPauseTrueSelected, - "{{AutoPauseServerFalseSelected}}": autoPauseFalseSelected, - "{{LocalIpAddress}}": config.LocalIpAddress, - "{{StartLocalHost}}": fmt.Sprintf("%v", config.StartLocalHost), - "{{StartLocalHostTrueSelected}}": startLocalTrueSelected, - "{{StartLocalHostFalseSelected}}": startLocalFalseSelected, - "{{ServerVisible}}": fmt.Sprintf("%v", config.ServerVisible), - "{{ServerVisibleTrueSelected}}": serverVisibleTrueSelected, - "{{ServerVisibleFalseSelected}}": serverVisibleFalseSelected, - "{{UseSteamP2P}}": fmt.Sprintf("%v", config.UseSteamP2P), - "{{UseSteamP2PTrueSelected}}": steamP2PTrueSelected, - "{{UseSteamP2PFalseSelected}}": steamP2PFalseSelected, - "{{ExePath}}": config.ExePath, - "{{AdditionalParams}}": config.AdditionalParams, - "{{AutoRestartServerTimer}}": config.AutoRestartServerTimer, - } - - for placeholder, value := range replacements { - htmlContent = strings.ReplaceAll(htmlContent, placeholder, value) - } - - fmt.Fprint(w, htmlContent) -} - // StartServer HTTP handler func StartServer(w http.ResponseWriter, r *http.Request) { logger.Web.Debug("Received start request from API") @@ -190,7 +26,7 @@ func StartServer(w http.ResponseWriter, r *http.Request) { logger.Web.Core("Error starting server: " + err.Error()) return } - fmt.Fprint(w, "Server started.") + fmt.Fprint(w, localization.GetString("BackendText_ServerStarted")) logger.Web.Core("Server started.") } @@ -199,7 +35,7 @@ func StopServer(w http.ResponseWriter, r *http.Request) { logger.Web.Debug("Received stop request from API") if err := gamemgr.InternalStopServer(); err != nil { if err.Error() == "server not running" { - fmt.Fprint(w, "Server was not running or was already stopped") + fmt.Fprint(w, localization.GetString("BackendText_ServerNotRunningOrAlreadyStopped")) logger.Web.Core("Server not running or was already stopped") return } @@ -207,7 +43,7 @@ func StopServer(w http.ResponseWriter, r *http.Request) { logger.Web.Core("Error stopping server: " + err.Error()) return } - fmt.Fprint(w, "Server stopped.") + fmt.Fprint(w, localization.GetString("BackendText_ServerStopped")) logger.Web.Core("Server stopped.") } @@ -244,21 +80,6 @@ func StartDetectionEventStream() http.HandlerFunc { return ssestream.EventStreamManager.CreateStreamHandler("Event") } -func ServeTwoBoxCss(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/css") - http.ServeFile(w, r, config.UIModFolder+"twoboxform/twoboxform.css") -} - -func ServeTwoBoxJs(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/javascript") - http.ServeFile(w, r, config.UIModFolder+"twoboxform/twoboxform.js") -} - -func ServeSSCMJs(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/javascript") - http.ServeFile(w, r, config.SSCMWebDir+"sscm.js") -} - // 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) { @@ -315,3 +136,39 @@ func HandleIsSSCMEnabled(w http.ResponseWriter, r *http.Request) { // Success: return 200 OK w.WriteHeader(http.StatusOK) } + +var lastSteamCMDExecution time.Time // last time SteamCMD was executed via API. + +// run SteamCMD from API, but only allow once every 5 minutes to "kinda" prevent concurrent executions although that woluldnt hurn. +// If the user has a 5mbit connection, I cannot help them anyways. +func HandleRunSteamCMD(w http.ResponseWriter, r *http.Request) { + const rateLimitDuration = 30 * time.Second + + // Only allow GET requests + if r.Method != http.MethodGet { + http.Error(w, "Only GET requests are allowed", http.StatusMethodNotAllowed) + return + } + + // Check rate limit + if time.Since(lastSteamCMDExecution) < rateLimitDuration { + json.NewEncoder(w).Encode(map[string]string{"statuscode": "200", "status": "Rejected", "message": "Slow down, you just called SteamCMD.", "advanced": "Use SSUICLI or restart SSUI to run SteamCMD repeatedly without limit."}) + return + } + + if gamemgr.InternalIsServerRunning() { + logger.Core.Warn("Server is running, stopping server first...") + gamemgr.InternalStopServer() + time.Sleep(10000 * time.Millisecond) + } + logger.Core.Info("Running SteamCMD") + setup.InstallAndRunSteamCMD() + + // Update last execution time + lastSteamCMDExecution = time.Now() + + // Success: return 202 Accepted and JSON + w.WriteHeader(http.StatusAccepted) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"statuscode": "202", "status": "Accepted", "message": "SteamCMD ran successfully."}) +} diff --git a/src/web/indexpage.go b/src/web/indexpage.go new file mode 100644 index 00000000..1741493d --- /dev/null +++ b/src/web/indexpage.go @@ -0,0 +1,54 @@ +package web + +import ( + "io/fs" + "net/http" + "text/template" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" +) + +func ServeIndex(w http.ResponseWriter, r *http.Request) { + htmlFS, err := fs.Sub(config.V1UIFS, "UIMod/onboard_bundled/ui") + if err != nil { + http.Error(w, "Error accessing Virt FS: "+err.Error(), http.StatusInternalServerError) + return + } + + tmpl, err := template.ParseFS(htmlFS, "index.html") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Core.Error("failed to serve v1 Index.html") + return + } + + data := IndexTemplateData{ + Version: config.Version, + Branch: config.Branch, + UIText_StartButton: localization.GetString("UIText_StartButton"), + UIText_StopButton: localization.GetString("UIText_StopButton"), + UIText_Settings: localization.GetString("UIText_Settings"), + UIText_Update_SteamCMD: localization.GetString("UIText_Update_SteamCMD"), + UIText_Console: localization.GetString("UIText_Console"), + UIText_Detection_Events: localization.GetString("UIText_Detection_Events"), + UIText_Backup_Manager: localization.GetString("UIText_Backup_Manager"), + UIText_Discord_Info: localization.GetString("UIText_Discord_Info"), + UIText_API_Info: localization.GetString("UIText_API_Info"), + UIText_Copyright1: localization.GetString("UIText_Copyright1"), + UIText_Copyright2: localization.GetString("UIText_Copyright2"), + } + if data.Version == "" { + data.Version = "unknown" + } + if data.Branch == "" { + data.Branch = "unknown" + } + + err = tmpl.Execute(w, data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/src/web/login.go b/src/web/login.go index 030eb284..84883b1a 100644 --- a/src/web/login.go +++ b/src/web/login.go @@ -10,10 +10,9 @@ import ( "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/configchanger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/loader" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/security" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/security" ) var setupReminderCount = 0 // to limit the number of setup reminders shown to the user @@ -192,7 +191,7 @@ func RegisterUserHandler(w http.ResponseWriter, r *http.Request) { existingConfig.Users[creds.Username] = hashedPassword // Persist the updated config - if err := configchanger.SaveConfig(existingConfig); err != nil { + if err := loader.SaveConfig(existingConfig); err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error - Failed to save config"}) @@ -214,7 +213,7 @@ func SetupFinalizeHandler(w http.ResponseWriter, r *http.Request) { if len(config.Users) == 0 { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]string{"error": "Bad Request - No users registered - cannot finalize setup at this time"}) + json.NewEncoder(w).Encode(map[string]string{"error": "No users registered - cannot finalize setup at this time. You should really enable authentication - or click 'Skip authentication'"}) return } @@ -235,7 +234,7 @@ func SetupFinalizeHandler(w http.ResponseWriter, r *http.Request) { newConfig.AuthEnabled = &isTrue // Set the pointer to true // Save the updated config - err = configchanger.SaveConfig(newConfig) + err = loader.SaveConfig(newConfig) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) @@ -250,5 +249,5 @@ func SetupFinalizeHandler(w http.ResponseWriter, r *http.Request) { "message": "Setup finalized successfully", "restart_hint": "You will be redirected to the login page...", }) - loader.ReloadConfig() + loader.ReloadBackend() } diff --git a/src/web/start.go b/src/web/start.go index 36aed086..d2e5c93e 100644 --- a/src/web/start.go +++ b/src/web/start.go @@ -2,16 +2,18 @@ package web import ( + "io/fs" "net/http" "net/http/pprof" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/backupmgr" + terminal "github.com/JacksonTheMaster/StationeersServerUI/v5/src/cli" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/configchanger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/detectionmgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config/configchanger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/security" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/security" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/backupmgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/detectionmgr" ) func StartWebServer(wg *sync.WaitGroup) { @@ -21,17 +23,18 @@ func StartWebServer(wg *sync.WaitGroup) { mux := http.NewServeMux() // Use a mux to apply middleware globally // Unprotected auth routes - mux.HandleFunc("/twoboxform/twoboxform.js", ServeTwoBoxJs) - mux.HandleFunc("/twoboxform/twoboxform.css", ServeTwoBoxCss) - mux.HandleFunc("/sscm/sscm.js", ServeSSCMJs) + twoboxformAssetsFS, _ := fs.Sub(config.GetV1UIFS(), "UIMod/onboard_bundled/twoboxform") + mux.Handle("/twoboxform/", http.StripPrefix("/twoboxform/", http.FileServer(http.FS(twoboxformAssetsFS)))) mux.HandleFunc("/auth/login", LoginHandler) // Token issuer mux.HandleFunc("/auth/logout", LogoutHandler) mux.HandleFunc("/login", ServeTwoBoxFormTemplate) // Protected routes (wrapped with middleware) protectedMux := http.NewServeMux() - fs := http.FileServer(http.Dir(config.UIModFolder + "/assets")) - protectedMux.Handle("/static/", http.StripPrefix("/static/", fs)) + + legacyAssetsFS, _ := fs.Sub(config.GetV1UIFS(), "UIMod/onboard_bundled/assets") + protectedMux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(legacyAssetsFS)))) + protectedMux.HandleFunc("/config", ServeConfigPage) protectedMux.HandleFunc("/detectionmanager", ServeDetectionManager) protectedMux.HandleFunc("/", ServeIndex) @@ -57,6 +60,7 @@ func StartWebServer(wg *sync.WaitGroup) { protectedMux.HandleFunc("/api/v2/saveconfig", configchanger.SaveConfigRestful) protectedMux.HandleFunc("/api/v2/SSCM/run", HandleCommand) // Command execution via SSCM (needs to be enable, config.IsSSCMEnabled) protectedMux.HandleFunc("/api/v2/SSCM/enabled", HandleIsSSCMEnabled) // Check if SSCM is enabled + protectedMux.HandleFunc("/api/v2/steamcmd/run", HandleRunSteamCMD) // Run SteamCMD // Custom Detections protectedMux.HandleFunc("/api/v2/custom-detections", detectionmgr.HandleCustomDetection) @@ -77,12 +81,9 @@ func StartWebServer(wg *sync.WaitGroup) { wg.Add(1) go func() { defer wg.Done() - logger.Web.Info("Starting the HTTP server on port 8443...") - logger.Web.Info("UI available at: https://0.0.0.0:8443 or https://localhost:8443") + terminal.PrintStartupMessage() if config.IsFirstTimeSetup { - logger.Web.Error("For first-time setup, visit the UI to configure a user or skip authentication.") - logger.Web.Warn("Fill the Username and Password fields, then click Register User and when done Finalize Setup.") - logger.Web.Warn("For more details, check the GitHub Wiki: https://github.com/JacksonTheMaster/StationeersServerUI/v5/wiki") + terminal.PrintFirstTimeSetupMessage() } // Ensure TLS certs are ready if err := security.EnsureTLSCerts(); err != nil { diff --git a/src/web/templatevars.go b/src/web/templatevars.go new file mode 100644 index 00000000..5b02f10e --- /dev/null +++ b/src/web/templatevars.go @@ -0,0 +1,176 @@ +package web + +// TemplateData holds data to be passed to templates +type IndexTemplateData struct { + Version string + Branch string + UIText_StartButton string + UIText_StopButton string + UIText_Settings string + UIText_Update_SteamCMD string + UIText_Console string + UIText_Detection_Events string + UIText_Backup_Manager string + UIText_Discord_Info string + UIText_API_Info string + UIText_Copyright1 string + UIText_Copyright2 string +} + +// ConfigTemplateData holds data for the config page template +type ConfigTemplateData struct { + // Config values + DiscordToken string + ControlChannelID string + StatusChannelID string + ConnectionListChannelID string + LogChannelID string + SaveChannelID string + ControlPanelChannelID string + BlackListFilePath string + ErrorChannelID string + IsDiscordEnabled string + IsDiscordEnabledTrueSelected string + IsDiscordEnabledFalseSelected string + GameBranch string + Difficulty string + StartCondition string + StartLocation string + ServerName string + SaveInfo string + ServerMaxPlayers string + ServerPassword string + ServerAuthSecret string + AdminPassword string + GamePort string + UpdatePort string + UPNPEnabled string + UPNPEnabledTrueSelected string + UPNPEnabledFalseSelected string + AutoSave string + AutoSaveTrueSelected string + AutoSaveFalseSelected string + SaveInterval string + AutoPauseServer string + AutoPauseServerTrueSelected string + AutoPauseServerFalseSelected string + LocalIpAddress string + StartLocalHost string + StartLocalHostTrueSelected string + StartLocalHostFalseSelected string + ServerVisible string + ServerVisibleTrueSelected string + ServerVisibleFalseSelected string + UseSteamP2P string + UseSteamP2PTrueSelected string + UseSteamP2PFalseSelected string + ExePath string + AdditionalParams string + AutoRestartServerTimer string + IsNewTerrainAndSaveSystem string + IsNewTerrainAndSaveSystemTrueSelected string + IsNewTerrainAndSaveSystemFalseSelected string + AutoStartServerOnStartup string + AutoStartServerOnStartupTrueSelected string + AutoStartServerOnStartupFalseSelected string + + UIText_ServerConfig string + UIText_DiscordIntegration string + UIText_DetectionManager string + UIText_ConfigurationWizard string + UIText_PleaseSelectSection string + UIText_UseWizardAlternative string + UIText_BasicSettings string + UIText_NetworkSettings string + UIText_AdvancedSettings string + UIText_BetaSettings string + UIText_BasicServerSettings string + + UIText_ServerName string + UIText_ServerNameInfo string + UIText_SaveFileName string + UIText_SaveFileNameInfo string + UIText_MaxPlayers string + UIText_MaxPlayersInfo string + UIText_ServerPassword string + UIText_ServerPasswordInfo string + UIText_AdminPassword string + UIText_AdminPasswordInfo string + UIText_AutoSave string + UIText_AutoSaveInfo string + UIText_SaveInterval string + UIText_SaveIntervalInfo string + UIText_AutoPauseServer string + UIText_AutoPauseServerInfo string + UIText_NetworkConfiguration string + UIText_GamePort string + UIText_GamePortInfo string + UIText_UpdatePort string + UIText_UpdatePortInfo string + UIText_UPNPEnabled string + UIText_UPNPEnabledInfo string + UIText_LocalIpAddress string + UIText_LocalIpAddressInfo string + UIText_StartLocalHost string + UIText_StartLocalHostInfo string + UIText_ServerVisible string + UIText_ServerVisibleInfo string + UIText_UseSteamP2P string + UIText_UseSteamP2PInfo string + UIText_AdvancedConfiguration string + UIText_ServerAuthSecret string + UIText_ServerAuthSecretInfo string + UIText_ServerExePath string + UIText_ServerExePathInfo string + UIText_ServerExePathInfo2 string + UIText_AdditionalParams string + UIText_AdditionalParamsInfo string + UIText_AutoRestartServerTimer string + UIText_AutoRestartServerTimerInfo string + UIText_GameBranch string + UIText_GameBranchInfo string + UIText_BetaOnlySettings string + UIText_BetaWarning string + UIText_UseNewTerrainAndSave string + UIText_UseNewTerrainAndSaveInfo string + UIText_Difficulty string + UIText_DifficultyInfo string + UIText_StartCondition string + UIText_StartConditionInfo string + UIText_StartLocation string + UIText_StartLocationInfo string + UIText_AutoStartServerOnStartup string + UIText_AutoStartServerOnStartupInfo string + + UIText_DiscordIntegrationTitle string + UIText_DiscordBotToken string + UIText_DiscordBotTokenInfo string + UIText_ChannelConfiguration string + UIText_AdminCommandChannel string + UIText_AdminCommandChannelInfo string + UIText_ControlPanelChannel string + UIText_ControlPanelChannelInfo string + UIText_StatusChannel string + UIText_StatusChannelInfo string + UIText_ConnectionListChannel string + UIText_ConnectionListChannelInfo string + UIText_LogChannel string + UIText_LogChannelInfo string + UIText_SaveInfoChannel string + UIText_SaveInfoChannelInfo string + UIText_ErrorChannel string + UIText_ErrorChannelInfo string + UIText_BannedPlayersListPath string + UIText_BannedPlayersListPathInfo string + UIText_DiscordIntegrationBenefits string + UIText_DiscordBenefit1 string + UIText_DiscordBenefit2 string + UIText_DiscordBenefit3 string + UIText_DiscordBenefit4 string + UIText_DiscordBenefit5 string + UIText_DiscordSetupInstructions string + + UIText_Copyright string + UIText_CopyrightConfig1 string + UIText_CopyrightConfig2 string +} diff --git a/sscm/SSCM.dll b/sscm/SSCM.dll index 32defd63..766d35c7 100644 Binary files a/sscm/SSCM.dll and b/sscm/SSCM.dll differ diff --git a/sscm/SSCM.pdb b/sscm/SSCM.pdb deleted file mode 100644 index 31c47a01..00000000 Binary files a/sscm/SSCM.pdb and /dev/null differ