diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..dfe07704 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/workflows/nightly-sync.yml b/.github/workflows/nightly-sync.yml new file mode 100644 index 00000000..a6900396 --- /dev/null +++ b/.github/workflows/nightly-sync.yml @@ -0,0 +1,38 @@ +# .github/workflows/nightly-sync.yml + +name: Push to Public Nightly Branch + +on: + push: + branches: + - '' # This triggers the action whenever there's a push to the nightly branch in the private repo + +jobs: + push_to_public_nightly: + runs-on: ubuntu-latest + + steps: + # Checkout the private repo's code + - name: Checkout private repository code + uses: actions/checkout@v3 + + # Set up Git configuration + - name: Set up Git config + run: | + git config --global user.name "JacksonTheMaster" + git config --global user.email "jakob.langisch@gmail.com" + + # Add the public repository as a remote + - name: Add public repository as remote + run: | + git remote add public https://JacksonTheMaster:${{ secrets.PAT_TOKEN }}@github.com/JacksonTheMaster/StationeersServerUI.git + git fetch public + + # Force push to the public repo's nightly branch + - name: Push to public nightly branch + run: | + git push public HEAD:nightly --force + + # Optional: Remove the public remote if you don't want it sticking around + - name: Clean up remotes + run: git remote remove public diff --git a/.gitignore b/.gitignore index 37e8c1d5..d62df4a3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,6 @@ UnityCrashHandler64.exe UnityPlayer.dll setting.xml rocketstation_DedicatedServer.exe -/saves/* \ No newline at end of file +/saves/* +.github/ +repos.md diff --git a/StationeersServerControl2.3.0_ReleaseCanary.exe b/StationeersServerControl2.4.1_Release.exe similarity index 63% rename from StationeersServerControl2.3.0_ReleaseCanary.exe rename to StationeersServerControl2.4.1_Release.exe index 298e98a4..3ba04b90 100644 Binary files a/StationeersServerControl2.3.0_ReleaseCanary.exe and b/StationeersServerControl2.4.1_Release.exe differ diff --git a/UIMod/config.html b/UIMod/config.html index 49c4d8ee..6ec06622 100644 --- a/UIMod/config.html +++ b/UIMod/config.html @@ -1,76 +1,79 @@ + - - - Edit Configuration - + + + Edit Configuration + + -
- -
-
- -

Edit Configuration

-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-

- - -
-
+
+ +
+
+

Edit Configuration

+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+

+ + +
+
+ \ No newline at end of file diff --git a/UIMod/config.json b/UIMod/config.json index 6759fe0b..fc1af852 100644 --- a/UIMod/config.json +++ b/UIMod/config.json @@ -1,12 +1,12 @@ { - "discordToken": "redacted", - "controlChannelID": "1279897558495531129", - "statusChannelID": "1279897558495531129", - "connectionListChannelID": "1279897558495531129", - "logChannelID": "1279902452044267530", - "saveChannelID": "1279897558495531129", - "controlPanelChannelID": "1279897558495531129", - "blackListFilePath": "C:/SteamCMD/Stationeers/Blacklist.txt", - "backupDir": "C:/SteamCMD/Stationeers/saves/EuropaProd/backup", - "isDiscordEnabled": false + "discordToken": "redacted", + "controlChannelID": "1279897558495531129", + "statusChannelID": "1279897558495531129", + "connectionListChannelID": "1279897558495531129", + "logChannelID": "1279902452044267530", + "saveChannelID": "1279897558495531129", + "controlPanelChannelID": "1279897558495531129", + "blackListFilePath": "./Blacklist.txt", + "isDiscordEnabled": false, + "errorChannelID": "1279897558495531129" } diff --git a/UIMod/config.xml b/UIMod/config.xml index 61deda05..e0cfa5cf 100644 --- a/UIMod/config.xml +++ b/UIMod/config.xml @@ -1,7 +1,7 @@ ./rocketstation_DedicatedServer.exe - StartLocalHost true ServerVisible true GamePort 27016 UpdatePort 27015 AutoSave true SaveInterval 500 LocalIpAddress 10.0.50.166 ServerMaxPlayers 1 ServerName StationeersServerWithUI + StartLocalHost true ServerVisible true GamePort 27016 UpdatePort 27015 AutoSave true SaveInterval 500 LocalIpAddress 127.0.0.1 ServerMaxPlayers 1 ServerName StationeersServerWithUI EuropaProd \ No newline at end of file diff --git a/UIMod/futherconfig.html b/UIMod/futherconfig.html new file mode 100644 index 00000000..63d4da6c --- /dev/null +++ b/UIMod/futherconfig.html @@ -0,0 +1,60 @@ + + + + + + Edit Configuration + + + +
+

Edit Configuration

+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ + + diff --git a/UIMod/index.html b/UIMod/index.html index 9b5dd951..ab0ea883 100644 --- a/UIMod/index.html +++ b/UIMod/index.html @@ -11,12 +11,13 @@
-

Stationeers Dedicated Server Control v2.2.X

+

Stationeers Dedicated Server Control v2.4.1

- - + + +

@@ -25,7 +26,7 @@

Saves



- Copyright © 2024 JMG J. Langisch. Licensed under MIT License. + Copyright © 2024 JMG J. Langisch. Licensed under MIT License.
diff --git a/UIMod/script.js b/UIMod/script.js index 45d09511..ff54d664 100644 --- a/UIMod/script.js +++ b/UIMod/script.js @@ -111,6 +111,10 @@ function setDefaultConsoleMessage() { const bootCompleteMessage = "System boot complete.\nINFO: Press 'Start' to launch the game server."; + // Random chance to trigger a funny bug (e.g., 20% chance) + const bugChance = Math.random(); + const bugMessage = "ERROR: Nuclear parts in airflow detected! Initiating repair sequence..."; + // First, type the boot title typeTextWithCallback(consoleElement, bootTitle, 50, () => { // After the title is typed, start the progress bar update sequence @@ -127,18 +131,46 @@ function setDefaultConsoleMessage() { index++; } else { clearInterval(bootInterval); // Stop when progress is done - // Display the completion message - setTimeout(() => { - const completionElement = document.createElement('div'); - completionElement.innerHTML = bootCompleteMessage.replace(/\n/g, '
'); // Add the completion message - consoleElement.appendChild(completionElement); + + if (bugChance < 0.05) { + const bugElement = document.createElement('div'); + bugElement.style.color = 'red'; // Make it look like an error + bugElement.textContent = bugMessage; + consoleElement.appendChild(bugElement); consoleElement.scrollTop = consoleElement.scrollHeight; - }, 500); // Delay for .5 second after progress reaches 100% + + // Delay for 2 seconds before "repairing" + setTimeout(() => { + const repairMessage = "Repair complete. Continuing boot sequence..."; + const repairElement = document.createElement('div'); + repairElement.style.color = 'green'; + repairElement.textContent = repairMessage; + consoleElement.appendChild(repairElement); + consoleElement.scrollTop = consoleElement.scrollHeight; + + // Finally, display the completion message after another short delay + setTimeout(() => { + const completionElement = document.createElement('div'); + completionElement.innerHTML = bootCompleteMessage.replace(/\n/g, '
'); // Add the completion message + consoleElement.appendChild(completionElement); + consoleElement.scrollTop = consoleElement.scrollHeight; + }, 1000); // Delay for 1 second after repair message + }, 2000); // Repair after 2 seconds + } else { + // Display the normal completion message + setTimeout(() => { + const completionElement = document.createElement('div'); + completionElement.innerHTML = bootCompleteMessage.replace(/\n/g, '
'); // Add the completion message + consoleElement.appendChild(completionElement); + consoleElement.scrollTop = consoleElement.scrollHeight; + }, 200); // Delay for .5 second after progress reaches 100% + } } - }, 200); // Each progress update every 200ms - }, 2000); // Initial delay of 1s to simulate a pause + }, 150); // Each progress update every 150ms + }, 100); // Initial delay of 2s to simulate a pause }); } + fetchOutput(); fetchBackups(); \ No newline at end of file diff --git a/UIMod/style.css b/UIMod/style.css index bf936448..348752f9 100644 --- a/UIMod/style.css +++ b/UIMod/style.css @@ -1,5 +1,9 @@ @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); +html { + scroll-behavior: smooth; +} + body { font-family: 'Press Start 2P', cursive; background-color: #000000; @@ -180,6 +184,50 @@ input[type="submit"]:hover { animation: blink 1s infinite; } + + +select { + width: 100%; + padding: 12px; + margin: 10px 0; + box-sizing: border-box; + background-color: #10101A; + color: #00FFAB; + border: 2px solid #00FFAB; + border-radius: 4px; + font-family: 'Press Start 2P', cursive; + font-size: 1rem; + cursor: pointer; + appearance: none; /* Removes default styling for better customization */ + -webkit-appearance: none; + -moz-appearance: none; + transition: background-color 0.3s ease, transform 0.2s ease; + box-shadow: 0 0 10px rgba(0, 255, 171, 0.4), 0 0 20px rgba(0, 255, 171, 0.1); +} + +select:hover { + background-color: #111; + transform: translateY(-3px); + box-shadow: 0 0 15px rgba(0, 255, 171, 0.7), 0 0 30px rgba(0, 255, 171, 0.5); +} + +select option { + background-color: #1b1b2f; /* Background for dropdown items */ + color: #00FFAB; /* Text color for dropdown items */ + font-family: 'Press Start 2P', cursive; +} + +/* Add a down arrow icon for the select dropdown */ +select::after { + content: '▼'; + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + pointer-events: none; + color: #00FFAB; +} + /* Mobile adjustments */ @media only screen and (max-width: 768px) { main { @@ -219,3 +267,10 @@ input[type="submit"]:hover { width: 100%; } } + +@media only screen and (max-width: 768px) { + select { + font-size: 0.9rem; + padding: 10px; + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 5286ca79..f9db6f5b 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,11 @@ go 1.22.1 require ( github.com/bwmarrin/discordgo v0.28.1 + github.com/fsnotify/fsnotify v1.7.0 github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc ) require ( - github.com/creack/pty v1.1.23 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/stretchr/testify v1.7.0 // indirect golang.org/x/crypto v0.26.0 // indirect diff --git a/go.sum b/go.sum index 84b967eb..77a913cc 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= -github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= -github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= diff --git a/readme.md b/readme.md index 7fdc6849..5b56ae68 100644 --- a/readme.md +++ b/readme.md @@ -55,7 +55,7 @@ DISCLAIMER: PUBLISHING THIS UI TO THE WEB SHOULD ONLY BE DONE BEHIND A SECURE AU | Error | When an error is detected, a notification will be sent ## Discord Integration Setup - +- *use the "further setup" button on the UI!* - Create a Discord Bot and add it to your server. You can find instructions on how to do this [here](https://chatgpt.com/). - Obtain an OAuth2 Token for your Discord Bot. - Add the OAuth2 Token to the "DiscordToken" field in the config.json file. @@ -65,36 +65,20 @@ DISCLAIMER: PUBLISHING THIS UI TO THE WEB SHOULD ONLY BE DONE BEHIND A SECURE AU - Enable the Discord integration in the config.json file. - Restart the Software. + ## Requirements -- Windows OS -- Downloaded and installed the Stationeers Dedicated Server. -- Administrative Privileges Hostesly i still havn't tested it without running as admin, but from my experience it..should'nt? +- Window OS is tested, Linux is still not a priority but will be soon™. +- Administrative Privileges +- an Empty folder of your Choice + + ## Quick Installation Instrcutions for Administrators & Server Operators -1. Download & Extract release ZIP from GitHub. -2. Move "startStatoneersServerUI.exe" and the "UIMod" folder to the server's executable directory. -3. Run "startStatoneersServerUI.exe". (If you start "UIMod/Stationeers-ServerUI.exe", the Server wont auto restart) -4. Access UI at `http://:8080`. -5. Open firewall ports 27015, 27016, 8080. -6. Check /config before starting the server. - -## Detailed Installation Instrcutions for "Normal" Windows Users - -1. Go to the link: https://github.com/jacksonthemaster/StationeersServerUI/releases. -2. Find the latest release and click to download the ZIP file. -3. Once downloaded, locate the ZIP file, right-click on it, and select "Extract All...". -4. Choose a folder where you want to save the extracted files and click "Extract". -5. Open the folder with the extracted files and locate "startStatoneersServerUI.exe". -6. Cut (Ctrl+X) or copy (Ctrl+C) "startStatoneersServerUI.exe". -7. Navigate to the folder where you have installed your Stationeers Dedicated Server. -8. Paste (Ctrl+V) "startStatoneersServerUI.exe" into this folder. -9. Go back to the extracted files folder and find the "UIMod" folder. -10. Cut (Ctrl+X) or copy (Ctrl+C) the "UIMod" folder. -11. Paste (Ctrl+V) the "UIMod" folder into the same folder where your Stationeers Dedicated Server executable is located. -13. Double-click "startStatoneersServerUI.exe" to run it. Do not run "UIMod/Stationeers-ServerUI.exe" unless you DONT want the server to auto restart. -14. Open your web browser and type `http://:8080` in the address bar. Replace `` with the actual IP address of your server. You can find this by opening the Command Prompt and typing `ipconfig`. -15. To allow other users to connect to your UI and the Server, open the Windows Firewall settings: +1. Download & run the exe release file. +2. read the console output +3. Open your web browser and type `http://:8080` in the address bar. Replace `` with the actual IP address of your server. You can find this by opening the Command Prompt and typing `ipconfig`. +4. To allow other users to connect to your UI and the Server, open the Windows Firewall settings: - Go to Control Panel > System and Security > Windows Defender Firewall. - Click "Advanced settings" on the left. - In the Windows Firewall with Advanced Security window, click "Inbound Rules" on the left. @@ -105,7 +89,8 @@ DISCLAIMER: PUBLISHING THIS UI TO THE WEB SHOULD ONLY BE DONE BEHIND A SECURE AU - Select the network types to apply this rule (usually Domain, Private, and Public) and click "Next". - Name the rule something recognizable (e.g., "Stationeers Server Ports") and click "Finish". - __Note__: Depending on your Setup, you might need to Port forward those ports on your router. For this, please consider using google or any other search engine exept bing to find a tutorial on how to do this. -16. Before starting your server, ensure the configuration files on the /config page are set up correctly. +5. Before starting your Server, ensure the configuration files on the /config and /furtherconfig page are set up correctly. + ## REST API Information diff --git a/repos.md b/repos.md new file mode 100644 index 00000000..1168a192 --- /dev/null +++ b/repos.md @@ -0,0 +1,16 @@ +##Work in the Private Repo: +Develop your code in the private dev repository, making commits as usual. + +##Add the Public Repository as a Remote: +In your private repository, add the public repository as a remote by running: + +'''bash +git remote add public-repo https://github.com/username/public-repo.git +Push Changes to Public Repository: +Once you’re ready to sync the private dev branch (or any feature branch) to the public nightly branch, push the changes from your private repository: +''' + +'''bash +git push public-repo dev:nightly +This command will push the dev branch from your private repo to the nightly branch of the public repository. +''' diff --git a/src/api/futherconfig.go b/src/api/futherconfig.go new file mode 100644 index 00000000..5c93ccb1 --- /dev/null +++ b/src/api/futherconfig.go @@ -0,0 +1,117 @@ +package api + +import ( + "StationeersServerUI/src/config" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" +) + +// LoadConfigJSON loads the configuration from the JSON file +func loadConfigJSON() (*config.Config, error) { + configPath := "./UIMod/config.json" + jsonFile, err := os.Open(configPath) + if err != nil { + return nil, fmt.Errorf("error opening config.json: %v", err) + } + defer jsonFile.Close() + + byteValue, err := io.ReadAll(jsonFile) + if err != nil { + return nil, fmt.Errorf("error reading config.json: %v", err) + } + + var config config.Config + err = json.Unmarshal(byteValue, &config) + if err != nil { + return nil, fmt.Errorf("error unmarshalling config.json: %v", err) + } + + return &config, nil +} + +func HandleConfigJSON(w http.ResponseWriter, r *http.Request) { + config, err := loadConfigJSON() + if err != nil { + http.Error(w, fmt.Sprintf("Error loading config.json: %v", err), http.StatusInternalServerError) + return + } + + htmlFile, err := os.ReadFile("./UIMod/futherconfig.html") + if err != nil { + http.Error(w, fmt.Sprintf("Error reading discord.html: %v", err), http.StatusInternalServerError) + return + } + + htmlContent := string(htmlFile) + + // Check the value of IsDiscordEnabled and set the appropriate option as selected + isDiscordEnabledTrue := "" + isDiscordEnabledFalse := "" + + if config.IsDiscordEnabled { + isDiscordEnabledTrue = "selected" + } else { + isDiscordEnabledFalse = "selected" + } + + // Replace placeholders in the HTML with actual config values, including the new errorChannelID + 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, // New errorChannelID field + "{{isDiscordEnabledTrue}}": isDiscordEnabledTrue, + "{{isDiscordEnabledFalse}}": isDiscordEnabledFalse, + } + + for placeholder, value := range replacements { + htmlContent = strings.ReplaceAll(htmlContent, placeholder, value) + } + + fmt.Fprint(w, htmlContent) +} + +func SaveConfigJSON(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + config := config.Config{ + DiscordToken: r.FormValue("discordToken"), + ControlChannelID: r.FormValue("controlChannelID"), + StatusChannelID: r.FormValue("statusChannelID"), + ConnectionListChannelID: r.FormValue("connectionListChannelID"), + LogChannelID: r.FormValue("logChannelID"), + SaveChannelID: r.FormValue("saveChannelID"), + ControlPanelChannelID: r.FormValue("controlPanelChannelID"), + BlackListFilePath: r.FormValue("blackListFilePath"), + ErrorChannelID: r.FormValue("errorChannelID"), // New errorChannelID field + IsDiscordEnabled: r.FormValue("isDiscordEnabled") == "true", + } + + configPath := "./UIMod/config.json" + file, err := os.Create(configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Error creating config.json: %v", err), http.StatusInternalServerError) + return + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(&config); err != nil { + http.Error(w, fmt.Sprintf("Error encoding config.json: %v", err), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/futherconfig", http.StatusSeeOther) + } else { + http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) + } +} diff --git a/src/config/config.go b/src/config/config.go index ff38decf..8b7701b1 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -42,8 +42,8 @@ var ( BackupRestoreMessageID string ControlPanelChannelID string IsDiscordEnabled bool - Version = "2.3.0" - Branch = "ReleaseCanary" + Version = "2.4.1" + Branch = "Release" ) func LoadConfig(filename string) (*Config, error) { diff --git a/src/install/install.go b/src/install/install.go new file mode 100644 index 00000000..0c27767b --- /dev/null +++ b/src/install/install.go @@ -0,0 +1,114 @@ +package install + +import ( + "fmt" + "io" + "net/http" + "os" + "sync" +) + +// Install performs the entire installation process and ensures the server waits for it to complete +func Install(wg *sync.WaitGroup) { + defer wg.Done() // Signal that installation is complete + + // Check and download the UIMod folder contents + CheckAndDownloadUIMod() + + // Check for Blacklist.txt and create it if it doesn't exist + checkAndCreateBlacklist() + + InstallAndRunSteamCMD() +} + +func CheckAndDownloadUIMod() { + workingDir := "./UIMod/" + + // Check if the directory exists + if _, err := os.Stat(workingDir); os.IsNotExist(err) { + fmt.Println("Folder ./UIMod does not exist. Creating it...") + + // Create the folder + err := os.MkdirAll(workingDir, os.ModePerm) + if err != nil { + fmt.Printf("Error creating folder: %v\n", err) + return + } + + // List of files to download + files := map[string]string{ + "apiinfo.html": "https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/main/UIMod/apiinfo.html", + "config.html": "https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/main/UIMod/config.html", + "config.json": "https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/main/UIMod/config.json", + "config.xml": "https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/main/UIMod/config.xml", + "index.html": "https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/main/UIMod/index.html", + "script.js": "https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/main/UIMod/script.js", + "stationeers.png": "https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/main/UIMod/stationeers.png", + "style.css": "https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/main/UIMod/style.css", + } + + // Download each file + for fileName, url := range files { + err := downloadFile(workingDir+fileName, url) + if err != nil { + fmt.Printf("Error downloading %s: %v\n", fileName, err) + return + } + fmt.Printf("Downloaded %s successfully\n", fileName) + } + + fmt.Println("All files downloaded successfully.") + } else { + fmt.Println("Folder ./UIMod already exists. Skipping download.") + } +} + +// checkAndCreateBlacklist ensures Blacklist.txt exists in the root directory +func checkAndCreateBlacklist() { + blacklistFile := "./Blacklist.txt" + + // Check if Blacklist.txt exists + if _, err := os.Stat(blacklistFile); os.IsNotExist(err) { + // Create an empty Blacklist.txt file + file, err := os.Create(blacklistFile) + if err != nil { + fmt.Printf("Error creating Blacklist.txt: %v\n", err) + return + } + defer file.Close() + + fmt.Println("Created Blacklist.txt.") + } else { + fmt.Println("Blacklist.txt already exists. Skipping creation.") + } +} + +// downloadFile downloads a file from the given URL and saves it to the given filepath +func downloadFile(filepath string, url string) error { + // Create the file + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + + // Get the data + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + // Check server response + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bad status: %s", resp.Status) + } + + // Write the body to file + _, err = io.Copy(out, resp.Body) + if err != nil { + return err + } + + return nil +} diff --git a/src/install/steamcmd.go b/src/install/steamcmd.go new file mode 100644 index 00000000..acd81160 --- /dev/null +++ b/src/install/steamcmd.go @@ -0,0 +1,241 @@ +package install + +import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" +) + +// Color codes for terminal +const ( + ColorReset = "\033[0m" + ColorRed = "\033[31m" + ColorGreen = "\033[32m" + ColorYellow = "\033[33m" + ColorBlue = "\033[34m" + ColorPurple = "\033[35m" + ColorCyan = "\033[36m" + ColorWhite = "\033[37m" +) + +// InstallAndRunSteamCMD installs and runs SteamCMD based on the platform (Windows/Linux) +func InstallAndRunSteamCMD() { + if runtime.GOOS == "windows" { + installSteamCMDWindows() + } else if runtime.GOOS == "linux" { + installSteamCMDLinux() + } else { + fmt.Println(ColorRed + "SteamCMD installation is not supported on this OS." + ColorReset) + return + } +} + +// installSteamCMDWindows downloads and installs SteamCMD on Windows +func installSteamCMDWindows() { + steamCMDDir := "C:\\SteamCMD" + + // Check if SteamCMD is already installed + if _, err := os.Stat(steamCMDDir); os.IsNotExist(err) { + fmt.Println(ColorYellow + "SteamCMD not found, downloading..." + ColorReset) + + // Create SteamCMD directory + err := os.MkdirAll(steamCMDDir, os.ModePerm) + if err != nil { + fmt.Printf(ColorRed+"Error creating SteamCMD directory: %v\n"+ColorReset, err) + return + } + + // Download SteamCMD + downloadURL := "https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip" + resp, err := http.Get(downloadURL) + if err != nil { + fmt.Printf(ColorRed+"Error downloading SteamCMD: %v\n"+ColorReset, err) + return + } + defer resp.Body.Close() + + // Read zip content + zipContent, err := io.ReadAll(resp.Body) + zipReader := bytes.NewReader(zipContent) + + if err != nil { + fmt.Printf(ColorRed+"Error reading SteamCMD zip: %v\n"+ColorReset, err) + return + } + + // Unzip to C:\SteamCMD + err = unzip(zipReader, zipReader.Size(), steamCMDDir) + if err != nil { + fmt.Printf(ColorRed+"Error extracting SteamCMD zip: %v\n"+ColorReset, err) + return + } + + fmt.Println(ColorGreen + "SteamCMD installed successfully." + ColorReset) + } + + // Run SteamCMD + runSteamCMD(steamCMDDir) +} + +// installSteamCMDLinux downloads and installs SteamCMD on Linux +func installSteamCMDLinux() { + steamCMDDir := "./steamcmd" + + // Check if SteamCMD is already installed + if _, err := os.Stat(steamCMDDir); os.IsNotExist(err) { + fmt.Println(ColorYellow + "SteamCMD not found, downloading..." + ColorReset) + + // Create SteamCMD directory + err := os.MkdirAll(steamCMDDir, os.ModePerm) + if err != nil { + fmt.Printf(ColorRed+"Error creating SteamCMD directory: %v\n"+ColorReset, err) + return + } + + // Download SteamCMD for Linux + downloadURL := "https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" + resp, err := http.Get(downloadURL) + if err != nil { + fmt.Printf(ColorRed+"Error downloading SteamCMD: %v\n"+ColorReset, err) + return + } + defer resp.Body.Close() + + // Read tar.gz content + err = untar(steamCMDDir, resp.Body) + if err != nil { + fmt.Printf(ColorRed+"Error extracting SteamCMD tar.gz: %v\n"+ColorReset, err) + return + } + + // Ensure executable permissions + err = os.Chmod(filepath.Join(steamCMDDir, "steamcmd.sh"), 0755) + if err != nil { + fmt.Printf(ColorRed+"Error setting SteamCMD executable permissions: %v\n"+ColorReset, err) + return + } + + fmt.Println(ColorGreen + "SteamCMD installed successfully." + ColorReset) + } + + // Run SteamCMD + runSteamCMD(steamCMDDir) +} + +// runSteamCMD runs the SteamCMD command to update the game +func runSteamCMD(steamCMDDir string) { + currentDir, err := os.Getwd() + if err != nil { + fmt.Printf(ColorRed+"Error getting current working directory: %v\n"+ColorReset, err) + return + } + + // Construct SteamCMD command based on OS + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.Command(filepath.Join(steamCMDDir, "steamcmd.exe"), "+force_install_dir", currentDir, "+login", "anonymous", "+app_update", "600760", "+quit") + } else if runtime.GOOS == "linux" { + cmd = exec.Command(filepath.Join(steamCMDDir, "steamcmd.sh"), "+force_install_dir", currentDir, "+login", "anonymous", "+app_update", "600760", "+quit") + } + + // Set output to stdout + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // Run the command + fmt.Println(ColorBlue + "Running SteamCMD..." + ColorReset) + err = cmd.Run() + if err != nil { + fmt.Printf(ColorRed+"Error running SteamCMD: %v\n"+ColorReset, err) + return + } + + fmt.Println(ColorGreen + "SteamCMD command executed successfully." + ColorReset) +} + +// unzip extracts a zip archive +func unzip(zipReader io.ReaderAt, size int64, dest string) error { + reader, err := zip.NewReader(zipReader, size) + if err != nil { + return err + } + + for _, f := range reader.File { + fpath := filepath.Join(dest, f.Name) + if f.FileInfo().IsDir() { + os.MkdirAll(fpath, os.ModePerm) + continue + } + + // Create the file + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + defer outFile.Close() + + rc, err := f.Open() + if err != nil { + return err + } + defer rc.Close() + + _, err = io.Copy(outFile, rc) + if err != nil { + return err + } + } + + return nil +} + +// untar extracts a tar.gz archive +func untar(dest string, r io.Reader) error { + gr, err := gzip.NewReader(r) + if err != nil { + return err + } + defer gr.Close() + + tr := tar.NewReader(gr) + + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + target := filepath.Join(dest, header.Name) + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(target, os.ModePerm); err != nil { + return err + } + case tar.TypeReg: + outFile, err := os.Create(target) + if err != nil { + return err + } + defer outFile.Close() + + if _, err := io.Copy(outFile, tr); err != nil { + return err + } + default: + return fmt.Errorf(ColorRed+"unknown type: %v in %s"+ColorReset, header.Typeflag, header.Name) + } + } + + return nil +} diff --git a/src/server.go b/src/server.go index df5511d7..61c32844 100644 --- a/src/server.go +++ b/src/server.go @@ -4,8 +4,10 @@ import ( "StationeersServerUI/src/api" "StationeersServerUI/src/config" discord "StationeersServerUI/src/discord" + "StationeersServerUI/src/install" "fmt" "net/http" + "sync" "time" _ "net/http/pprof" @@ -15,6 +17,14 @@ import ( func main() { + var wg sync.WaitGroup + + // Start the installation process and wait for it to complete + wg.Add(1) + go install.Install(&wg) + + // Wait for the installation to finish before starting the rest of the server + wg.Wait() // Check if the branch is not "Prod" and enable pprof if its not if config.Branch != "Prod" { go func() { @@ -46,6 +56,8 @@ func main() { http.HandleFunc("/restore", api.RestoreBackup) http.HandleFunc("/config", api.HandleConfig) http.HandleFunc("/saveconfig", api.SaveConfig) + http.HandleFunc("/futherconfig", api.HandleConfigJSON) + http.HandleFunc("/saveconfigasjson", api.SaveConfigJSON) http.ListenAndServe(":8080", nil) }