From dde2738d77a91afcb3716e2d65523cdb68032009 Mon Sep 17 00:00:00 2001 From: Diogo Martino Date: Tue, 13 Feb 2024 20:51:21 +0000 Subject: [PATCH 01/15] feat: remote server support --- .gitignore => app/.gitignore | 0 app.go => app/app.go | 35 +- {build => app/build}/appicon.png | Bin {frontend => app/frontend}/.eslintrc.cjs | 0 {frontend => app/frontend}/.gitignore | 0 {frontend => app/frontend}/.npmrc | 0 {frontend => app/frontend}/.prettierignore | 0 {frontend => app/frontend}/.prettierrc | 0 {frontend => app/frontend}/index.html | 0 {frontend => app/frontend}/package.json | 5 +- app/frontend/package.json.md5 | 1 + {frontend => app/frontend}/pnpm-lock.yaml | 59 ++++ {frontend => app/frontend}/postcss.config.js | 0 {frontend => app/frontend}/public/vite.svg | 0 {frontend => app/frontend}/src/actions/app.ts | 46 +-- app/frontend/src/actions/console.ts | 7 + .../frontend}/src/actions/modal.ts | 0 .../frontend}/src/actions/server.ts | 22 +- app/frontend/src/actions/socket.ts | 93 ++++++ .../frontend}/src/assets/palworld-logo.webp | Bin .../frontend}/src/components/app/index.tsx | 7 +- .../custom-error-boundary/index.tsx | 63 ++++ .../custom-toast-container/index.tsx | 24 ++ .../frontend}/src/components/layout/index.tsx | 0 .../modals/confirm-action/index.tsx | 0 .../frontend}/src/components/modals/index.tsx | 0 .../src/components/routing/index.tsx | 0 app/frontend/src/components/sidebar/index.tsx | 62 ++++ .../src/components/sidebar/nav-item.tsx | 5 +- .../src/components/socket-provider/index.tsx | 130 ++++++++ .../src/components/terminal-output/index.tsx | 2 +- app/frontend/src/desktop.ts | 132 ++++++++ .../frontend}/src/helpers/bytes-to-mb.ts | 0 .../frontend}/src/helpers/config-parser.ts | 0 .../frontend}/src/helpers/sleep.ts | 0 app/frontend/src/hooks/use-backup-settings.ts | 6 + app/frontend/src/hooks/use-backups-list.ts | 6 + app/frontend/src/hooks/use-console-entries.ts | 7 + app/frontend/src/hooks/use-events-init.ts | 43 +++ .../frontend}/src/hooks/use-latest-version.ts | 0 .../frontend}/src/hooks/use-launch-params.ts | 0 .../frontend}/src/hooks/use-modals-info.ts | 0 .../src/hooks/use-rcon-credentials.ts | 0 .../frontend}/src/hooks/use-selected-theme.ts | 0 .../frontend}/src/hooks/use-server-config.ts | 0 .../src/hooks/use-server-credentials.ts | 6 + .../src/hooks/use-server-save-name.ts.ts | 0 .../frontend}/src/hooks/use-server-status.ts | 0 .../frontend}/src/hooks/use-settings.ts | 0 app/frontend/src/hooks/use-socket.ts | 12 + .../frontend}/src/hooks/use-steam-images.ts | 0 .../frontend}/src/hooks/use-theme.ts | 0 .../src/hooks/use-websocket-context.ts | 8 + {frontend => app/frontend}/src/main.css | 0 app/frontend/src/main.tsx | 31 ++ .../frontend}/src/screens/about/index.tsx | 10 +- .../frontend}/src/screens/admin/index.tsx | 33 +- .../src/screens/app-settings/index.tsx | 69 ++++ .../frontend}/src/screens/backups/index.tsx | 136 +++++--- .../frontend}/src/screens/home/index.tsx | 25 +- .../src/screens/initializing/index.tsx | 100 ++++++ .../src/screens/server-settings/index.tsx | 24 +- .../frontend}/src/selectors/app.ts | 6 +- app/frontend/src/selectors/console.ts | 4 + .../frontend}/src/selectors/modals.ts | 0 .../frontend}/src/selectors/server.ts | 6 + app/frontend/src/selectors/socket.ts | 3 + app/frontend/src/server.ts | 176 ++++++++++ .../frontend}/src/store/app-slice.ts | 31 +- .../frontend}/src/store/console-slice.ts | 16 +- {frontend => app/frontend}/src/store/index.ts | 4 +- .../frontend}/src/store/modals-slice.ts | 0 .../frontend}/src/store/server-slice.ts | 23 +- app/frontend/src/store/socket-slice.ts | 46 +++ app/frontend/src/types/index.ts | 98 ++++++ {frontend => app/frontend}/src/types/rcon.ts | 0 .../frontend}/src/types/server-config.ts | 0 {frontend => app/frontend}/src/vite-env.d.ts | 0 .../frontend}/src/wailsjs/go/main/App.d.ts | 4 + .../frontend}/src/wailsjs/go/main/App.js | 8 + .../src/wailsjs/go/rconclient/RconClient.d.ts | 0 .../src/wailsjs/go/rconclient/RconClient.js | 0 .../src/wailsjs/runtime/package.json | 0 .../src/wailsjs/runtime/runtime.d.ts | 0 .../frontend}/src/wailsjs/runtime/runtime.js | 0 {frontend => app/frontend}/tailwind.config.js | 0 {frontend => app/frontend}/tsconfig.json | 0 {frontend => app/frontend}/tsconfig.node.json | 0 {frontend => app/frontend}/vite.config.ts | 0 go.mod => app/go.mod | 12 - go.sum => app/go.sum | 36 -- main.go => app/main.go | 14 +- .../rcon-client}/rcon-client.go | 0 app/utils/utils.go | 86 +++++ wails.json => app/wails.json | 0 dedicated-server/dedicated-server.go | 315 ------------------ frontend/package.json.md5 | 1 - frontend/src/actions/console.ts | 7 - frontend/src/components/sidebar/index.tsx | 41 --- frontend/src/desktop.ts | 206 ------------ frontend/src/hooks/use-consoles-by-id.ts | 8 - frontend/src/hooks/use-events-init.ts | 92 ----- frontend/src/hooks/use-loading-status.ts | 6 - frontend/src/main.tsx | 22 -- frontend/src/screens/app-settings/index.tsx | 26 -- frontend/src/screens/initializing/index.tsx | 50 --- frontend/src/selectors/console.ts | 20 -- frontend/src/types/index.ts | 62 ---- .../go/backupsmanager/BackupManager.d.ts | 24 -- .../go/backupsmanager/BackupManager.js | 43 --- .../go/dedicatedserver/DedicatedServer.d.ts | 31 -- .../go/dedicatedserver/DedicatedServer.js | 59 ---- go.work | 6 + go.work.sum | 55 +++ server/.gitignore | 21 ++ server/api.go | 74 ++++ .../backups-manager.go | 154 +++++---- server/client-init-handler.go | 54 +++ server/config-manager.go | 98 ++++++ server/create-backup-handler.go | 50 +++ server/delete-backup-handler.go | 61 ++++ server/get-backups-config-handler.go | 46 +++ server/get-backups-list-handler.go | 58 ++++ server/go.mod | 24 ++ server/go.sum | 36 ++ server/main.go | 36 ++ server/read-config-handler.go | 50 +++ server/read-save-handler.go | 50 +++ server/restart-server-handler.go | 51 +++ server/restore-backup-handler.go | 62 ++++ server/server-manager.go | 214 ++++++++++++ server/start-backups-handler.go | 51 +++ server/start-server-handler.go | 51 +++ {steamcmd => server}/steamcmd.go | 32 +- server/stop-backups-handler.go | 51 +++ server/stop-server-handler.go | 43 +++ server/update-server-handler.go | 51 +++ {utils => server/utils}/utils.go | 203 +++++++++-- server/websocket.go | 186 +++++++++++ server/write-config-handler.go | 53 +++ server/write-save-handler.go | 62 ++++ 141 files changed, 3393 insertions(+), 1385 deletions(-) rename .gitignore => app/.gitignore (100%) rename app.go => app/app.go (63%) rename {build => app/build}/appicon.png (100%) rename {frontend => app/frontend}/.eslintrc.cjs (100%) rename {frontend => app/frontend}/.gitignore (100%) rename {frontend => app/frontend}/.npmrc (100%) rename {frontend => app/frontend}/.prettierignore (100%) rename {frontend => app/frontend}/.prettierrc (100%) rename {frontend => app/frontend}/index.html (100%) rename {frontend => app/frontend}/package.json (92%) create mode 100644 app/frontend/package.json.md5 rename {frontend => app/frontend}/pnpm-lock.yaml (99%) rename {frontend => app/frontend}/postcss.config.js (100%) rename {frontend => app/frontend}/public/vite.svg (100%) rename {frontend => app/frontend}/src/actions/app.ts (68%) create mode 100644 app/frontend/src/actions/console.ts rename {frontend => app/frontend}/src/actions/modal.ts (100%) rename {frontend => app/frontend}/src/actions/server.ts (56%) create mode 100644 app/frontend/src/actions/socket.ts rename {frontend => app/frontend}/src/assets/palworld-logo.webp (100%) rename {frontend => app/frontend}/src/components/app/index.tsx (62%) create mode 100644 app/frontend/src/components/custom-error-boundary/index.tsx create mode 100644 app/frontend/src/components/custom-toast-container/index.tsx rename {frontend => app/frontend}/src/components/layout/index.tsx (100%) rename {frontend => app/frontend}/src/components/modals/confirm-action/index.tsx (100%) rename {frontend => app/frontend}/src/components/modals/index.tsx (100%) rename {frontend => app/frontend}/src/components/routing/index.tsx (100%) create mode 100644 app/frontend/src/components/sidebar/index.tsx rename {frontend => app/frontend}/src/components/sidebar/nav-item.tsx (96%) create mode 100644 app/frontend/src/components/socket-provider/index.tsx rename {frontend => app/frontend}/src/components/terminal-output/index.tsx (94%) create mode 100644 app/frontend/src/desktop.ts rename {frontend => app/frontend}/src/helpers/bytes-to-mb.ts (100%) rename {frontend => app/frontend}/src/helpers/config-parser.ts (100%) rename {frontend => app/frontend}/src/helpers/sleep.ts (100%) create mode 100644 app/frontend/src/hooks/use-backup-settings.ts create mode 100644 app/frontend/src/hooks/use-backups-list.ts create mode 100644 app/frontend/src/hooks/use-console-entries.ts create mode 100644 app/frontend/src/hooks/use-events-init.ts rename {frontend => app/frontend}/src/hooks/use-latest-version.ts (100%) rename {frontend => app/frontend}/src/hooks/use-launch-params.ts (100%) rename {frontend => app/frontend}/src/hooks/use-modals-info.ts (100%) rename {frontend => app/frontend}/src/hooks/use-rcon-credentials.ts (100%) rename {frontend => app/frontend}/src/hooks/use-selected-theme.ts (100%) rename {frontend => app/frontend}/src/hooks/use-server-config.ts (100%) create mode 100644 app/frontend/src/hooks/use-server-credentials.ts rename {frontend => app/frontend}/src/hooks/use-server-save-name.ts.ts (100%) rename {frontend => app/frontend}/src/hooks/use-server-status.ts (100%) rename {frontend => app/frontend}/src/hooks/use-settings.ts (100%) create mode 100644 app/frontend/src/hooks/use-socket.ts rename {frontend => app/frontend}/src/hooks/use-steam-images.ts (100%) rename {frontend => app/frontend}/src/hooks/use-theme.ts (100%) create mode 100644 app/frontend/src/hooks/use-websocket-context.ts rename {frontend => app/frontend}/src/main.css (100%) create mode 100644 app/frontend/src/main.tsx rename {frontend => app/frontend}/src/screens/about/index.tsx (91%) rename {frontend => app/frontend}/src/screens/admin/index.tsx (93%) create mode 100644 app/frontend/src/screens/app-settings/index.tsx rename {frontend => app/frontend}/src/screens/backups/index.tsx (66%) rename {frontend => app/frontend}/src/screens/home/index.tsx (89%) create mode 100644 app/frontend/src/screens/initializing/index.tsx rename {frontend => app/frontend}/src/screens/server-settings/index.tsx (83%) rename {frontend => app/frontend}/src/selectors/app.ts (84%) create mode 100644 app/frontend/src/selectors/console.ts rename {frontend => app/frontend}/src/selectors/modals.ts (100%) rename {frontend => app/frontend}/src/selectors/server.ts (60%) create mode 100644 app/frontend/src/selectors/socket.ts create mode 100644 app/frontend/src/server.ts rename {frontend => app/frontend}/src/store/app-slice.ts (74%) rename {frontend => app/frontend}/src/store/console-slice.ts (54%) rename {frontend => app/frontend}/src/store/index.ts (84%) rename {frontend => app/frontend}/src/store/modals-slice.ts (100%) rename {frontend => app/frontend}/src/store/server-slice.ts (56%) create mode 100644 app/frontend/src/store/socket-slice.ts create mode 100644 app/frontend/src/types/index.ts rename {frontend => app/frontend}/src/types/rcon.ts (100%) rename {frontend => app/frontend}/src/types/server-config.ts (100%) rename {frontend => app/frontend}/src/vite-env.d.ts (100%) rename {frontend => app/frontend}/src/wailsjs/go/main/App.d.ts (74%) rename {frontend => app/frontend}/src/wailsjs/go/main/App.js (70%) rename {frontend => app/frontend}/src/wailsjs/go/rconclient/RconClient.d.ts (100%) rename {frontend => app/frontend}/src/wailsjs/go/rconclient/RconClient.js (100%) rename {frontend => app/frontend}/src/wailsjs/runtime/package.json (100%) rename {frontend => app/frontend}/src/wailsjs/runtime/runtime.d.ts (100%) rename {frontend => app/frontend}/src/wailsjs/runtime/runtime.js (100%) rename {frontend => app/frontend}/tailwind.config.js (100%) rename {frontend => app/frontend}/tsconfig.json (100%) rename {frontend => app/frontend}/tsconfig.node.json (100%) rename {frontend => app/frontend}/vite.config.ts (100%) rename go.mod => app/go.mod (80%) rename go.sum => app/go.sum (85%) rename main.go => app/main.go (83%) rename {rcon-client => app/rcon-client}/rcon-client.go (100%) create mode 100644 app/utils/utils.go rename wails.json => app/wails.json (100%) delete mode 100644 dedicated-server/dedicated-server.go delete mode 100644 frontend/package.json.md5 delete mode 100644 frontend/src/actions/console.ts delete mode 100644 frontend/src/components/sidebar/index.tsx delete mode 100644 frontend/src/desktop.ts delete mode 100644 frontend/src/hooks/use-consoles-by-id.ts delete mode 100644 frontend/src/hooks/use-events-init.ts delete mode 100644 frontend/src/hooks/use-loading-status.ts delete mode 100644 frontend/src/main.tsx delete mode 100644 frontend/src/screens/app-settings/index.tsx delete mode 100644 frontend/src/screens/initializing/index.tsx delete mode 100644 frontend/src/selectors/console.ts delete mode 100644 frontend/src/types/index.ts delete mode 100644 frontend/src/wailsjs/go/backupsmanager/BackupManager.d.ts delete mode 100644 frontend/src/wailsjs/go/backupsmanager/BackupManager.js delete mode 100644 frontend/src/wailsjs/go/dedicatedserver/DedicatedServer.d.ts delete mode 100644 frontend/src/wailsjs/go/dedicatedserver/DedicatedServer.js create mode 100644 go.work create mode 100644 go.work.sum create mode 100644 server/.gitignore create mode 100644 server/api.go rename {backups-manager => server}/backups-manager.go (50%) create mode 100644 server/client-init-handler.go create mode 100644 server/config-manager.go create mode 100644 server/create-backup-handler.go create mode 100644 server/delete-backup-handler.go create mode 100644 server/get-backups-config-handler.go create mode 100644 server/get-backups-list-handler.go create mode 100644 server/go.mod create mode 100644 server/go.sum create mode 100644 server/main.go create mode 100644 server/read-config-handler.go create mode 100644 server/read-save-handler.go create mode 100644 server/restart-server-handler.go create mode 100644 server/restore-backup-handler.go create mode 100644 server/server-manager.go create mode 100644 server/start-backups-handler.go create mode 100644 server/start-server-handler.go rename {steamcmd => server}/steamcmd.go (50%) create mode 100644 server/stop-backups-handler.go create mode 100644 server/stop-server-handler.go create mode 100644 server/update-server-handler.go rename {utils => server/utils}/utils.go (52%) create mode 100644 server/websocket.go create mode 100644 server/write-config-handler.go create mode 100644 server/write-save-handler.go diff --git a/.gitignore b/app/.gitignore similarity index 100% rename from .gitignore rename to app/.gitignore diff --git a/app.go b/app/app.go similarity index 63% rename from app.go rename to app/app.go index 4b02d4f..4cc2bb3 100644 --- a/app.go +++ b/app/app.go @@ -3,10 +3,7 @@ package main import ( "context" "fmt" - backupsmanager "palword-ds-gui/backups-manager" - dedicatedserver "palword-ds-gui/dedicated-server" rconclient "palword-ds-gui/rcon-client" - "palword-ds-gui/steamcmd" "palword-ds-gui/utils" "github.com/gocolly/colly/v2" @@ -14,20 +11,14 @@ import ( ) type App struct { - ctx context.Context - steamCmd *steamcmd.SteamCMD - dedicatedServer *dedicatedserver.DedicatedServer - backupsManager *backupsmanager.BackupManager - rconClient *rconclient.RconClient - reactReady bool + ctx context.Context + rconClient *rconclient.RconClient + reactReady bool } -func NewApp(server *dedicatedserver.DedicatedServer, cmd *steamcmd.SteamCMD, backupManager *backupsmanager.BackupManager, rconClient *rconclient.RconClient) *App { +func NewApp(rconClient *rconclient.RconClient) *App { return &App{ - steamCmd: cmd, - dedicatedServer: server, - backupsManager: backupManager, - rconClient: rconClient, + rconClient: rconClient, } } @@ -48,19 +39,11 @@ func (a *App) InitApp() { return } - a.steamCmd.Init(a.ctx) - a.dedicatedServer.Init(a.ctx) - a.backupsManager.Init(a.ctx) a.reactReady = true - - runtime.EventsEmit(a.ctx, "SET_LOADING_STATUS", "DONE") utils.LogToFile("app.go: initApp()") } func (a *App) beforeClose(ctx context.Context) (prevent bool) { - a.dedicatedServer.Dispose() - a.backupsManager.Dispose() - utils.LogToFile("app.go: beforeClose()") return false } @@ -90,3 +73,11 @@ func (a *App) GetSteamProfileURL(steamid string) { return } } + +func (a *App) SaveLog(log string) { + utils.LogToFile(log) +} + +func (a *App) OpenLogs() { + utils.OpenExplorerWithFile(utils.Config.AppDataDir, "logs.txt") +} diff --git a/build/appicon.png b/app/build/appicon.png similarity index 100% rename from build/appicon.png rename to app/build/appicon.png diff --git a/frontend/.eslintrc.cjs b/app/frontend/.eslintrc.cjs similarity index 100% rename from frontend/.eslintrc.cjs rename to app/frontend/.eslintrc.cjs diff --git a/frontend/.gitignore b/app/frontend/.gitignore similarity index 100% rename from frontend/.gitignore rename to app/frontend/.gitignore diff --git a/frontend/.npmrc b/app/frontend/.npmrc similarity index 100% rename from frontend/.npmrc rename to app/frontend/.npmrc diff --git a/frontend/.prettierignore b/app/frontend/.prettierignore similarity index 100% rename from frontend/.prettierignore rename to app/frontend/.prettierignore diff --git a/frontend/.prettierrc b/app/frontend/.prettierrc similarity index 100% rename from frontend/.prettierrc rename to app/frontend/.prettierrc diff --git a/frontend/index.html b/app/frontend/index.html similarity index 100% rename from frontend/index.html rename to app/frontend/index.html diff --git a/frontend/package.json b/app/frontend/package.json similarity index 92% rename from frontend/package.json rename to app/frontend/package.json index e00eb76..a165ad5 100644 --- a/frontend/package.json +++ b/app/frontend/package.json @@ -11,10 +11,13 @@ "framer-motion": "^10.16.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.12", "react-redux": "^8.1.3", "react-router": "^6.21.3", "react-router-dom": "^6.21.3", - "redux": "^4.2.1" + "react-toastify": "^10.0.4", + "redux": "^4.2.1", + "ws": "^8.16.0" }, "devDependencies": { "@types/react": "^18.2.34", diff --git a/app/frontend/package.json.md5 b/app/frontend/package.json.md5 new file mode 100644 index 0000000..fa9726d --- /dev/null +++ b/app/frontend/package.json.md5 @@ -0,0 +1 @@ +5a84e7b692d7b7a616c13967a1f63bfe \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/app/frontend/pnpm-lock.yaml similarity index 99% rename from frontend/pnpm-lock.yaml rename to app/frontend/pnpm-lock.yaml index 75e3f73..39979c5 100644 --- a/frontend/pnpm-lock.yaml +++ b/app/frontend/pnpm-lock.yaml @@ -26,6 +26,9 @@ dependencies: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-error-boundary: + specifier: ^4.0.12 + version: 4.0.12(react@18.2.0) react-redux: specifier: ^8.1.3 version: 8.1.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) @@ -35,9 +38,15 @@ dependencies: react-router-dom: specifier: ^6.21.3 version: 6.21.3(react-dom@18.2.0)(react@18.2.0) + react-toastify: + specifier: ^10.0.4 + version: 10.0.4(react-dom@18.2.0)(react@18.2.0) redux: specifier: ^4.2.1 version: 4.2.1 + ws: + specifier: ^8.16.0 + version: 8.16.0 devDependencies: '@types/react': @@ -3977,6 +3986,14 @@ packages: engines: { node: '>=6' } dev: false + /clsx@2.1.0: + resolution: + { + integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg== + } + engines: { node: '>=6' } + dev: false + /color-convert@2.0.1: resolution: { @@ -5532,6 +5549,18 @@ packages: scheduler: 0.23.0 dev: false + /react-error-boundary@4.0.12(react@18.2.0): + resolution: + { + integrity: sha512-kJdxdEYlb7CPC1A0SeUY38cHpjuu6UkvzKiAmqmOFL21VRfMhOcWxTCBgLVCO0VEMh9JhFNcVaXlV4/BTpiwOA== + } + peerDependencies: + react: '>=16.13.1' + dependencies: + '@babel/runtime': 7.23.2 + react: 18.2.0 + dev: false + /react-is@16.13.1: resolution: { @@ -5690,6 +5719,20 @@ packages: - '@types/react' dev: false + /react-toastify@10.0.4(react-dom@18.2.0)(react@18.2.0): + resolution: + { + integrity: sha512-etR3RgueY8pe88SA67wLm8rJmL1h+CLqUGHuAoNsseW35oTGJEri6eBTyaXnFKNQ80v/eO10hBYLgz036XRGgA== + } + peerDependencies: + react: '>=16' + react-dom: '>=16' + dependencies: + clsx: 2.1.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react@18.2.0: resolution: { @@ -6301,6 +6344,22 @@ packages: integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== } + /ws@8.16.0: + resolution: + { + integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + } + engines: { node: '>=10.0.0' } + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /yallist@4.0.0: resolution: { diff --git a/frontend/postcss.config.js b/app/frontend/postcss.config.js similarity index 100% rename from frontend/postcss.config.js rename to app/frontend/postcss.config.js diff --git a/frontend/public/vite.svg b/app/frontend/public/vite.svg similarity index 100% rename from frontend/public/vite.svg rename to app/frontend/public/vite.svg diff --git a/frontend/src/actions/app.ts b/app/frontend/src/actions/app.ts similarity index 68% rename from frontend/src/actions/app.ts rename to app/frontend/src/actions/app.ts index 6ea749d..f0b3ecd 100644 --- a/frontend/src/actions/app.ts +++ b/app/frontend/src/actions/app.ts @@ -1,12 +1,11 @@ import { appSliceActions } from '../store/app-slice'; import { store } from '../store'; -import { LoadingStatus, TSettings } from '../types'; -import { DesktopApi } from '../desktop'; +import { TSettings } from '../types'; +import { DesktopAPI } from '../desktop'; import { settingsSelector, steamImagesCacheSelector } from '../selectors/app'; - -export const setLoadingStatus = (loadingStatus: LoadingStatus) => { - store.dispatch(appSliceActions.setLoadingStatus(loadingStatus)); -}; +import { ServerAPI } from '../server'; +import { serverSliceActions } from '../store/server-slice'; +import { toast } from 'react-toastify'; export const toggleTheme = () => { store.dispatch(appSliceActions.toggleTheme()); @@ -20,6 +19,10 @@ export const setRconCredentials = (host: string, password: string) => { store.dispatch(appSliceActions.setRconCredentials({ host, password })); }; +export const setServerCredentials = (host: string, apiKey: string) => { + store.dispatch(appSliceActions.setServerCredentials({ host, apiKey })); +}; + export const saveSettings = (settings?: TSettings) => { let targetSettings; @@ -35,30 +38,25 @@ export const saveSettings = (settings?: TSettings) => { }; export const initApp = async () => { - const state = store.getState(); - const { backup } = settingsSelector(state); - - await DesktopApi.server.readConfig(); - await DesktopApi.server.readSaveName(); - - if (backup.enabled) { - await DesktopApi.backups.start(backup.intervalHours, backup.keepCount); - } + await ServerAPI.fetchConfig(); + await ServerAPI.fetchSaveName(); + await ServerAPI.backups.fetchCurrentSettings(); + await ServerAPI.backups.fetchList(); }; -export const changeBackupSettings = ( +export const changeBackupSettings = async ( enabled: boolean, intervalHours: number, keepCount: number ) => { if (enabled) { - DesktopApi.backups.start(+intervalHours, +keepCount); + await ServerAPI.backups.start(+intervalHours, +keepCount); } else { - DesktopApi.backups.stop(); + await ServerAPI.backups.stop(); } store.dispatch( - appSliceActions.setBackupSettings({ + serverSliceActions.setBackupSettings({ enabled, intervalHours, keepCount @@ -96,5 +94,13 @@ export const cacheSteamImage = async (steamId: string) => { return; } - DesktopApi.getProfileImageURL(steamId); + DesktopAPI.getProfileImageURL(steamId); +}; + +export const notifyError = (message: string) => { + toast(message, { type: 'error' }); +}; + +export const notifySuccess = (message: string) => { + toast(message, { type: 'success', autoClose: 2000 }); }; diff --git a/app/frontend/src/actions/console.ts b/app/frontend/src/actions/console.ts new file mode 100644 index 0000000..babc084 --- /dev/null +++ b/app/frontend/src/actions/console.ts @@ -0,0 +1,7 @@ +import { consolesSliceActions } from '../store/console-slice'; +import { store } from '../store'; +import { TConsoleEntry } from '../types'; + +export const addConsoleEntry = (entry: TConsoleEntry) => { + store.dispatch(consolesSliceActions.addConsoleEntry(entry)); +}; diff --git a/frontend/src/actions/modal.ts b/app/frontend/src/actions/modal.ts similarity index 100% rename from frontend/src/actions/modal.ts rename to app/frontend/src/actions/modal.ts diff --git a/frontend/src/actions/server.ts b/app/frontend/src/actions/server.ts similarity index 56% rename from frontend/src/actions/server.ts rename to app/frontend/src/actions/server.ts index 37eb33a..95748f0 100644 --- a/frontend/src/actions/server.ts +++ b/app/frontend/src/actions/server.ts @@ -2,8 +2,7 @@ import { serverSliceActions } from '../store/server-slice'; import { store } from '../store'; import { TConfig } from '../types/server-config'; import { ServerStatus } from '../types'; -import { DesktopApi } from '../desktop'; -import { saveSettings } from './app'; +import { consolesSliceActions } from '../store/console-slice'; export const setConfig = (config: TConfig) => { store.dispatch(serverSliceActions.setConfig(config)); @@ -17,20 +16,11 @@ export const setStatus = (status: ServerStatus) => { store.dispatch(serverSliceActions.setStatus(status)); }; -export const startServer = async () => { - await DesktopApi.server.start(); - - saveSettings(); -}; - -export const stopServer = async () => { - await DesktopApi.server.stop(); -}; - -export const restartServer = async () => { - await DesktopApi.server.restart(); +export const clearServerState = () => { + store.dispatch(serverSliceActions.clearServerState()); + store.dispatch(consolesSliceActions.clearConsole()); }; -export const updateServer = async () => { - await DesktopApi.server.update(); +export const setBackupsList = (backupsList: any) => { + store.dispatch(serverSliceActions.setBackupsList(backupsList)); }; diff --git a/app/frontend/src/actions/socket.ts b/app/frontend/src/actions/socket.ts new file mode 100644 index 0000000..f2b34d3 --- /dev/null +++ b/app/frontend/src/actions/socket.ts @@ -0,0 +1,93 @@ +import { socketSliceActions } from '../store/socket-slice'; +import { serverSliceActions } from '../store/server-slice'; +import { store } from '../store'; +import { + ServerStatus, + TBackup, + TBackupSettings, + TConsoleEntry +} from '../types'; +import { consolesSliceActions } from '../store/console-slice'; +import { initApp } from './app'; +import { setStatus } from './server'; +import { parseConfig } from '../helpers/config-parser'; + +export const setSocket = (socket: WebSocket) => { + store.dispatch(socketSliceActions.setSocket(socket)); +}; + +export const setSocketConnecting = (connecting: boolean) => { + store.dispatch(socketSliceActions.setConnecting(connecting)); +}; + +export const setSocketError = (error: string | boolean) => { + store.dispatch(socketSliceActions.setError(error)); +}; + +export const setSocketInited = (inited: boolean) => { + store.dispatch(socketSliceActions.setInited(inited)); +}; + +export const clearSocket = (clearError = false) => { + store.dispatch(socketSliceActions.clear()); + + if (clearError) { + store.dispatch(socketSliceActions.setError(undefined)); + } +}; + +export const onErrorReceived = (error: string) => { + store.dispatch(socketSliceActions.setError(error)); +}; + +export const onServerStatusChanged = (data: ServerStatus) => { + store.dispatch(serverSliceActions.setStatus(data)); +}; + +export const onAddConsoleEntry = (message: string) => { + const entry: TConsoleEntry = { + message, + msgType: 'stdout', + timestamp: Date.now() + }; + + store.dispatch(consolesSliceActions.addConsoleEntry(entry)); +}; + +export const onClientInited = (data) => { + setStatus(data); + setSocketInited(true); + setSocketConnecting(false); + initApp(); +}; + +export const onBackupListUpdated = (data) => { + const backups: TBackup[] = data.map((backup) => ({ + fileName: backup.Filename, + saveName: backup.SaveName, + size: backup.Size, + timestamp: backup.Timestamp + })); + + store.dispatch(serverSliceActions.setBackupsList(backups)); +}; + +export const onBackupSettingsUpdated = (data) => { + const backupsSettings: TBackupSettings = { + enabled: data.Enabled, + intervalHours: data.Interval, + keepCount: data.KeepCount + }; + + store.dispatch(serverSliceActions.setBackupSettings(backupsSettings)); +}; + +export const onServerConfigChanged = (configStr: string) => { + const config = parseConfig(configStr); + + store.dispatch(serverSliceActions.setConfig(config)); +}; + +export const onServerSaveNameChanged = (saveName: string) => { + store.dispatch(serverSliceActions.setSaveName(saveName)); +}; diff --git a/frontend/src/assets/palworld-logo.webp b/app/frontend/src/assets/palworld-logo.webp similarity index 100% rename from frontend/src/assets/palworld-logo.webp rename to app/frontend/src/assets/palworld-logo.webp diff --git a/frontend/src/components/app/index.tsx b/app/frontend/src/components/app/index.tsx similarity index 62% rename from frontend/src/components/app/index.tsx rename to app/frontend/src/components/app/index.tsx index 19cccd1..93951b7 100644 --- a/frontend/src/components/app/index.tsx +++ b/app/frontend/src/components/app/index.tsx @@ -1,17 +1,16 @@ import Routing from '../routing'; import useEventsInit from '../../hooks/use-events-init'; -import useLoadingStatus from '../../hooks/use-loading-status'; -import { LoadingStatus } from '../../types'; import Initializing from '../../screens/initializing'; import useTheme from '../../hooks/use-theme'; +import useSocket from '../../hooks/use-socket'; const App = () => { useEventsInit(); useTheme(); - const loadingStatus = useLoadingStatus(); + const { socket, inited } = useSocket(); - if (loadingStatus !== LoadingStatus.DONE) { + if (!socket || socket.readyState !== WebSocket.OPEN || !inited) { return ; } diff --git a/app/frontend/src/components/custom-error-boundary/index.tsx b/app/frontend/src/components/custom-error-boundary/index.tsx new file mode 100644 index 0000000..18607d1 --- /dev/null +++ b/app/frontend/src/components/custom-error-boundary/index.tsx @@ -0,0 +1,63 @@ +import { Button } from '@nextui-org/react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { DesktopAPI } from '../../desktop'; +import { useCallback } from 'react'; + +type TErrorBoundaryProps = { + children: React.ReactNode; +}; + +type TErrorFallbackProps = { + error: Error; + resetErrorBoundary: () => void; +}; + +const ErrorFallback = ({ error }: TErrorFallbackProps) => { + return ( +
+

ERROR

+

+ Something went wrong and the application could not recover :( +

+

Error: {error.message}

+ + + +

+ If the error persists, please open an issue on GitHub{' '} + + DesktopAPI.openUrl( + 'https://github.com/diogomartino/palworld-ds-gui/issues' + ) + } + > + here. + +

+
+ ); +}; + +const CustomErrorBoundary = ({ children }: TErrorBoundaryProps) => { + const logError = useCallback((error: Error) => { + DesktopAPI.logToFile(error.stack ?? error.message); + }, []); + + return ( + + {children} + + ); +}; + +export default CustomErrorBoundary; diff --git a/app/frontend/src/components/custom-toast-container/index.tsx b/app/frontend/src/components/custom-toast-container/index.tsx new file mode 100644 index 0000000..10f32bf --- /dev/null +++ b/app/frontend/src/components/custom-toast-container/index.tsx @@ -0,0 +1,24 @@ +import { ToastContainer, Slide } from 'react-toastify'; +import useSelectedTheme from '../../hooks/use-selected-theme'; + +const CustomToastContainer = () => { + const theme = useSelectedTheme(); + + return ( + + ); +}; + +export default CustomToastContainer; diff --git a/frontend/src/components/layout/index.tsx b/app/frontend/src/components/layout/index.tsx similarity index 100% rename from frontend/src/components/layout/index.tsx rename to app/frontend/src/components/layout/index.tsx diff --git a/frontend/src/components/modals/confirm-action/index.tsx b/app/frontend/src/components/modals/confirm-action/index.tsx similarity index 100% rename from frontend/src/components/modals/confirm-action/index.tsx rename to app/frontend/src/components/modals/confirm-action/index.tsx diff --git a/frontend/src/components/modals/index.tsx b/app/frontend/src/components/modals/index.tsx similarity index 100% rename from frontend/src/components/modals/index.tsx rename to app/frontend/src/components/modals/index.tsx diff --git a/frontend/src/components/routing/index.tsx b/app/frontend/src/components/routing/index.tsx similarity index 100% rename from frontend/src/components/routing/index.tsx rename to app/frontend/src/components/routing/index.tsx diff --git a/app/frontend/src/components/sidebar/index.tsx b/app/frontend/src/components/sidebar/index.tsx new file mode 100644 index 0000000..4a4e3b5 --- /dev/null +++ b/app/frontend/src/components/sidebar/index.tsx @@ -0,0 +1,62 @@ +import { + IconBolt, + IconDoorExit, + IconFiles, + IconInfoHexagon, + IconServer, + IconSettings, + IconSettingsUp +} from '@tabler/icons-react'; +import NavItem from './nav-item'; +import useHasUpdates from '../../hooks/use-latest-version'; +import useSocket from '../../hooks/use-socket'; + +const Sidebar = () => { + const { hasUpdates } = useHasUpdates(); + const { disconnect } = useSocket(); + + return ( +
+
+
+ + + + + + +
+ +
+ +
+
+
+ ); +}; + +export default Sidebar; diff --git a/frontend/src/components/sidebar/nav-item.tsx b/app/frontend/src/components/sidebar/nav-item.tsx similarity index 96% rename from frontend/src/components/sidebar/nav-item.tsx rename to app/frontend/src/components/sidebar/nav-item.tsx index 2e4f8ad..8c8881a 100644 --- a/frontend/src/components/sidebar/nav-item.tsx +++ b/app/frontend/src/components/sidebar/nav-item.tsx @@ -63,9 +63,10 @@ const NavItem = ({ >

{label}

diff --git a/app/frontend/src/components/socket-provider/index.tsx b/app/frontend/src/components/socket-provider/index.tsx new file mode 100644 index 0000000..370bc0b --- /dev/null +++ b/app/frontend/src/components/socket-provider/index.tsx @@ -0,0 +1,130 @@ +import { createContext } from 'react'; +import { + clearSocket, + onAddConsoleEntry, + onBackupListUpdated, + onBackupSettingsUpdated, + onServerConfigChanged, + onServerSaveNameChanged, + onServerStatusChanged, + setSocket, + setSocketConnecting, + setSocketError +} from '../../actions/socket'; +import { SocketEvent, TSocketMessage } from '../../types'; +import { ServerAPI } from '../../server'; +import { clearServerState } from '../../actions/server'; +import { useNavigate } from 'react-router'; +import { DesktopAPI } from '../../desktop'; + +type TSocketProviderProps = { + children: React.ReactNode; +}; + +type TContext = { + connect: (address: string, apiKey: string) => void; + disconnect: () => void; +}; + +export const WebSocketContext = createContext({ + connect: () => {}, + disconnect: () => {} +}); + +const SocketProvider = ({ children }: TSocketProviderProps) => { + const navigate = useNavigate(); + + const connect = (address: string = 'localhost:21577', apiKey: string) => { + clearServerState(); + clearSocket(true); + setSocketConnecting(true); + setSocketError(false); + DesktopAPI.logToFile(`Connecting to ${address}`); + + const socketUrl = `ws://${address}/ws?auth=${apiKey}`; + const socket = new WebSocket(socketUrl); + + const onOpen = () => { + DesktopAPI.logToFile(`Connected to ${address}`); + navigate('/'); + setSocket(socket); + ServerAPI.init(); + }; + + const onClose = () => { + clearSocket(); + DesktopAPI.logToFile(`Disconnected from ${address}`); + }; + + const onError = (event: Event) => { + setSocketError(true); + setSocketConnecting(false); + DesktopAPI.logToFile(`Websocket error: ${JSON.stringify(event)}`); + }; + + const onMessage = (event: MessageEvent) => { + if (event.type === 'message') { + const message: TSocketMessage = JSON.parse( + event.data || '{success: false}' + ); + + if (!message.success) { + console.error('Something went wrong', message); + } + + switch (message.event) { + case SocketEvent.SERVER_STATUS_CHANGED: + onServerStatusChanged(message.data); + break; + case SocketEvent.SERVER_CONFIG_CHANGED: + onServerConfigChanged(message.data); + break; + case SocketEvent.SERVER_SAVE_NAME_CHANGED: + onServerSaveNameChanged(message.data); + break; + case SocketEvent.ADD_CONSOLE_ENTRY: + onAddConsoleEntry(message.data); + break; + case SocketEvent.BACKUP_LIST_CHANGED: + onBackupListUpdated(message.data); + break; + case SocketEvent.BACKUP_SETTINGS_CHANGED: + onBackupSettingsUpdated(message.data); + break; + default: + break; + } + } + }; + + socket.addEventListener('open', onOpen); + socket.addEventListener('close', onClose); + socket.addEventListener('error', onError); + socket.addEventListener('message', onMessage); + + return () => { + socket.removeEventListener('open', onOpen); + socket.removeEventListener('close', onClose); + socket.removeEventListener('error', onError); + socket.removeEventListener('message', onMessage); + socket.close(); + }; + }; + + const disconnect = () => { + clearSocket(); + }; + + return ( + + {children} + + ); +}; + +export default SocketProvider; diff --git a/frontend/src/components/terminal-output/index.tsx b/app/frontend/src/components/terminal-output/index.tsx similarity index 94% rename from frontend/src/components/terminal-output/index.tsx rename to app/frontend/src/components/terminal-output/index.tsx index 0d892ea..5ea9388 100644 --- a/frontend/src/components/terminal-output/index.tsx +++ b/app/frontend/src/components/terminal-output/index.tsx @@ -16,7 +16,7 @@ const Entry = ({ entry }: TEntryProps) => { return (
- {new Date(entry.timestamp * 1000).toLocaleTimeString()} + {new Date(entry.timestamp).toLocaleTimeString()} {entry.message}
diff --git a/app/frontend/src/desktop.ts b/app/frontend/src/desktop.ts new file mode 100644 index 0000000..1bc89fc --- /dev/null +++ b/app/frontend/src/desktop.ts @@ -0,0 +1,132 @@ +import { AppEvent, TGenericFunction } from './types'; +import { EventsOff, EventsOn } from './wailsjs/runtime/runtime'; +import * as RconClient from './wailsjs/go/rconclient/RconClient'; +import * as App from './wailsjs/go/main/App'; +import { store } from './store'; +import { rconCredentialsSelector } from './selectors/app'; +import { RconCommand, TRconInfo, TRconPlayer } from './types/rcon'; + +export const DesktopAPI = { + onAppEvent: ( + event: AppEvent, + callback, + unsubscribes?: TGenericFunction[] + ) => { + EventsOn(event, callback); + + const unsubscribe = () => { + EventsOff(event, callback); + }; + + if (unsubscribes) { + unsubscribes.push(unsubscribe); + } + + return unsubscribe; + }, + openUrl: async (url: string) => { + await App.OpenInBrowser(url); + }, + initApp: async () => { + await App.InitApp(); + }, + getProfileImageURL: async (steamID64: string) => { + await App.GetSteamProfileURL(steamID64); + }, + logToFile: async (message: string) => { + await App.SaveLog(message); + }, + openLogFile: async () => { + await App.OpenLogs(); + }, + rcon: { + execute: async (command: string) => { + const state = store.getState(); + const rconCredentials = rconCredentialsSelector(state); + + const result = await RconClient.Execute( + rconCredentials.host, + rconCredentials.password, + command + ); + + return result.trim(); + }, + getInfo: async (): Promise => { + try { + const result = ( + (await DesktopAPI.rcon.execute(RconCommand.INFO)) || '' + ).trim(); + + const regex = /Welcome to Pal Server\[(.*?)\]\s*(.*)/; + const match = result.match(regex); + const [, version, name] = match || []; + + return { + version, + name + }; + } catch { + // + } + + return undefined; + }, + getPlayers: async (): Promise => { + try { + const result = ( + (await DesktopAPI.rcon.execute(RconCommand.SHOW_PLAYERS)) || '' + ).trim(); + + const lines = result.split('\n'); + + lines.shift(); // remove the first line which is the header + + const players = lines.map((line) => { + const [name, uid, steamId] = line.split(',').map((s) => s.trim()); + const player: TRconPlayer = { + name, + uid, + steamId + }; + + DesktopAPI.getProfileImageURL(steamId); // crawl steam profile image and cache the url in the store so we don't have to do it again for the same player + + return player; + }); + + return players; + } catch { + // + } + + return []; + }, + save: async () => { + await DesktopAPI.rcon.execute(RconCommand.SAVE); + }, + shutdown: async ( + message: string = 'Server is being shutdown', + seconds?: number + ) => { + const command = `${RconCommand.SHUTDOWN} ${seconds ?? 1} ${message}`; + + await DesktopAPI.rcon.execute(command); + }, + sendMessage: async (messages: string) => { + const command = `${RconCommand.BROADCAST} ${messages}`; + + await DesktopAPI.rcon.execute(command); + }, + ban: async (uid: string) => { + const command = `${RconCommand.BAN} ${uid}`; + + await DesktopAPI.rcon.execute(command); + }, + kick: async (uid: string) => { + const command = `${RconCommand.KICK} ${uid}`; + + await DesktopAPI.rcon.execute(command); + } + } +}; diff --git a/frontend/src/helpers/bytes-to-mb.ts b/app/frontend/src/helpers/bytes-to-mb.ts similarity index 100% rename from frontend/src/helpers/bytes-to-mb.ts rename to app/frontend/src/helpers/bytes-to-mb.ts diff --git a/frontend/src/helpers/config-parser.ts b/app/frontend/src/helpers/config-parser.ts similarity index 100% rename from frontend/src/helpers/config-parser.ts rename to app/frontend/src/helpers/config-parser.ts diff --git a/frontend/src/helpers/sleep.ts b/app/frontend/src/helpers/sleep.ts similarity index 100% rename from frontend/src/helpers/sleep.ts rename to app/frontend/src/helpers/sleep.ts diff --git a/app/frontend/src/hooks/use-backup-settings.ts b/app/frontend/src/hooks/use-backup-settings.ts new file mode 100644 index 0000000..ee40972 --- /dev/null +++ b/app/frontend/src/hooks/use-backup-settings.ts @@ -0,0 +1,6 @@ +import { useSelector } from 'react-redux'; +import { backupSetingsSelector } from '../selectors/server'; + +export const useBackupSettings = () => useSelector(backupSetingsSelector); + +export default useBackupSettings; diff --git a/app/frontend/src/hooks/use-backups-list.ts b/app/frontend/src/hooks/use-backups-list.ts new file mode 100644 index 0000000..ffb5d3b --- /dev/null +++ b/app/frontend/src/hooks/use-backups-list.ts @@ -0,0 +1,6 @@ +import { useSelector } from 'react-redux'; +import { serverBackupsListSelector } from '../selectors/server'; + +const useBackupsList = () => useSelector(serverBackupsListSelector); + +export default useBackupsList; diff --git a/app/frontend/src/hooks/use-console-entries.ts b/app/frontend/src/hooks/use-console-entries.ts new file mode 100644 index 0000000..785cc52 --- /dev/null +++ b/app/frontend/src/hooks/use-console-entries.ts @@ -0,0 +1,7 @@ +import { useSelector } from 'react-redux'; +import { consolesEntriesSelector } from '../selectors/console'; + +const useConsoleEntries = () => + useSelector((state: any) => consolesEntriesSelector(state)); + +export default useConsoleEntries; diff --git a/app/frontend/src/hooks/use-events-init.ts b/app/frontend/src/hooks/use-events-init.ts new file mode 100644 index 0000000..5b82122 --- /dev/null +++ b/app/frontend/src/hooks/use-events-init.ts @@ -0,0 +1,43 @@ +import { useEffect, useRef } from 'react'; +import { DesktopAPI } from '../desktop'; +import { AppEvent, TGenericFunction } from '../types'; +import { addSteamImage, checkForUpdates } from '../actions/app'; + +const CHECK_FOR_UPDATES_INTERVAL = 1000 * 60 * 60 * 24; // 1 day + +const useEventsInit = () => { + const hasInit = useRef(false); + + useEffect(() => { + if (!hasInit.current) { + DesktopAPI.initApp(); + checkForUpdates(); + + setInterval(() => { + checkForUpdates(); + }, CHECK_FOR_UPDATES_INTERVAL); + + hasInit.current = true; + } + }, []); + + useEffect(() => { + const unsubscribes: TGenericFunction[] = []; + + DesktopAPI.onAppEvent( + AppEvent.RETURN_STEAM_IMAGE, + (resultString: string) => { + const [steamId, imageUrl] = resultString.split('|'); + + addSteamImage(steamId, imageUrl); + }, + unsubscribes + ); + + return () => { + unsubscribes.forEach((unsubscribe) => unsubscribe()); + }; + }, []); +}; + +export default useEventsInit; diff --git a/frontend/src/hooks/use-latest-version.ts b/app/frontend/src/hooks/use-latest-version.ts similarity index 100% rename from frontend/src/hooks/use-latest-version.ts rename to app/frontend/src/hooks/use-latest-version.ts diff --git a/frontend/src/hooks/use-launch-params.ts b/app/frontend/src/hooks/use-launch-params.ts similarity index 100% rename from frontend/src/hooks/use-launch-params.ts rename to app/frontend/src/hooks/use-launch-params.ts diff --git a/frontend/src/hooks/use-modals-info.ts b/app/frontend/src/hooks/use-modals-info.ts similarity index 100% rename from frontend/src/hooks/use-modals-info.ts rename to app/frontend/src/hooks/use-modals-info.ts diff --git a/frontend/src/hooks/use-rcon-credentials.ts b/app/frontend/src/hooks/use-rcon-credentials.ts similarity index 100% rename from frontend/src/hooks/use-rcon-credentials.ts rename to app/frontend/src/hooks/use-rcon-credentials.ts diff --git a/frontend/src/hooks/use-selected-theme.ts b/app/frontend/src/hooks/use-selected-theme.ts similarity index 100% rename from frontend/src/hooks/use-selected-theme.ts rename to app/frontend/src/hooks/use-selected-theme.ts diff --git a/frontend/src/hooks/use-server-config.ts b/app/frontend/src/hooks/use-server-config.ts similarity index 100% rename from frontend/src/hooks/use-server-config.ts rename to app/frontend/src/hooks/use-server-config.ts diff --git a/app/frontend/src/hooks/use-server-credentials.ts b/app/frontend/src/hooks/use-server-credentials.ts new file mode 100644 index 0000000..7f37078 --- /dev/null +++ b/app/frontend/src/hooks/use-server-credentials.ts @@ -0,0 +1,6 @@ +import { useSelector } from 'react-redux'; +import { serverCredentialsSelector } from '../selectors/app'; + +const useServerCredentials = () => useSelector(serverCredentialsSelector); + +export default useServerCredentials; diff --git a/frontend/src/hooks/use-server-save-name.ts.ts b/app/frontend/src/hooks/use-server-save-name.ts.ts similarity index 100% rename from frontend/src/hooks/use-server-save-name.ts.ts rename to app/frontend/src/hooks/use-server-save-name.ts.ts diff --git a/frontend/src/hooks/use-server-status.ts b/app/frontend/src/hooks/use-server-status.ts similarity index 100% rename from frontend/src/hooks/use-server-status.ts rename to app/frontend/src/hooks/use-server-status.ts diff --git a/frontend/src/hooks/use-settings.ts b/app/frontend/src/hooks/use-settings.ts similarity index 100% rename from frontend/src/hooks/use-settings.ts rename to app/frontend/src/hooks/use-settings.ts diff --git a/app/frontend/src/hooks/use-socket.ts b/app/frontend/src/hooks/use-socket.ts new file mode 100644 index 0000000..615d986 --- /dev/null +++ b/app/frontend/src/hooks/use-socket.ts @@ -0,0 +1,12 @@ +import { useSelector } from 'react-redux'; +import { socketStateSelector } from '../selectors/socket'; +import useWebSocketContext from './use-websocket-context'; + +const useSocket = () => { + const { connect, disconnect } = useWebSocketContext(); + const socketData = useSelector(socketStateSelector); + + return { connect, disconnect, ...socketData }; +}; + +export default useSocket; diff --git a/frontend/src/hooks/use-steam-images.ts b/app/frontend/src/hooks/use-steam-images.ts similarity index 100% rename from frontend/src/hooks/use-steam-images.ts rename to app/frontend/src/hooks/use-steam-images.ts diff --git a/frontend/src/hooks/use-theme.ts b/app/frontend/src/hooks/use-theme.ts similarity index 100% rename from frontend/src/hooks/use-theme.ts rename to app/frontend/src/hooks/use-theme.ts diff --git a/app/frontend/src/hooks/use-websocket-context.ts b/app/frontend/src/hooks/use-websocket-context.ts new file mode 100644 index 0000000..0f05381 --- /dev/null +++ b/app/frontend/src/hooks/use-websocket-context.ts @@ -0,0 +1,8 @@ +import { useContext } from 'react'; +import { WebSocketContext } from '../components/socket-provider'; + +const useWebSocketContext = () => { + return useContext(WebSocketContext); +}; + +export default useWebSocketContext; diff --git a/frontend/src/main.css b/app/frontend/src/main.css similarity index 100% rename from frontend/src/main.css rename to app/frontend/src/main.css diff --git a/app/frontend/src/main.tsx b/app/frontend/src/main.tsx new file mode 100644 index 0000000..8201d8e --- /dev/null +++ b/app/frontend/src/main.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './components/app'; +import { store } from './store'; +import { Provider } from 'react-redux'; +import { NextUIProvider } from '@nextui-org/react'; +import './main.css'; +import 'react-toastify/dist/ReactToastify.css'; +import ModalsProvider from './components/modals'; +import { BrowserRouter } from 'react-router-dom'; +import SocketProvider from './components/socket-provider'; +import CustomErrorBoundary from './components/custom-error-boundary'; +import CustomToastContainer from './components/custom-toast-container'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + + + + + + + + + +); diff --git a/frontend/src/screens/about/index.tsx b/app/frontend/src/screens/about/index.tsx similarity index 91% rename from frontend/src/screens/about/index.tsx rename to app/frontend/src/screens/about/index.tsx index b5de527..83dab18 100644 --- a/frontend/src/screens/about/index.tsx +++ b/app/frontend/src/screens/about/index.tsx @@ -1,6 +1,6 @@ import { Button } from '@nextui-org/react'; import Layout from '../../components/layout'; -import { DesktopApi } from '../../desktop'; +import { DesktopAPI } from '../../desktop'; import useHasUpdates from '../../hooks/use-latest-version'; import { checkForUpdates } from '../../actions/app'; import { useState } from 'react'; @@ -28,7 +28,7 @@ const About = () => { - DesktopApi.openUrl( + DesktopAPI.openUrl( 'https://github.com/diogomartino/palworld-ds-gui/releases/latest' ) } @@ -59,7 +59,7 @@ const About = () => { - DesktopApi.openUrl( + DesktopAPI.openUrl( 'https://github.com/diogomartino/palworld-ds-gui' ) } @@ -77,7 +77,7 @@ const About = () => { - DesktopApi.openUrl('https://opensource.org/licenses/MIT') + DesktopAPI.openUrl('https://opensource.org/licenses/MIT') } > MIT License @@ -88,7 +88,7 @@ const About = () => { Created by{' '} DesktopApi.openUrl('https://github.com/diogomartino')} + onClick={() => DesktopAPI.openUrl('https://github.com/diogomartino')} > Diogo Martino diff --git a/frontend/src/screens/admin/index.tsx b/app/frontend/src/screens/admin/index.tsx similarity index 93% rename from frontend/src/screens/admin/index.tsx rename to app/frontend/src/screens/admin/index.tsx index 31e1210..bb596be 100644 --- a/frontend/src/screens/admin/index.tsx +++ b/app/frontend/src/screens/admin/index.tsx @@ -16,7 +16,7 @@ import { getKeyValue } from '@nextui-org/react'; import Layout from '../../components/layout'; -import { DesktopApi } from '../../desktop'; +import { DesktopAPI } from '../../desktop'; import { useEffect, useState } from 'react'; import useSteamImages from '../../hooks/use-steam-images'; import { @@ -35,12 +35,7 @@ import { TRconInfo, TRconPlayer } from '../../types/rcon'; import useServerConfig from '../../hooks/use-server-config'; import { ConfigKey } from '../../types/server-config'; import useRconCredentials from '../../hooks/use-rcon-credentials'; -import { setRconCredentials } from '../../actions/app'; - -/* -name,playeruid,steamid -"bruxo","AAAAAAAA","76561198032964582" -*/ +import { notifySuccess, setRconCredentials } from '../../actions/app'; const columns = [ { @@ -80,7 +75,7 @@ const AdminActions = ({ player }: TAdminActionsProps) => { confirmLabel: 'Ban', variant: 'danger', onConfirm: async () => { - await DesktopApi.rcon.ban(player.uid); + await DesktopAPI.rcon.ban(player.uid); } }); }; @@ -92,7 +87,7 @@ const AdminActions = ({ player }: TAdminActionsProps) => { confirmLabel: 'Kick', variant: 'danger', onConfirm: async () => { - await DesktopApi.rcon.kick(player.uid); + await DesktopAPI.rcon.kick(player.uid); } }); }; @@ -142,7 +137,7 @@ const Admin = () => { const loadInfo = async () => { setIsOnline(false); - const result = await DesktopApi.rcon.getInfo(); + const result = await DesktopAPI.rcon.getInfo(); if (result?.name) { setIsOnline(true); @@ -157,7 +152,7 @@ const Admin = () => { const loadPlayers = async () => { setLoading(true); - const players = await DesktopApi.rcon.getPlayers(); + const players = await DesktopAPI.rcon.getPlayers(); const processedPlayers = players.map((player) => ({ ...player, key: player.uid @@ -168,21 +163,21 @@ const Admin = () => { }; const onSendMessageClick = async () => { - await DesktopApi.rcon.sendMessage(message); - + await DesktopAPI.rcon.sendMessage(message); setMessage(''); + notifySuccess('Message sent'); }; const onSaveClick = async () => { setIsSaving(true); - - await DesktopApi.rcon.save(); - + await DesktopAPI.rcon.save(); setIsSaving(false); + notifySuccess('Save command executed'); }; - const onRefreshClick = () => { - loadInfo(); + const onRefreshClick = async () => { + await loadInfo(); + notifySuccess('Data refreshed'); }; const onConnectClick = async () => { @@ -336,7 +331,7 @@ const Admin = () => { - DesktopApi.openUrl( + DesktopAPI.openUrl( `https://steamcommunity.com/profiles/${item.steamId}` ) } diff --git a/app/frontend/src/screens/app-settings/index.tsx b/app/frontend/src/screens/app-settings/index.tsx new file mode 100644 index 0000000..4a71937 --- /dev/null +++ b/app/frontend/src/screens/app-settings/index.tsx @@ -0,0 +1,69 @@ +import { Button } from '@nextui-org/react'; +import Layout from '../../components/layout'; +import { notifySuccess, toggleTheme } from '../../actions/app'; +import useSelectedTheme from '../../hooks/use-selected-theme'; +import { + IconClearAll, + IconMoon, + IconSun, + IconWriting +} from '@tabler/icons-react'; +import { DesktopAPI } from '../../desktop'; +import { requestConfirmation } from '../../actions/modal'; + +const AppSettings = () => { + const theme = useSelectedTheme(); + + const onClearCacheClick = async () => { + requestConfirmation({ + title: 'Clear Data', + message: + 'Are you sure you want to clear your data? All your app settings will be lost. The data of your dedicated server will NOT be affected. This can help to solve some issues. You will need to restart the app after clearing the data.', + onConfirm: () => { + localStorage.clear(); + notifySuccess('Data cleared. Please restart the app.'); + } + }); + }; + + return ( + +
+ + + + + +
+
+ ); +}; + +export default AppSettings; diff --git a/frontend/src/screens/backups/index.tsx b/app/frontend/src/screens/backups/index.tsx similarity index 66% rename from frontend/src/screens/backups/index.tsx rename to app/frontend/src/screens/backups/index.tsx index e0674c9..d1aba8d 100644 --- a/frontend/src/screens/backups/index.tsx +++ b/app/frontend/src/screens/backups/index.tsx @@ -16,20 +16,21 @@ import { getKeyValue } from '@nextui-org/react'; import Layout from '../../components/layout'; -import { DesktopApi } from '../../desktop'; import { useEffect, useMemo, useState } from 'react'; -import { GetBackupsList } from '../../wailsjs/go/backupsmanager/BackupManager'; import bytesToMb from '../../helpers/bytes-to-mb'; import { + IconArchive, + IconDeviceFloppy, IconDotsVertical, - IconFolderOpen, IconRestore, IconTrash } from '@tabler/icons-react'; import { requestConfirmation } from '../../actions/modal'; import { TGenericObject } from '../../types'; -import useSettings from '../../hooks/use-settings'; import { changeBackupSettings } from '../../actions/app'; +import { ServerAPI } from '../../server'; +import useBackupsList from '../../hooks/use-backups-list'; +import useBackupSettings from '../../hooks/use-backup-settings'; const columns = [ { @@ -54,10 +55,9 @@ const columns = [ type TBackupActionsProps = { backup: TGenericObject; - loadList: () => void; }; -const BackupActions = ({ backup, loadList }: TBackupActionsProps) => { +const BackupActions = ({ backup }: TBackupActionsProps) => { const onRestoreClick = async () => { await requestConfirmation({ title: 'Confirmation', @@ -65,7 +65,7 @@ const BackupActions = ({ backup, loadList }: TBackupActionsProps) => { 'Are you sure you want to restore this backup? This action is irreversible. Make sure you have a backup of your current save.', confirmLabel: 'Restore', onConfirm: async () => { - await DesktopApi.backups.restore(backup.originalName); + await ServerAPI.backups.restore(backup.originalName); } }); }; @@ -78,8 +78,7 @@ const BackupActions = ({ backup, loadList }: TBackupActionsProps) => { confirmLabel: 'Delete', variant: 'danger', onConfirm: async () => { - await DesktopApi.backups.delete(backup.originalName); - await loadList(); + await ServerAPI.backups.delete(backup.originalName); } }); }; @@ -92,15 +91,6 @@ const BackupActions = ({ backup, loadList }: TBackupActionsProps) => { - } - onClick={() => { - DesktopApi.backups.open(backup.originalName); - }} - > - Show in Explorer - } @@ -123,73 +113,109 @@ const BackupActions = ({ backup, loadList }: TBackupActionsProps) => { }; const Backups = () => { - const { backup: currentBackupSettings } = useSettings(); - const [backups, setBackups] = useState([]); - const [loading, setLoading] = useState(true); + const currentBackupSettings = useBackupSettings(); + const backups = useBackupsList(); const [isCreating, setIsCreating] = useState(false); + const [isSaving, setIsSaving] = useState(false); const [settings, setSettings] = useState({ enabled: currentBackupSettings.enabled, intervalHours: currentBackupSettings.intervalHours, keepCount: currentBackupSettings.keepCount }); + const [errors, setErrors] = useState({}); const rows = useMemo(() => { return backups.map((backup) => ({ - key: backup.Filename, - originalName: backup.Filename, - save: `${backup.SaveName.substring(0, 12)}...${backup.SaveName.substring( - backup.SaveName.length - 12 + key: backup.fileName, + originalName: backup.fileName, + save: `${backup.saveName.substring(0, 12)}...${backup.saveName.substring( + backup.saveName.length - 12 )}`, - date: new Date(backup.Timestamp * 1000).toLocaleString(), - size: `${bytesToMb(backup.Size)} MB` + date: new Date(backup.timestamp * 1000).toLocaleString(), + size: `${bytesToMb(backup.size)} MB` })); }, [backups]); const onCreateBackupClick = async () => { setIsCreating(true); - await DesktopApi.backups.create(); - await loadList(); + await ServerAPI.backups.create(); setIsCreating(false); }; - const loadList = async () => { - setLoading(true); - setBackups(await GetBackupsList()); - setLoading(false); + const onSaveSettingsClick = async () => { + const newErrors: TGenericObject = {}; + + if ( + isNaN(settings.intervalHours) || + settings.intervalHours < 1 || + settings.intervalHours > 24 + ) { + newErrors.intervalHours = true; + } + + if ( + isNaN(settings.keepCount) || + settings.keepCount < 1 || + settings.keepCount > 100 + ) { + newErrors.keepCount = true; + } + + if (Object.keys(newErrors).length > 0) { + setErrors(newErrors); + return; + } + + setIsSaving(true); + await changeBackupSettings( + settings.enabled, + settings.intervalHours, + settings.keepCount + ); + setIsSaving(false); }; const onSettingsChange = (key: string, value: any) => { const newState = { ...settings, [key]: value }; - if (key === 'intervalHours') { - if (value < 1) newState.intervalHours = 1; - if (value > 24) newState.intervalHours = 24; - } else if (key === 'keepCount') { - if (value < 1) newState.keepCount = 1; - if (value > 20) newState.keepCount = 20; - } - + setErrors((prev) => { + const newErrors = { ...prev }; + delete newErrors[key]; + return newErrors; + }); setSettings(newState); - changeBackupSettings( - newState.enabled, - newState.intervalHours, - newState.keepCount - ); }; useEffect(() => { - loadList(); - }, []); + setSettings({ + enabled: currentBackupSettings.enabled, + intervalHours: currentBackupSettings.intervalHours, + keepCount: currentBackupSettings.keepCount + }); + }, [currentBackupSettings]); return ( } + > + Create new backup now + + } >
{ {
onSettingsChange('enabled', Boolean(!settings.enabled)) } @@ -225,10 +253,11 @@ const Backups = () => {
@@ -239,7 +268,6 @@ const Backups = () => { } className="overflow-y-scroll" > @@ -249,7 +277,7 @@ const Backups = () => { if (columnKey === 'actions') { return ( - + ); } diff --git a/frontend/src/screens/home/index.tsx b/app/frontend/src/screens/home/index.tsx similarity index 89% rename from frontend/src/screens/home/index.tsx rename to app/frontend/src/screens/home/index.tsx index 0b8426e..9de563c 100644 --- a/frontend/src/screens/home/index.tsx +++ b/app/frontend/src/screens/home/index.tsx @@ -9,19 +9,14 @@ import { import { ConfigKey } from '../../types/server-config'; import TerminalOutput from '../../components/terminal-output'; import useServerStatus from '../../hooks/use-server-status'; -import { ConsoleId, ServerStatus } from '../../types'; -import { - restartServer, - startServer, - stopServer, - updateServer -} from '../../actions/server'; -import useConsolesById from '../../hooks/use-consoles-by-id'; +import { ServerStatus } from '../../types'; +import useConsolesById from '../../hooks/use-console-entries'; import { IconRefresh } from '@tabler/icons-react'; import { requestConfirmation } from '../../actions/modal'; -import { DesktopApi } from '../../desktop'; +import { DesktopAPI } from '../../desktop'; import useLaunchParams from '../../hooks/use-launch-params'; import { setLaunchParams } from '../../actions/app'; +import { ServerAPI } from '../../server'; const statusDict = { [ServerStatus.STARTED]: 'Server is running', @@ -35,7 +30,7 @@ const statusDict = { const Home = () => { const currentConfig = useServerConfig(); const status = useServerStatus(); - const consoleEntries = useConsolesById([ConsoleId.SERVER]); + const consoleEntries = useConsolesById(); const launchParams = useLaunchParams(); const startDisabled = status !== ServerStatus.STOPPED; @@ -57,7 +52,7 @@ const Home = () => { + +
+

+ If you need help, please read the quick start guide here. +

+
+
+ + + ); +}; + +export default Initializing; diff --git a/frontend/src/screens/server-settings/index.tsx b/app/frontend/src/screens/server-settings/index.tsx similarity index 83% rename from frontend/src/screens/server-settings/index.tsx rename to app/frontend/src/screens/server-settings/index.tsx index cd18eee..5647ae4 100644 --- a/frontend/src/screens/server-settings/index.tsx +++ b/app/frontend/src/screens/server-settings/index.tsx @@ -2,10 +2,11 @@ import { Button, Divider, Input, Switch } from '@nextui-org/react'; import Layout from '../../components/layout'; import useServerConfig from '../../hooks/use-server-config'; import { configLabels, configTypes } from '../../types/server-config'; -import { useState } from 'react'; -import { DesktopApi } from '../../desktop'; +import { useEffect, useState } from 'react'; import useServerSaveName from '../../hooks/use-server-save-name.ts'; import { TGenericObject } from '../../types/index.ts'; +import { ServerAPI } from '../../server.ts'; +import { notifySuccess } from '../../actions/app.ts'; const InputProvider = ({ label, @@ -21,7 +22,12 @@ const InputProvider = ({ if (type === 'boolean') { return ( - + {label} ); @@ -57,15 +63,22 @@ const ServerSettings = () => { if (!validate()) return; setIsSaving(true); - await DesktopApi.server.writeConfig(config); + await ServerAPI.writeConfig(config); if (saveName) { - await DesktopApi.server.writeSaveName(saveName); + await ServerAPI.writeSaveName(saveName); } setIsSaving(false); + notifySuccess('Server settings saved'); }; + useEffect(() => { + // trigger a re-render when the currentConfig or currentSaveName changes (eg: other client changed the config) + setConfig(currentConfig); + setSaveName(currentSaveName); + }, [currentConfig, currentSaveName]); + return ( { {config && Object.keys(config).map((key) => ( state.app.settings.theme; export const settingsSelector = (state: IRootState) => state.app.settings; -export const loadingStatusSelector = (state: IRootState) => - state.app.loadingStatus; - export const latestVersionSelector = (state: IRootState) => state.app.latestVersion; @@ -18,3 +15,6 @@ export const launchParamsSelector = (state: IRootState) => export const rconCredentialsSelector = (state: IRootState) => state.app.rconCredentials; + +export const serverCredentialsSelector = (state: IRootState) => + state.app.settings.serverCredentials; diff --git a/app/frontend/src/selectors/console.ts b/app/frontend/src/selectors/console.ts new file mode 100644 index 0000000..94f764d --- /dev/null +++ b/app/frontend/src/selectors/console.ts @@ -0,0 +1,4 @@ +import { IRootState } from '../store'; + +export const consolesEntriesSelector = (state: IRootState) => + state.consoles.entries; diff --git a/frontend/src/selectors/modals.ts b/app/frontend/src/selectors/modals.ts similarity index 100% rename from frontend/src/selectors/modals.ts rename to app/frontend/src/selectors/modals.ts diff --git a/frontend/src/selectors/server.ts b/app/frontend/src/selectors/server.ts similarity index 60% rename from frontend/src/selectors/server.ts rename to app/frontend/src/selectors/server.ts index f9d2000..e0cb335 100644 --- a/frontend/src/selectors/server.ts +++ b/app/frontend/src/selectors/server.ts @@ -6,3 +6,9 @@ export const serverStatusSelector = (state: IRootState) => state.server.status; export const serverSaveNameSelector = (state: IRootState) => state.server.saveName; + +export const serverBackupsListSelector = (state: IRootState) => + state.server.backupsList; + +export const backupSetingsSelector = (state: IRootState) => + state.server.backupSettings; diff --git a/app/frontend/src/selectors/socket.ts b/app/frontend/src/selectors/socket.ts new file mode 100644 index 0000000..dd38075 --- /dev/null +++ b/app/frontend/src/selectors/socket.ts @@ -0,0 +1,3 @@ +import { IRootState } from '../store'; + +export const socketStateSelector = (state: IRootState) => state.socket; diff --git a/app/frontend/src/server.ts b/app/frontend/src/server.ts new file mode 100644 index 0000000..f158383 --- /dev/null +++ b/app/frontend/src/server.ts @@ -0,0 +1,176 @@ +import { SocketAction, TGenericObject } from './types'; +import { parseConfig, serializeConfig } from './helpers/config-parser'; +import { setConfig, setSaveName } from './actions/server'; +import { ConfigKey, TConfig } from './types/server-config'; +import { store } from './store'; +import { + notifyError, + notifySuccess, + saveSettings, + setRconCredentials +} from './actions/app'; +import { socketStateSelector } from './selectors/socket'; +import { + onBackupListUpdated, + onBackupSettingsUpdated, + onClientInited +} from './actions/socket'; +import { DesktopAPI } from './desktop'; + +export const ServerAPI = { + send: async ( + event: SocketAction, + data?: TGenericObject, + waitForResponse = true + ): Promise => { + return new Promise((resolve, reject) => { + const state = store.getState(); + const { socket } = socketStateSelector(state); + + // TODO: add timeout + + try { + const eventId = Math.random().toString(36).substring(2); + + socket.send(JSON.stringify({ event, eventId, data })); + + if (!waitForResponse) { + resolve({}); + return; + } + + const onMessage = (event) => { + const response = JSON.parse(event.data); + + if (response.eventId === eventId) { + socket.removeEventListener('message', onMessage); + + delete response.eventId; + delete response.event; + + if (response.success) { + resolve(response); + } else { + DesktopAPI.logToFile( + `Socket responded with error: ${response.error} for event: ${event} and data: ${data}` + ); + reject(response.error ?? 'Unknown error'); + } + } + }; + + socket.addEventListener('message', onMessage); + } catch (error) { + DesktopAPI.logToFile('Unknown error while handling socket event'); + reject(error ?? 'Unknown error'); + } + }); + }, + fetchConfig: async () => { + const { data: configString } = await ServerAPI.send( + SocketAction.READ_CONFIG + ); + const config = parseConfig(configString); + + setConfig(config); + setRconCredentials( + `127.0.0.1:${config[ConfigKey.RCONPort]}`, + config[ConfigKey.AdminPassword] + ); + }, + writeConfig: async (config: TConfig) => { + const serializedConfig = serializeConfig(config); + await ServerAPI.send(SocketAction.WRITE_CONFIG, { + config: serializedConfig + }); + + // // Read again to confirm and update the store + // await ServerAPI.fetchConfig(); + }, + fetchSaveName: async () => { + const { data: saveNameString } = await ServerAPI.send( + SocketAction.READ_SAVE_NAME + ); + + setSaveName(saveNameString); + }, + writeSaveName: async (saveName: string) => { + await ServerAPI.send(SocketAction.WRITE_SAVE_NAME, { saveName }); + + // Read again to confirm and update the store + await ServerAPI.fetchSaveName(); + }, + start: async () => { + ServerAPI.send(SocketAction.START_SERVER); + saveSettings(); + }, + stop: () => { + ServerAPI.send(SocketAction.STOP_SERVER); + }, + restart: () => { + ServerAPI.send(SocketAction.RESTART_SERVER); + }, + update: () => { + ServerAPI.send(SocketAction.UPDATE_SERVER); + }, + init: async () => { + const { data } = await ServerAPI.send(SocketAction.INIT); + + onClientInited(data); + }, + backups: { + start: async (interval: number, keepCount: number) => { + try { + await ServerAPI.send(SocketAction.START_BACKUPS, { + interval, + keepCount + }); + notifySuccess('Backups are now enabled'); + } catch (error) { + notifyError('Could not start backups'); + } + }, + stop: async () => { + try { + await ServerAPI.send(SocketAction.STOP_BACKUPS); + notifySuccess('Backups are now disabled'); + } catch { + notifyError('Could not stop backups'); + } + }, + fetchCurrentSettings: async () => { + const { data } = await ServerAPI.send(SocketAction.GET_BACKUPS_SETTINGS); + + onBackupSettingsUpdated(data); + }, + fetchList: async () => { + const { data } = await ServerAPI.send(SocketAction.GET_BACKUPS_LIST); + + onBackupListUpdated(data); + }, + delete: async (backupFileName: string) => { + try { + await ServerAPI.send(SocketAction.DELETE_BACKUP, { backupFileName }); + notifySuccess('Backup deleted'); + } catch { + notifyError('Could not delete backup'); + } + }, + create: async () => { + try { + await ServerAPI.send(SocketAction.CREATE_BACKUP); + notifySuccess('Backup created'); + } catch { + notifyError('Could not create backup'); + } + }, + restore: async (backupFileName: string) => { + try { + await ServerAPI.send(SocketAction.RESTORE_BACKUP, { backupFileName }); + notifySuccess('Backup restored'); + } catch { + notifyError('Could not restore backup'); + } + } + } +}; diff --git a/frontend/src/store/app-slice.ts b/app/frontend/src/store/app-slice.ts similarity index 74% rename from frontend/src/store/app-slice.ts rename to app/frontend/src/store/app-slice.ts index 6b64487..60245cb 100644 --- a/frontend/src/store/app-slice.ts +++ b/app/frontend/src/store/app-slice.ts @@ -1,5 +1,6 @@ +import WebSocket from 'ws'; import { createSlice } from '@reduxjs/toolkit'; -import { LoadingStatus, TSettings, TSteamImageMap } from '../types'; +import { TSettings, TSteamImageMap } from '../types'; import { saveSettings } from '../actions/app'; const getStoredSettings = () => { @@ -7,19 +8,17 @@ const getStoredSettings = () => { return { theme: stored.theme ?? 'dark', - backup: { - enabled: stored.backup?.enabled ?? false, - intervalHours: stored.backup?.intervalHours ?? 1, - keepCount: stored.backup?.keepCount ?? 6 - }, launchParams: stored.launchParams ?? - '-useperfthreads -NoAsyncLoadingThread -UseMultithreadForDS' + '-useperfthreads -NoAsyncLoadingThread -UseMultithreadForDS', + serverCredentials: { + host: stored.serverCredentials?.host ?? '127.0.0.1:21577', + apiKey: stored.serverCredentials?.apiKey ?? '' + } } as TSettings; }; export interface IAppState { - loadingStatus: LoadingStatus; settings: TSettings; latestVersion: string; steamImagesCache: TSteamImageMap; @@ -27,10 +26,11 @@ export interface IAppState { host: string; password: string; }; + socket: WebSocket | undefined; } const initialState: IAppState = { - loadingStatus: LoadingStatus.IDLE, + socket: undefined, settings: getStoredSettings(), latestVersion: APP_VERSION, steamImagesCache: {}, @@ -44,20 +44,14 @@ export const appSlice = createSlice({ name: 'modals', initialState, reducers: { - setLoadingStatus: (state, action) => { - state.loadingStatus = action.payload; - }, toggleTheme: (state) => { state.settings.theme = state.settings.theme === 'light' ? 'dark' : 'light'; saveSettings(state.settings); }, - setBackupSettings: (state, action) => { - state.settings.backup = { - ...state.settings.backup, - ...action.payload - }; + setServerCredentials: (state, action) => { + state.settings.serverCredentials = action.payload; saveSettings(state.settings); }, @@ -72,6 +66,9 @@ export const appSlice = createSlice({ }, setRconCredentials: (state, action) => { state.rconCredentials = action.payload; + }, + setSocket: (state, action) => { + state.socket = action.payload; } } }); diff --git a/frontend/src/store/console-slice.ts b/app/frontend/src/store/console-slice.ts similarity index 54% rename from frontend/src/store/console-slice.ts rename to app/frontend/src/store/console-slice.ts index d16d8ac..6e06d56 100644 --- a/frontend/src/store/console-slice.ts +++ b/app/frontend/src/store/console-slice.ts @@ -2,24 +2,22 @@ import { createSlice } from '@reduxjs/toolkit'; import { TConsoleEntry } from '../types'; export interface IConsoleSlice { - [consoleId: string]: TConsoleEntry[]; + entries: TConsoleEntry[]; } -const initialState: IConsoleSlice = {}; +const initialState: IConsoleSlice = { + entries: [] +}; export const consoleSlice = createSlice({ name: 'consoles', initialState, reducers: { addConsoleEntry: (state, action) => { - if (!state[action.payload.consoleId]) { - state[action.payload.consoleId] = []; - } - - state[action.payload.consoleId].push(action.payload.entry); + state.entries.push(action.payload); }, - clearConsole: (state, action) => { - state[action.payload] = []; + clearConsole: (state) => { + state.entries = []; } } }); diff --git a/frontend/src/store/index.ts b/app/frontend/src/store/index.ts similarity index 84% rename from frontend/src/store/index.ts rename to app/frontend/src/store/index.ts index ec8936f..675b9be 100644 --- a/frontend/src/store/index.ts +++ b/app/frontend/src/store/index.ts @@ -3,13 +3,15 @@ import modalsSlice from './modals-slice'; import appSlice from './app-slice'; import consoleSlice from './console-slice'; import serverSlice from './server-slice'; +import socketSlice from './socket-slice'; export const store = configureStore({ reducer: { app: appSlice, consoles: consoleSlice, modals: modalsSlice, - server: serverSlice + server: serverSlice, + socket: socketSlice }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ diff --git a/frontend/src/store/modals-slice.ts b/app/frontend/src/store/modals-slice.ts similarity index 100% rename from frontend/src/store/modals-slice.ts rename to app/frontend/src/store/modals-slice.ts diff --git a/frontend/src/store/server-slice.ts b/app/frontend/src/store/server-slice.ts similarity index 56% rename from frontend/src/store/server-slice.ts rename to app/frontend/src/store/server-slice.ts index 9d1d655..bd1c5ff 100644 --- a/frontend/src/store/server-slice.ts +++ b/app/frontend/src/store/server-slice.ts @@ -1,17 +1,25 @@ import { createSlice } from '@reduxjs/toolkit'; import { TConfig } from '../types/server-config'; -import { ServerStatus } from '../types'; +import { ServerStatus, TBackup, TBackupSettings } from '../types'; export interface IServerState { config: TConfig; // PalWorldSettings.ini saveName: string; // GameUserSettings.ini -> DedicatedServerName status: ServerStatus; + backupsList: TBackup[]; + backupSettings: TBackupSettings; } const initialState: IServerState = { config: {} as TConfig, saveName: '', - status: ServerStatus.STOPPED + status: ServerStatus.STOPPED, + backupsList: [], + backupSettings: { + enabled: false, + intervalHours: 1, + keepCount: 24 + } }; export const serverSlice = createSlice({ @@ -26,6 +34,17 @@ export const serverSlice = createSlice({ }, setSaveName: (state, action) => { state.saveName = action.payload; + }, + clearServerState: (state) => { + state.config = {} as TConfig; + state.saveName = ''; + state.status = ServerStatus.STOPPED; + }, + setBackupsList: (state, action) => { + state.backupsList = action.payload; + }, + setBackupSettings: (state, action) => { + state.backupSettings = action.payload; } } }); diff --git a/app/frontend/src/store/socket-slice.ts b/app/frontend/src/store/socket-slice.ts new file mode 100644 index 0000000..eec2835 --- /dev/null +++ b/app/frontend/src/store/socket-slice.ts @@ -0,0 +1,46 @@ +import WebSocket from 'ws'; +import { createSlice } from '@reduxjs/toolkit'; + +export interface ISocketState { + socket: WebSocket | undefined; + connecting: boolean; + inited: boolean; + error: string | undefined | boolean; +} + +const initialState: ISocketState = { + socket: undefined, + connecting: false, + inited: false, + error: undefined +}; + +export const socketSlice = createSlice({ + name: 'socket', + initialState, + reducers: { + setSocket: (state, action) => { + state.error = undefined; + state.socket = action.payload; + }, + setConnecting: (state, action) => { + state.connecting = action.payload; + }, + setError: (state, action) => { + state.error = action.payload; + }, + setInited: (state, action) => { + state.inited = action.payload; + }, + clear: (state) => { + state.socket?.close(); + state.socket = undefined; + state.connecting = false; + state.inited = false; + } + } +}); + +export const socketSliceActions = socketSlice.actions; + +export default socketSlice.reducer; diff --git a/app/frontend/src/types/index.ts b/app/frontend/src/types/index.ts new file mode 100644 index 0000000..4f960ec --- /dev/null +++ b/app/frontend/src/types/index.ts @@ -0,0 +1,98 @@ +export type TTheme = 'light' | 'dark'; + +export type TGenericObject = { + [key: string]: any; +}; + +export type TGenericFunction = (...args: any[]) => any; + +export enum Modal { + ACTION_CONFIRMATION = 'ACTION_CONFIRMATION' +} + +export enum ServerStatus { + STARTED = 'STARTED', + STOPPED = 'STOPPED', + STARTING = 'STARTING', + STOPPING = 'STOPPING', + RESTARTING = 'RESTARTING', + UPDATING = 'UPDATING', + ERROR = 'ERROR' +} + +export enum AppEvent { + RETURN_STEAM_IMAGE = 'RETURN_STEAM_IMAGE' +} + +export enum SocketAction { + INIT = 'INIT', + START_SERVER = 'START_SERVER', + STOP_SERVER = 'STOP_SERVER', + RESTART_SERVER = 'RESTART_SERVER', + UPDATE_SERVER = 'UPDATE_SERVER', + READ_CONFIG = 'READ_CONFIG', + READ_SAVE_NAME = 'READ_SAVE_NAME', + WRITE_CONFIG = 'WRITE_CONFIG', + WRITE_SAVE_NAME = 'WRITE_SAVE_NAME', + START_BACKUPS = 'START_BACKUPS', + STOP_BACKUPS = 'STOP_BACKUPS', + GET_BACKUPS_LIST = 'GET_BACKUPS_LIST', + DELETE_BACKUP = 'DELETE_BACKUP', + CREATE_BACKUP = 'CREATE_BACKUP', + OPEN_BACKUP = 'OPEN_BACKUP', + RESTORE_BACKUP = 'RESTORE_BACKUP', + DOWNLOAD_BACKUP = 'DOWNLOAD_BACKUP', + GET_BACKUPS_SETTINGS = 'GET_BACKUPS_SETTINGS' +} + +export enum SocketEvent { + SERVER_STATUS_CHANGED = 'SERVER_STATUS_CHANGED', + SERVER_CONFIG_CHANGED = 'SERVER_CONFIG_CHANGED', + SERVER_SAVE_NAME_CHANGED = 'SERVER_SAVE_NAME_CHANGED', + BACKUP_LIST_CHANGED = 'BACKUP_LIST_CHANGED', + BACKUP_SETTINGS_CHANGED = 'BACKUP_SETTINGS_CHANGED', + CUSTOM_ERROR = 'CUSTOM_ERROR', + ADD_CONSOLE_ENTRY = 'ADD_CONSOLE_ENTRY' +} + +export type TConsoleEntry = { + timestamp: number; + message: string; + msgType: 'stdout' | 'stderr'; +}; + +export type TBackupSettings = { + enabled: boolean; + intervalHours: number; + keepCount: number; +}; + +export type TServerCredentials = { + host: string; + apiKey: string; +}; + +export type TSettings = { + theme: 'light' | 'dark'; + serverCredentials: TServerCredentials; + launchParams: string | undefined; +}; + +export type TSteamImageMap = { + [steamId64: string]: string; +}; + +export type TSocketMessage = { + event: SocketEvent; + eventId: string; + data: any; + success: boolean; + error: string; +}; + +export type TBackup = { + fileName: string; + saveName: string; + size: number; + timestamp: number; +}; diff --git a/frontend/src/types/rcon.ts b/app/frontend/src/types/rcon.ts similarity index 100% rename from frontend/src/types/rcon.ts rename to app/frontend/src/types/rcon.ts diff --git a/frontend/src/types/server-config.ts b/app/frontend/src/types/server-config.ts similarity index 100% rename from frontend/src/types/server-config.ts rename to app/frontend/src/types/server-config.ts diff --git a/frontend/src/vite-env.d.ts b/app/frontend/src/vite-env.d.ts similarity index 100% rename from frontend/src/vite-env.d.ts rename to app/frontend/src/vite-env.d.ts diff --git a/frontend/src/wailsjs/go/main/App.d.ts b/app/frontend/src/wailsjs/go/main/App.d.ts similarity index 74% rename from frontend/src/wailsjs/go/main/App.d.ts rename to app/frontend/src/wailsjs/go/main/App.d.ts index c11485b..da6ccd2 100644 --- a/frontend/src/wailsjs/go/main/App.d.ts +++ b/app/frontend/src/wailsjs/go/main/App.d.ts @@ -6,3 +6,7 @@ export function GetSteamProfileURL(arg1:string):Promise; export function InitApp():Promise; export function OpenInBrowser(arg1:string):Promise; + +export function OpenLogs():Promise; + +export function SaveLog(arg1:string):Promise; diff --git a/frontend/src/wailsjs/go/main/App.js b/app/frontend/src/wailsjs/go/main/App.js similarity index 70% rename from frontend/src/wailsjs/go/main/App.js rename to app/frontend/src/wailsjs/go/main/App.js index 97ea353..b076159 100644 --- a/frontend/src/wailsjs/go/main/App.js +++ b/app/frontend/src/wailsjs/go/main/App.js @@ -13,3 +13,11 @@ export function InitApp() { export function OpenInBrowser(arg1) { return window['go']['main']['App']['OpenInBrowser'](arg1); } + +export function OpenLogs() { + return window['go']['main']['App']['OpenLogs'](); +} + +export function SaveLog(arg1) { + return window['go']['main']['App']['SaveLog'](arg1); +} diff --git a/frontend/src/wailsjs/go/rconclient/RconClient.d.ts b/app/frontend/src/wailsjs/go/rconclient/RconClient.d.ts similarity index 100% rename from frontend/src/wailsjs/go/rconclient/RconClient.d.ts rename to app/frontend/src/wailsjs/go/rconclient/RconClient.d.ts diff --git a/frontend/src/wailsjs/go/rconclient/RconClient.js b/app/frontend/src/wailsjs/go/rconclient/RconClient.js similarity index 100% rename from frontend/src/wailsjs/go/rconclient/RconClient.js rename to app/frontend/src/wailsjs/go/rconclient/RconClient.js diff --git a/frontend/src/wailsjs/runtime/package.json b/app/frontend/src/wailsjs/runtime/package.json similarity index 100% rename from frontend/src/wailsjs/runtime/package.json rename to app/frontend/src/wailsjs/runtime/package.json diff --git a/frontend/src/wailsjs/runtime/runtime.d.ts b/app/frontend/src/wailsjs/runtime/runtime.d.ts similarity index 100% rename from frontend/src/wailsjs/runtime/runtime.d.ts rename to app/frontend/src/wailsjs/runtime/runtime.d.ts diff --git a/frontend/src/wailsjs/runtime/runtime.js b/app/frontend/src/wailsjs/runtime/runtime.js similarity index 100% rename from frontend/src/wailsjs/runtime/runtime.js rename to app/frontend/src/wailsjs/runtime/runtime.js diff --git a/frontend/tailwind.config.js b/app/frontend/tailwind.config.js similarity index 100% rename from frontend/tailwind.config.js rename to app/frontend/tailwind.config.js diff --git a/frontend/tsconfig.json b/app/frontend/tsconfig.json similarity index 100% rename from frontend/tsconfig.json rename to app/frontend/tsconfig.json diff --git a/frontend/tsconfig.node.json b/app/frontend/tsconfig.node.json similarity index 100% rename from frontend/tsconfig.node.json rename to app/frontend/tsconfig.node.json diff --git a/frontend/vite.config.ts b/app/frontend/vite.config.ts similarity index 100% rename from frontend/vite.config.ts rename to app/frontend/vite.config.ts diff --git a/go.mod b/app/go.mod similarity index 80% rename from go.mod rename to app/go.mod index c84334d..97bfccf 100644 --- a/go.mod +++ b/app/go.mod @@ -7,33 +7,25 @@ toolchain go1.21.3 require ( github.com/gocolly/colly/v2 v2.1.0 github.com/gorcon/rcon v1.3.4 - github.com/mholt/archiver/v3 v3.5.1 - github.com/mitchellh/go-ps v1.0.0 - github.com/robfig/cron v1.2.0 github.com/tidwall/gjson v1.17.0 github.com/wailsapp/wails/v2 v2.7.1 ) require ( github.com/PuerkitoBio/goquery v1.5.1 // indirect - github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/cascadia v1.2.0 // indirect github.com/antchfx/htmlquery v1.2.3 // indirect github.com/antchfx/xmlquery v1.2.4 // indirect github.com/antchfx/xpath v1.1.8 // indirect github.com/bep/debounce v1.2.1 // indirect - github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.4.2 // indirect - github.com/golang/snappy v0.0.4 // indirect github.com/google/uuid v1.3.0 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/kennygrant/sanitize v1.2.4 // indirect - github.com/klauspost/compress v1.17.5 // indirect - github.com/klauspost/pgzip v1.2.6 // indirect github.com/labstack/echo/v4 v4.10.2 // indirect github.com/labstack/gommon v0.4.0 // indirect github.com/leaanthony/go-ansi-parser v1.6.0 // indirect @@ -42,8 +34,6 @@ require ( github.com/leaanthony/u v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - github.com/nwaples/rardecode v1.1.3 // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.4.4 // indirect @@ -53,12 +43,10 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tkrajina/go-reflector v0.5.6 // indirect - github.com/ulikunitz/xz v0.5.11 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wailsapp/go-webview2 v1.0.10 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect golang.org/x/crypto v0.18.0 // indirect golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/net v0.20.0 // indirect diff --git a/go.sum b/app/go.sum similarity index 85% rename from go.sum rename to app/go.sum index 2aa4372..67d60a8 100644 --- a/go.sum +++ b/app/go.sum @@ -2,9 +2,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= -github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE= github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY= @@ -22,9 +19,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= -github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= -github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= @@ -51,14 +45,10 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= @@ -70,14 +60,6 @@ github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4P github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= -github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.17.5 h1:d4vBd+7CHydUqpFBgUEKkSdtSugf9YFmSkvUYPquI5E= -github.com/klauspost/compress v1.17.5/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= -github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= @@ -102,16 +84,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= -github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= -github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= -github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= -github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= -github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= -github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= -github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= -github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -122,8 +94,6 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= -github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI= github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= @@ -145,10 +115,6 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE= github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= -github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= -github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= @@ -160,8 +126,6 @@ github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhw github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/wails/v2 v2.7.1 h1:HAzp2c5ODOzsLC6ZMDVtNOB72ozM7/SJecJPB2Ur+UU= github.com/wailsapp/wails/v2 v2.7.1/go.mod h1:oIJVwwso5fdOgprBYWXBBqtx6PaSvxg8/KTQHNGkadc= -github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= -github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= diff --git a/main.go b/app/main.go similarity index 83% rename from main.go rename to app/main.go index 56d9428..bc792f3 100644 --- a/main.go +++ b/app/main.go @@ -6,10 +6,7 @@ package main import ( "embed" "os" - backupsmanager "palword-ds-gui/backups-manager" - dedicatedserver "palword-ds-gui/dedicated-server" rconclient "palword-ds-gui/rcon-client" - "palword-ds-gui/steamcmd" "palword-ds-gui/utils" "github.com/tidwall/gjson" @@ -32,6 +29,10 @@ var ( ) func main() { + if _, err := os.Stat(utils.Config.AppDataDir); os.IsNotExist(err) { + os.MkdirAll(utils.Config.AppDataDir, 0755) + } + logsFile, logErr := os.OpenFile(utils.Config.LogsPath, os.O_RDWR|os.O_CREATE, 0666) if logErr != nil { panic(logErr) @@ -42,11 +43,8 @@ func main() { version := gjson.Get(wailsJSON, "info.productVersion") utils.LogToFile("main.go: main() - Palword Dedicated Server GUI v" + version.String()) - dedicatedServer := dedicatedserver.NewDedicatedServer() - steamCmd := steamcmd.NewSteamCMD() - backupManager := backupsmanager.NewBackupManager(dedicatedServer) rconClient := rconclient.NewRconClient() - app := NewApp(dedicatedServer, steamCmd, backupManager, rconClient) + app := NewApp(rconClient) utils.LogToFile("main.go: main() - Managers created") @@ -77,8 +75,6 @@ func main() { WindowStartState: options.Normal, Bind: []interface{}{ app, - dedicatedServer, - backupManager, rconClient, }, Windows: &windows.Options{ diff --git a/rcon-client/rcon-client.go b/app/rcon-client/rcon-client.go similarity index 100% rename from rcon-client/rcon-client.go rename to app/rcon-client/rcon-client.go diff --git a/app/utils/utils.go b/app/utils/utils.go new file mode 100644 index 0000000..2b2d1d6 --- /dev/null +++ b/app/utils/utils.go @@ -0,0 +1,86 @@ +package utils + +import ( + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "time" +) + +type ConsoleEntry struct { + Message string + Timestamp int64 + MsgType string +} + +type AppConfig struct { + LogsPath string + AppDataDir string +} + +var Config AppConfig = AppConfig{ + AppDataDir: GetAppDataDir(), + LogsPath: filepath.Join(GetAppDataDir(), "logs.txt"), +} + +func GetCurrentDir() string { + ex, err := os.Executable() + + if err != nil { + panic(err) + } + + dir := filepath.Dir(ex) + + return dir +} + +func GetAppDataDir() string { + appDataDir := os.Getenv("APPDATA") + return filepath.Join(appDataDir, "PalworldDSGUI") +} + +func DownloadFile(url string, path string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + out, err := os.Create(path) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} + +func LogToFile(message string) { + logsFile, err := os.OpenFile(Config.LogsPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + panic(err) + } + + defer logsFile.Close() + + formatedMessage := fmt.Sprintf("[%s] %s", time.Now().Format("02-01-2006 15:04:05"), message) + + logsFile.WriteString(formatedMessage + "\n") +} + +func OpenExplorerWithFile(folderPath, fileName string) error { + cmd := exec.Command("explorer", "/select,", fileName) + cmd.Dir = folderPath + + err := cmd.Run() + if err != nil { + return err + } + + return nil +} diff --git a/wails.json b/app/wails.json similarity index 100% rename from wails.json rename to app/wails.json diff --git a/dedicated-server/dedicated-server.go b/dedicated-server/dedicated-server.go deleted file mode 100644 index fdc1eb4..0000000 --- a/dedicated-server/dedicated-server.go +++ /dev/null @@ -1,315 +0,0 @@ -package dedicatedserver - -import ( - "context" - "fmt" - "os" - "os/exec" - "palword-ds-gui/utils" - "regexp" - "strings" - "time" - - "github.com/mitchellh/go-ps" - "github.com/wailsapp/wails/v2/pkg/runtime" -) - -type DedicatedServer struct { - cmd *exec.Cmd - serverCmd *exec.Cmd - serverPid int - launchParams []string -} - -var ctx context.Context -var currentConsoleId string = "DEDICATED_SERVER" - -func NewDedicatedServer() *DedicatedServer { - return &DedicatedServer{} -} - -func Print(message string) { - utils.PrintEx(ctx, message, currentConsoleId) -} - -func (d *DedicatedServer) Init(srcCtx context.Context) { - ctx = srcCtx - proc, _ := utils.FindProcessByName(utils.Config.ServerProcessName) - - if proc != nil { - Print("A server is already running, killing it...") - - err := utils.KillProcessByPid(proc.Pid()) - - if err != nil { - Print("Error stopping server: " + err.Error()) - return - } - - Print("Server killed successfully") - } - - if _, err := os.Stat(utils.Config.ServerPath); os.IsNotExist(err) { - Print("Server directory not found, creating...") - runtime.EventsEmit(ctx, "SET_LOADING_STATUS", "INSTALLING_SERVER") - - os.Mkdir(utils.Config.ServerPath, 0755) - d.DownloadDedicatedServer() - } - - Print("Server is ready") -} - -func (d *DedicatedServer) DownloadDedicatedServer() { - Print("Downloading dedicated server...") - - if d.IsRunning() { - Print("Cannot update server while it's running. Stopping it...") - d.Stop() - return - } - - cmd := exec.Command(utils.Config.SteamCmdExe, - "+force_install_dir", utils.Config.ServerPath, - "+login", "anonymous", - "+app_update", utils.Config.AppId, "validate", - "+quit") - - cmd.Dir = utils.Config.SteamCmdPath - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - d.cmd = cmd - - cmd.Run() -} - -func (d *DedicatedServer) MonitorServerProcess() { - for { - time.Sleep(4 * time.Second) - - // was killed via gui - if d.serverPid == 0 { - break - } - - proc, err := utils.FindProcessByPid(d.serverPid) - - if proc == nil || err != nil { - Print("Server seems to have stopped (crashed?)") - runtime.EventsEmit(ctx, "SET_SERVER_STATUS", "STOPPED") - break - } - } -} - -func (d *DedicatedServer) Start() { - Print("Starting dedicated server...") - runtime.EventsEmit(ctx, "SET_SERVER_STATUS", "STARTING") - - d.serverCmd = exec.Command(utils.Config.ServerExe, d.launchParams...) - d.serverCmd.Dir = utils.Config.ServerPath - d.serverCmd.Stdout = os.Stdout - d.serverCmd.Stderr = os.Stderr - - err := d.serverCmd.Start() - if err != nil { - Print(err.Error()) - runtime.EventsEmit(ctx, "SET_SERVER_STATUS", "ERROR") - return - } - - var attempts int = 10 - var proc ps.Process - - whileLoop := true - for whileLoop { - time.Sleep(1 * time.Second) - - proc, err = utils.FindProcessByName(utils.Config.ServerProcessName) - if err != nil { - continue - } - - if proc != nil { - whileLoop = false - } - - attempts-- - - if attempts <= 0 { - whileLoop = false - } - } - - if proc == nil { - Print("Server process not found") - runtime.EventsEmit(ctx, "SET_SERVER_STATUS", "ERROR") - return - } - - d.serverPid = proc.Pid() - - Print("Server started") - runtime.EventsEmit(ctx, "SET_SERVER_STATUS", "STARTED") - - go d.MonitorServerProcess() -} - -func (d *DedicatedServer) Update() { - runtime.EventsEmit(ctx, "SET_SERVER_STATUS", "UPDATING") - d.DownloadDedicatedServer() - Print("Server updated") - runtime.EventsEmit(ctx, "SET_SERVER_STATUS", "STOPPED") -} - -func (d *DedicatedServer) Stop() { - if !d.IsRunning() { - return - } - - Print("Stopping dedicated server...") - runtime.EventsEmit(ctx, "SET_SERVER_STATUS", "STOPPING") - - err := utils.KillProcessByPid(d.serverPid) - if err != nil { - Print(err.Error()) - runtime.EventsEmit(ctx, "SET_SERVER_STATUS", "ERROR") - return - } - - if d.serverCmd != nil && d.serverCmd.Process != nil { - err := d.serverCmd.Process.Kill() - if err != nil { - Print(err.Error()) - runtime.EventsEmit(ctx, "SET_SERVER_STATUS", "ERROR") - return - } - } - - Print("Server stopped") - runtime.EventsEmit(ctx, "SET_SERVER_STATUS", "STOPPED") - d.serverPid = 0 -} - -func (d *DedicatedServer) IsRunning() bool { - if d.serverPid == 0 { - return false - } - - proc, _ := utils.FindProcessByPid(d.serverPid) - - return proc != nil -} - -func (d *DedicatedServer) Restart() { - Print("Restarting dedicated server...") - runtime.EventsEmit(ctx, "SET_SERVER_STATUS", "RESTARTING") - - d.Stop() - d.Start() -} - -func (d *DedicatedServer) ReadConfig() string { - configPath := utils.Config.ServerConfigPath - - // If config file doesn't exist yet, use default config - if _, err := os.Stat(configPath); os.IsNotExist(err) { - configPath = utils.Config.ServerDefaultConfigPath - } - - configData, err := os.ReadFile(configPath) - if err != nil { - panic(err) - } - - configString := strings.TrimSpace(string(configData)) - isEmpty := len(configString) == 0 - - // if the config file is empty, use default config - if isEmpty { - configData, err := os.ReadFile(utils.Config.ServerDefaultConfigPath) - - if err != nil { - panic(err) - } - - return strings.TrimSpace(string(configData)) - } - - return configString -} - -func (d *DedicatedServer) WriteConfig(configString string) { - if _, err := os.Stat(utils.Config.ServerConfigDir); os.IsNotExist(err) { - os.MkdirAll(utils.Config.ServerConfigDir, 0755) - } - - if _, err := os.Stat(utils.Config.ServerConfigPath); os.IsNotExist(err) { - - _, err := os.Create(utils.Config.ServerConfigPath) - if err != nil { - panic(err) - } - } - - err := os.WriteFile(utils.Config.ServerConfigPath, []byte(configString), 0644) - if err != nil { - panic(err) - } -} - -func (d *DedicatedServer) SetLaunchParams(params []string) { - d.launchParams = params -} - -func (d *DedicatedServer) ReadSaveName() string { - // If file doesn't exist yet, return empty string (user hasn't joined the server for the first time yet) - if _, err := os.Stat(utils.Config.ServerGameUserSettingsPath); os.IsNotExist(err) { - return "" - } - - settingsData, err := os.ReadFile(utils.Config.ServerGameUserSettingsPath) - if err != nil { - panic(err) - } - - settingsString := strings.TrimSpace(string(settingsData)) - re := regexp.MustCompile(`DedicatedServerName=([^\s]+)`) - match := re.FindStringSubmatch(settingsString) - - if len(match) == 2 { - return match[1] - } - - return "" -} - -func (d *DedicatedServer) WriteSaveName(newSaveName string) error { - settingsData, err := os.ReadFile(utils.Config.ServerGameUserSettingsPath) - if err != nil { - return err - } - - settingsString := strings.TrimSpace(string(settingsData)) - re := regexp.MustCompile(`DedicatedServerName=([^\s]+)`) - settingsString = re.ReplaceAllString(settingsString, fmt.Sprintf("DedicatedServerName=%s", newSaveName)) - - err = os.WriteFile(utils.Config.ServerGameUserSettingsPath, []byte(settingsString), os.ModePerm) - if err != nil { - return err - } - - return nil -} - -func (d *DedicatedServer) Dispose() { - if d.cmd != nil && d.cmd.Process != nil { - err := d.cmd.Process.Kill() - if err != nil { - Print(err.Error()) - } - } - - d.Stop() - utils.LogToFile("dedicated-server.go: Dispose()") -} diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 deleted file mode 100644 index cdebf51..0000000 --- a/frontend/package.json.md5 +++ /dev/null @@ -1 +0,0 @@ -b2cabdc755251f56814b02a49d3373c7 \ No newline at end of file diff --git a/frontend/src/actions/console.ts b/frontend/src/actions/console.ts deleted file mode 100644 index 5403c82..0000000 --- a/frontend/src/actions/console.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { consolesSliceActions } from '../store/console-slice'; -import { store } from '../store'; -import { ConsoleId, TConsoleEntry } from '../types'; - -export const addConsoleEntry = (consoleId: ConsoleId, entry: TConsoleEntry) => { - store.dispatch(consolesSliceActions.addConsoleEntry({ consoleId, entry })); -}; diff --git a/frontend/src/components/sidebar/index.tsx b/frontend/src/components/sidebar/index.tsx deleted file mode 100644 index 2b8c194..0000000 --- a/frontend/src/components/sidebar/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { - IconBolt, - IconFiles, - IconInfoHexagon, - IconServer, - IconSettings, - IconSettingsUp -} from '@tabler/icons-react'; -import NavItem from './nav-item'; -import useHasUpdates from '../../hooks/use-latest-version'; - -const Sidebar = () => { - const { hasUpdates } = useHasUpdates(); - - return ( -
-
- - - - - - -
-
- ); -}; - -export default Sidebar; diff --git a/frontend/src/desktop.ts b/frontend/src/desktop.ts deleted file mode 100644 index 8f5e019..0000000 --- a/frontend/src/desktop.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { AppEvent, TGenericFunction } from './types'; -import { EventsOff, EventsOn } from './wailsjs/runtime/runtime'; -import * as DedicatedServer from './wailsjs/go/dedicatedserver/DedicatedServer'; -import * as BackupManager from './wailsjs/go/backupsmanager/BackupManager'; -import * as RconClient from './wailsjs/go/rconclient/RconClient'; -import * as App from './wailsjs/go/main/App'; -import { parseConfig, serializeConfig } from './helpers/config-parser'; -import { setConfig, setSaveName } from './actions/server'; -import { ConfigKey, TConfig } from './types/server-config'; -import { store } from './store'; -import { launchParamsSelector, rconCredentialsSelector } from './selectors/app'; -import { RconCommand, TRconInfo, TRconPlayer } from './types/rcon'; -import { setRconCredentials } from './actions/app'; - -export const DesktopApi = { - onAppEvent: ( - event: AppEvent, - callback, - unsubscribes?: TGenericFunction[] - ) => { - EventsOn(event, callback); - - const unsubscribe = () => { - EventsOff(event, callback); - }; - - if (unsubscribes) { - unsubscribes.push(unsubscribe); - } - - return unsubscribe; - }, - openUrl: async (url: string) => { - await App.OpenInBrowser(url); - }, - initApp: async () => { - await App.InitApp(); - }, - getProfileImageURL: async (steamID64: string) => { - await App.GetSteamProfileURL(steamID64); - }, - server: { - readConfig: async () => { - const configString = await DedicatedServer.ReadConfig(); - const config = parseConfig(configString); - - setConfig(config); - setRconCredentials( - `127.0.0.1:${config[ConfigKey.RCONPort]}`, - config[ConfigKey.AdminPassword] - ); - }, - writeConfig: async (config: TConfig) => { - const serializedConfig = serializeConfig(config); - await DedicatedServer.WriteConfig(serializedConfig); - - // Read again to update the store - await DesktopApi.server.readConfig(); - }, - readSaveName: async () => { - const saveNameString = await DedicatedServer.ReadSaveName(); - - setSaveName(saveNameString); - }, - writeSaveName: async (saveName: string) => { - await DedicatedServer.WriteSaveName(saveName); - - // Read again to update the store - await DesktopApi.server.readSaveName(); - }, - start: async () => { - const state = store.getState(); - const params = launchParamsSelector(state); - const paramsArray = - params - ?.split(' ') - .filter((p) => p !== '') - .map((p) => p.trim()) ?? []; - - await DedicatedServer.SetLaunchParams(paramsArray); - await DedicatedServer.Start(); - }, - stop: async () => { - await DedicatedServer.Stop(); - }, - restart: async () => { - await DedicatedServer.Restart(); - }, - update: async () => { - await DedicatedServer.Update(); - } - }, - backups: { - start: async (interval: number, keepCount: number) => { - await BackupManager.Start(interval, keepCount); - }, - stop: async () => { - await BackupManager.Stop(); - }, - getList: async () => { - return await BackupManager.GetBackupsList(); - }, - delete: async (backupFileName: string) => { - await BackupManager.Delete(backupFileName); - }, - create: async () => { - await BackupManager.CreateBackup(); - }, - open: async (backupFileName: string) => { - await BackupManager.Open(backupFileName); - }, - restore: async (backupFileName: string) => { - await BackupManager.Restore(backupFileName); - } - }, - rcon: { - execute: async (command: string) => { - const state = store.getState(); - const rconCredentials = rconCredentialsSelector(state); - - const result = await RconClient.Execute( - rconCredentials.host, - rconCredentials.password, - command - ); - - return result.trim(); - }, - getInfo: async (): Promise => { - try { - const result = ( - (await DesktopApi.rcon.execute(RconCommand.INFO)) || '' - ).trim(); - - const regex = /Welcome to Pal Server\[(.*?)\]\s*(.*)/; - const match = result.match(regex); - const [, version, name] = match || []; - - return { - version, - name - }; - } catch { - // - } - - return undefined; - }, - getPlayers: async (): Promise => { - try { - const result = ( - (await DesktopApi.rcon.execute(RconCommand.SHOW_PLAYERS)) || '' - ).trim(); - - const lines = result.split('\n'); - - lines.shift(); // remove the first line which is the header - - const players = lines.map((line) => { - const [name, uid, steamId] = line.split(',').map((s) => s.trim()); - const player: TRconPlayer = { - name, - uid, - steamId - }; - - DesktopApi.getProfileImageURL(steamId); // crawl steam profile image and cache the url in the store so we don't have to do it again for the same player - - return player; - }); - - return players; - } catch { - // - } - - return []; - }, - save: async () => { - await DesktopApi.rcon.execute(RconCommand.SAVE); - }, - shutdown: async ( - message: string = 'Server is being shutdown', - seconds?: number - ) => { - const command = `${RconCommand.SHUTDOWN} ${seconds ?? 1} ${message}`; - - await DesktopApi.rcon.execute(command); - }, - sendMessage: async (messages: string) => { - const command = `${RconCommand.BROADCAST} ${messages}`; - - await DesktopApi.rcon.execute(command); - }, - ban: async (uid: string) => { - const command = `${RconCommand.BAN} ${uid}`; - - await DesktopApi.rcon.execute(command); - }, - kick: async (uid: string) => { - const command = `${RconCommand.KICK} ${uid}`; - - await DesktopApi.rcon.execute(command); - } - } -}; diff --git a/frontend/src/hooks/use-consoles-by-id.ts b/frontend/src/hooks/use-consoles-by-id.ts deleted file mode 100644 index ea6787d..0000000 --- a/frontend/src/hooks/use-consoles-by-id.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { useSelector } from 'react-redux'; -import { ConsoleId } from '../types'; -import { consolesByIdSelector } from '../selectors/console'; - -const useConsolesById = (ids: ConsoleId[]) => - useSelector((state: any) => consolesByIdSelector(state, ids)); - -export default useConsolesById; diff --git a/frontend/src/hooks/use-events-init.ts b/frontend/src/hooks/use-events-init.ts deleted file mode 100644 index 7d4e28b..0000000 --- a/frontend/src/hooks/use-events-init.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { DesktopApi } from '../desktop'; -import { - AppEvent, - ConsoleId, - LoadingStatus, - ServerStatus, - TConsoleEntry, - TGenericFunction, - TGenericObject -} from '../types'; -import { - addSteamImage, - checkForUpdates, - initApp, - setLoadingStatus -} from '../actions/app'; -import { addConsoleEntry } from '../actions/console'; -import { setStatus } from '../actions/server'; - -const CHECK_FOR_UPDATES_INTERVAL = 1000 * 60 * 60 * 24; // 1 day - -const useEventsInit = () => { - const hasInit = useRef(false); - - useEffect(() => { - if (!hasInit.current) { - DesktopApi.initApp(); - checkForUpdates(); - - setInterval(() => { - checkForUpdates(); - }, CHECK_FOR_UPDATES_INTERVAL); - - hasInit.current = true; - } - }, []); - - useEffect(() => { - const unsubscribes: TGenericFunction[] = []; - - DesktopApi.onAppEvent( - AppEvent.RETURN_STEAM_IMAGE, - (resultString: string) => { - const [steamId, imageUrl] = resultString.split('|'); - - addSteamImage(steamId, imageUrl); - }, - unsubscribes - ); - - DesktopApi.onAppEvent( - AppEvent.SET_LOADING_STATUS, - (status: LoadingStatus) => { - setLoadingStatus(status); - - if (status === LoadingStatus.DONE) { - initApp(); - } - }, - unsubscribes - ); - - DesktopApi.onAppEvent( - AppEvent.ADD_CONSOLE_ENTRY, - (consoleId: ConsoleId, entry: TGenericObject) => { - const entryObj: TConsoleEntry = { - timestamp: entry.Timestamp, - message: entry.Message, - msgType: entry.MsgType - }; - - addConsoleEntry(consoleId, entryObj); - }, - unsubscribes - ); - - DesktopApi.onAppEvent( - AppEvent.SET_SERVER_STATUS, - (status: ServerStatus) => { - setStatus(status); - }, - unsubscribes - ); - - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); - }; - }, []); -}; - -export default useEventsInit; diff --git a/frontend/src/hooks/use-loading-status.ts b/frontend/src/hooks/use-loading-status.ts deleted file mode 100644 index fb0cde3..0000000 --- a/frontend/src/hooks/use-loading-status.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { useSelector } from 'react-redux'; -import { loadingStatusSelector } from '../selectors/app'; - -const useLoadingStatus = () => useSelector(loadingStatusSelector); - -export default useLoadingStatus; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx deleted file mode 100644 index bf7aee7..0000000 --- a/frontend/src/main.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './components/app'; -import { store } from './store'; -import { Provider } from 'react-redux'; -import { NextUIProvider } from '@nextui-org/react'; -import './main.css'; -import ModalsProvider from './components/modals'; -import { BrowserRouter } from 'react-router-dom'; - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - - - - - - -); diff --git a/frontend/src/screens/app-settings/index.tsx b/frontend/src/screens/app-settings/index.tsx deleted file mode 100644 index af0fb55..0000000 --- a/frontend/src/screens/app-settings/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Button } from '@nextui-org/react'; -import Layout from '../../components/layout'; -import { toggleTheme } from '../../actions/app'; -import useSelectedTheme from '../../hooks/use-selected-theme'; -import { IconMoon, IconSun } from '@tabler/icons-react'; - -const AppSettings = () => { - const theme = useSelectedTheme(); - - return ( - -
- -
-
- ); -}; - -export default AppSettings; diff --git a/frontend/src/screens/initializing/index.tsx b/frontend/src/screens/initializing/index.tsx deleted file mode 100644 index 7e1f421..0000000 --- a/frontend/src/screens/initializing/index.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Image, Spinner } from '@nextui-org/react'; -import { LoadingStatus } from '../../types'; -import palworldLogo from '../../assets/palworld-logo.webp'; -import useLoadingStatus from '../../hooks/use-loading-status'; - -const statusText = { - [LoadingStatus.IDLE]: 'Waiting...', - [LoadingStatus.INSTALLING_STEAMCMD]: 'Installing SteamCMD...', - [LoadingStatus.INSTALLING_SERVER]: 'Installing dedicated server...', - [LoadingStatus.DONE]: 'Done!' -}; - -const Initializing = () => { - const status = useLoadingStatus(); - - return ( -
-
- Palworld Logo -

Dedicated Server GUI

-

v{APP_VERSION}

- -
-

Initializing, please wait.

- -

{statusText[status] || `Unknown`}

- -
-

- Be patient, this may take a while depending on your internet speed - and computer specs. -

- -

- Do not close any terminal that pops up, otherwise the installation - will fail. -

-
-
-
-
- ); -}; - -export default Initializing; diff --git a/frontend/src/selectors/console.ts b/frontend/src/selectors/console.ts deleted file mode 100644 index a953da0..0000000 --- a/frontend/src/selectors/console.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { IRootState } from '../store'; -import { ConsoleId, TConsoleEntry } from '../types'; - -export const consolesByIdSelector = ( - state: IRootState, - consoleIds: ConsoleId[] -) => { - const result: TConsoleEntry[] = []; - - consoleIds.forEach((consoleId) => { - if (state.consoles[consoleId]) { - result.push(...state.consoles[consoleId]); - } - }); - - // sort by timestamp - result.sort((a, b) => a.timestamp - b.timestamp); - - return result; -}; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts deleted file mode 100644 index 644110b..0000000 --- a/frontend/src/types/index.ts +++ /dev/null @@ -1,62 +0,0 @@ -export type TTheme = 'light' | 'dark'; - -export type TGenericObject = { - [key: string]: any; -}; - -export type TGenericFunction = (...args: any[]) => any; - -export enum Modal { - ACTION_CONFIRMATION = 'ACTION_CONFIRMATION' -} - -export enum LoadingStatus { - IDLE = 'IDLE', - INSTALLING_STEAMCMD = 'INSTALLING_STEAMCMD', - INSTALLING_SERVER = 'INSTALLING_SERVER', - DONE = 'DONE' -} - -export enum ServerStatus { - STARTED = 'STARTED', - STOPPED = 'STOPPED', - STARTING = 'STARTING', - STOPPING = 'STOPPING', - RESTARTING = 'RESTARTING', - UPDATING = 'UPDATING', - ERROR = 'ERROR' -} - -export enum ConsoleId { - STEAM_CMD = 'STEAM_CMD', - SERVER = 'DEDICATED_SERVER' -} - -export enum AppEvent { - SET_LOADING_STATUS = 'SET_LOADING_STATUS', - SET_SERVER_STATUS = 'SET_SERVER_STATUS', - ADD_CONSOLE_ENTRY = 'ADD_CONSOLE_ENTRY', - RETURN_STEAM_IMAGE = 'RETURN_STEAM_IMAGE' -} - -export type TConsoleEntry = { - timestamp: number; - message: string; - msgType: 'stdout' | 'stderr'; -}; - -export type TBackupSettings = { - enabled: boolean; - intervalHours: number; - keepCount: number; -}; - -export type TSettings = { - theme: 'light' | 'dark'; - backup: TBackupSettings; - launchParams: string | undefined; -}; - -export type TSteamImageMap = { - [steamId64: string]: string; -}; diff --git a/frontend/src/wailsjs/go/backupsmanager/BackupManager.d.ts b/frontend/src/wailsjs/go/backupsmanager/BackupManager.d.ts deleted file mode 100644 index 5f81886..0000000 --- a/frontend/src/wailsjs/go/backupsmanager/BackupManager.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT -import {backupsmanager} from '../models'; -import {context} from '../models'; - -export function CheckCount():Promise; - -export function CreateBackup():Promise; - -export function Delete(arg1:string):Promise; - -export function Dispose():Promise; - -export function GetBackupsList():Promise>; - -export function Init(arg1:context.Context):Promise; - -export function Open(arg1:string):Promise; - -export function Restore(arg1:string):Promise; - -export function Start(arg1:number,arg2:number):Promise; - -export function Stop():Promise; diff --git a/frontend/src/wailsjs/go/backupsmanager/BackupManager.js b/frontend/src/wailsjs/go/backupsmanager/BackupManager.js deleted file mode 100644 index f6f2021..0000000 --- a/frontend/src/wailsjs/go/backupsmanager/BackupManager.js +++ /dev/null @@ -1,43 +0,0 @@ -// @ts-check -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -export function CheckCount() { - return window['go']['backupsmanager']['BackupManager']['CheckCount'](); -} - -export function CreateBackup() { - return window['go']['backupsmanager']['BackupManager']['CreateBackup'](); -} - -export function Delete(arg1) { - return window['go']['backupsmanager']['BackupManager']['Delete'](arg1); -} - -export function Dispose() { - return window['go']['backupsmanager']['BackupManager']['Dispose'](); -} - -export function GetBackupsList() { - return window['go']['backupsmanager']['BackupManager']['GetBackupsList'](); -} - -export function Init(arg1) { - return window['go']['backupsmanager']['BackupManager']['Init'](arg1); -} - -export function Open(arg1) { - return window['go']['backupsmanager']['BackupManager']['Open'](arg1); -} - -export function Restore(arg1) { - return window['go']['backupsmanager']['BackupManager']['Restore'](arg1); -} - -export function Start(arg1, arg2) { - return window['go']['backupsmanager']['BackupManager']['Start'](arg1, arg2); -} - -export function Stop() { - return window['go']['backupsmanager']['BackupManager']['Stop'](); -} diff --git a/frontend/src/wailsjs/go/dedicatedserver/DedicatedServer.d.ts b/frontend/src/wailsjs/go/dedicatedserver/DedicatedServer.d.ts deleted file mode 100644 index e08cd01..0000000 --- a/frontend/src/wailsjs/go/dedicatedserver/DedicatedServer.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT -import {context} from '../models'; - -export function Dispose():Promise; - -export function DownloadDedicatedServer():Promise; - -export function Init(arg1:context.Context):Promise; - -export function IsRunning():Promise; - -export function MonitorServerProcess():Promise; - -export function ReadConfig():Promise; - -export function ReadSaveName():Promise; - -export function Restart():Promise; - -export function SetLaunchParams(arg1:Array):Promise; - -export function Start():Promise; - -export function Stop():Promise; - -export function Update():Promise; - -export function WriteConfig(arg1:string):Promise; - -export function WriteSaveName(arg1:string):Promise; diff --git a/frontend/src/wailsjs/go/dedicatedserver/DedicatedServer.js b/frontend/src/wailsjs/go/dedicatedserver/DedicatedServer.js deleted file mode 100644 index e38df1c..0000000 --- a/frontend/src/wailsjs/go/dedicatedserver/DedicatedServer.js +++ /dev/null @@ -1,59 +0,0 @@ -// @ts-check -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -export function Dispose() { - return window['go']['dedicatedserver']['DedicatedServer']['Dispose'](); -} - -export function DownloadDedicatedServer() { - return window['go']['dedicatedserver']['DedicatedServer']['DownloadDedicatedServer'](); -} - -export function Init(arg1) { - return window['go']['dedicatedserver']['DedicatedServer']['Init'](arg1); -} - -export function IsRunning() { - return window['go']['dedicatedserver']['DedicatedServer']['IsRunning'](); -} - -export function MonitorServerProcess() { - return window['go']['dedicatedserver']['DedicatedServer']['MonitorServerProcess'](); -} - -export function ReadConfig() { - return window['go']['dedicatedserver']['DedicatedServer']['ReadConfig'](); -} - -export function ReadSaveName() { - return window['go']['dedicatedserver']['DedicatedServer']['ReadSaveName'](); -} - -export function Restart() { - return window['go']['dedicatedserver']['DedicatedServer']['Restart'](); -} - -export function SetLaunchParams(arg1) { - return window['go']['dedicatedserver']['DedicatedServer']['SetLaunchParams'](arg1); -} - -export function Start() { - return window['go']['dedicatedserver']['DedicatedServer']['Start'](); -} - -export function Stop() { - return window['go']['dedicatedserver']['DedicatedServer']['Stop'](); -} - -export function Update() { - return window['go']['dedicatedserver']['DedicatedServer']['Update'](); -} - -export function WriteConfig(arg1) { - return window['go']['dedicatedserver']['DedicatedServer']['WriteConfig'](arg1); -} - -export function WriteSaveName(arg1) { - return window['go']['dedicatedserver']['DedicatedServer']['WriteSaveName'](arg1); -} diff --git a/go.work b/go.work new file mode 100644 index 0000000..9b41866 --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.21.3 + +use ( + ./app + ./server +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..66b791e --- /dev/null +++ b/go.work.sum @@ -0,0 +1,55 @@ +atomicgo.dev/cursor v0.1.1/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.8/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/flytam/filenamify v1.0.0/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw= +github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo= +github.com/jaypipes/ghw v0.12.0/go.mod h1:jeJGbkRB2lL3/gxYzNYzEDETV1ZJ56OKr+CSeSEym+g= +github.com/jaypipes/pcidb v1.0.0/go.mod h1:TnYUvqhPBzCKnH34KrIX22kAeEbDCSRJ9cqLRCuNDfk= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/leaanthony/clir v1.3.0/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0= +github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU= +github.com/lithammer/fuzzysearch v1.1.5/go.mod h1:1R1LRNk7yKid1BaQkmuLQaHruxcC4HmAH30Dh61Ih1Q= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pterm/pterm v0.12.49/go.mod h1:D4OBoWNqAfXkm5QLTjIgjNiMXPHemLJHnIreGUsWzWg= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= +github.com/tidwall/sjson v1.1.7/go.mod h1:w/yG+ezBeTdUxiKs5NcPicO9diP38nk96QBAbIIGeFs= +github.com/wzshiming/ctc v1.2.3/go.mod h1:2tVAtIY7SUyraSk0JxvwmONNPFL4ARavPuEsg5+KA28= +github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae/go.mod h1:VTAq37rkGeV+WOybvZwjXiJOicICdpLCN8ifpISjK20= +github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= +golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..7cd1091 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,21 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work \ No newline at end of file diff --git a/server/api.go b/server/api.go new file mode 100644 index 0000000..f4fe21a --- /dev/null +++ b/server/api.go @@ -0,0 +1,74 @@ +package main + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "net/http" + "palworld-ds-gui-server/utils" +) + +type Api struct { +} + +func NewApi() *Api { + return &Api{} +} + +func PrintApiKey() { + fmt.Printf("\n\n🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨\n") + fmt.Printf("Anyone with your API key can control your server. Keep it secret!\n") + fmt.Printf("Your current API key is: %s\n", utils.Settings.General.APIKey) + fmt.Printf("To generate a new API key, run the program with the --newkey flag\n") + fmt.Printf("🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨\n\n") +} + +func (a *Api) Init() { + if !HasApiKey() || utils.Launch.ForceNewKey { + GenerateApiKey() + } + + if utils.Launch.ShowKey { + PrintApiKey() + } + + internalIp := utils.GetOutboundIP() + externalIp, err := utils.GetExternalIPv4() + if err != nil { + fmt.Println("Failed to get external IP:", err) + fmt.Printf("Server is running on %s:%d\n", internalIp, utils.Launch.Port) + } else { + fmt.Printf("Server is running on %s:%d (Local IP: %s:%d)\n", externalIp, utils.Launch.Port, internalIp, utils.Launch.Port) + } + + utils.EmitConsoleLog = LogToClient + + http.HandleFunc("/ws", handleWebSocket) + http.ListenAndServe(fmt.Sprintf(":%d", utils.Launch.Port), nil) +} + +func GenerateApiKey() { + randomBytes := make([]byte, 32) + rand.Read(randomBytes) + hasher := sha256.New() + hasher.Write(randomBytes) + hash := hasher.Sum(nil) + stringHash := hex.EncodeToString(hash) + + utils.Settings.General.APIKey = stringHash + err := utils.SaveSettings() + if err != nil { + panic(err) + } + + PrintApiKey() +} + +func HasApiKey() bool { + if utils.Settings.General.APIKey == "" || utils.Settings.General.APIKey == "CHANGE_ME" { + return false + } + + return true +} diff --git a/backups-manager/backups-manager.go b/server/backups-manager.go similarity index 50% rename from backups-manager/backups-manager.go rename to server/backups-manager.go index 44e9f57..05f3e16 100644 --- a/backups-manager/backups-manager.go +++ b/server/backups-manager.go @@ -1,11 +1,9 @@ -package backupsmanager +package main import ( - "context" "fmt" "os" - dedicatedserver "palword-ds-gui/dedicated-server" - "palword-ds-gui/utils" + "palworld-ds-gui-server/utils" "path" "regexp" "sort" @@ -18,10 +16,7 @@ import ( ) type BackupManager struct { - cron *cron.Cron - dedicatedServer *dedicatedserver.DedicatedServer - ctx context.Context - keepCount int + cron *cron.Cron } type Backup struct { @@ -31,33 +26,30 @@ type Backup struct { Size int64 } -func NewBackupManager(dedicatedServer *dedicatedserver.DedicatedServer) *BackupManager { - return &BackupManager{ - dedicatedServer: dedicatedServer, - keepCount: 5, - } +func NewBackupManager() *BackupManager { + return &BackupManager{} } -func (b *BackupManager) GetBackupsList() []Backup { +func (b *BackupManager) GetBackupsList() ([]Backup, error) { files, err := os.ReadDir(utils.Config.BackupsPath) if err != nil { - panic(err) + return nil, err } backups := []Backup{} + re, err := regexp.Compile(`^bckp-\d+-\w+\.zip$`) + if err != nil { + return nil, err + } + for _, file := range files { if file.IsDir() { continue } - matched, err := regexp.MatchString(`^bckp-\d+-\w+\.zip$`, file.Name()) - - if err != nil { - panic(err) - } - + matched := re.MatchString(file.Name()) if !matched { continue } @@ -65,12 +57,12 @@ func (b *BackupManager) GetBackupsList() []Backup { timestamp, err := strconv.ParseInt(strings.Split(file.Name(), "-")[1], 10, 64) if err != nil { - panic(err) + return nil, err } fileInfo, err := file.Info() if err != nil { - panic(err) + return nil, err } backups = append(backups, Backup{ @@ -85,42 +77,46 @@ func (b *BackupManager) GetBackupsList() []Backup { return backups[i].Timestamp < backups[j].Timestamp }) - return backups + return backups, nil } func (b *BackupManager) CheckCount() { - backups := b.GetBackupsList() + backups, err := b.GetBackupsList() + + if err != nil { + return + } - if len(backups) <= b.keepCount { + if len(backups) <= utils.Settings.Backup.KeepCount { return } - utils.PrintEx(b.ctx, fmt.Sprintf("Found %d backups, deleting %d", len(backups), len(backups)-b.keepCount), "DEDICATED_SERVER") + backupsToDelete := len(backups) - utils.Settings.Backup.KeepCount - for i := 0; i < len(backups)-b.keepCount; i++ { + utils.Log(fmt.Sprintf("Found %d backups, deleting %d", len(backups), backupsToDelete)) + + for i := 0; i < backupsToDelete; i++ { b.Delete(backups[i].Filename) } } -func (b *BackupManager) CreateBackup() { - saveName := b.dedicatedServer.ReadSaveName() +func (b *BackupManager) CreateBackup() error { + saveName := ReadSaveName() if len(saveName) == 0 { - utils.PrintEx(b.ctx, "Can't create backup, please join the server first", "DEDICATED_SERVER") - return + return fmt.Errorf("can't create a backup yet, please join the server first") } - utils.PrintEx(b.ctx, "Creating backup...", "DEDICATED_SERVER") + utils.Log("Creating backup...") unix := time.Now().Unix() backupName := fmt.Sprintf("bckp-%d-%s.zip", unix, saveName) worldPath := path.Join(utils.Config.ServerSaveDir, saveName) - archiver := archiver.NewZip() files, err := os.ReadDir(worldPath) if err != nil { - panic(err) + return err } var filePaths []string @@ -131,27 +127,52 @@ func (b *BackupManager) CreateBackup() { err = archiver.Archive(filePaths, path.Join(utils.Config.BackupsPath, backupName)) if err != nil { - panic(err) + return err } - utils.PrintEx(b.ctx, "Backup created", "DEDICATED_SERVER") + utils.Log("Backup created") b.CheckCount() + + b.EmitBackupList() + + return nil } -func (b *BackupManager) Init(ctx context.Context) { - b.ctx = ctx +func (b *BackupManager) EmitBackupList() { + backups, err := b.GetBackupsList() + if err != nil { + utils.Log(err.Error()) + return + } + + type BackupListResponse struct { + Event string `json:"event"` + Success bool `json:"success"` + Data []Backup `json:"data"` + } + + BroadcastJSON(BackupListResponse{ + Event: "BACKUP_LIST_CHANGED", + Success: true, + Data: backups, + }, nil) +} + +func (b *BackupManager) Init() { if _, err := os.Stat(utils.Config.BackupsPath); os.IsNotExist(err) { os.Mkdir(utils.Config.BackupsPath, 0755) } - - utils.PrintEx(b.ctx, "Backups manager is ready", "DEDICATED_SERVER") } func (b *BackupManager) Start(interval int, keepCount int) { - utils.PrintEx(b.ctx, fmt.Sprintf("Creating backups every %d hours (keep %d)", interval, keepCount), "DEDICATED_SERVER") + utils.Log(fmt.Sprintf("Creating backups every %d hours (keep %d)", interval, keepCount)) + + utils.Settings.Backup.Enabled = true + utils.Settings.Backup.KeepCount = keepCount + utils.Settings.Backup.Interval = float32(interval) + utils.SaveSettings() - b.keepCount = keepCount cronString := fmt.Sprintf("@every %dh", interval) if b.cron != nil { @@ -159,60 +180,75 @@ func (b *BackupManager) Start(interval int, keepCount int) { } b.cron = cron.New() - b.cron.AddFunc(cronString, b.CreateBackup) + b.cron.AddFunc(cronString, func() { + err := b.CreateBackup() + if err != nil { + utils.Log(fmt.Sprintf("Error creating backup: %s", err.Error())) + } + }) b.cron.Start() } -func (b *BackupManager) Delete(backupFileName string) { +func (b *BackupManager) Delete(backupFileName string) error { err := os.Remove(path.Join(utils.Config.BackupsPath, backupFileName)) if err != nil { - panic(err) + return err } - utils.PrintEx(b.ctx, fmt.Sprintf("Backup %s deleted", backupFileName), "DEDICATED_SERVER") + utils.Log(fmt.Sprintf("Backup %s deleted", backupFileName)) + + return nil } func (b *BackupManager) Stop() { if b.cron != nil { - utils.PrintEx(b.ctx, "Backups manager stopped", "DEDICATED_SERVER") + utils.Log("Backups manager stopped") b.cron.Stop() + + utils.Settings.Backup.Enabled = false + utils.SaveSettings() } } -func (b *BackupManager) Open(backupFileName string) { - utils.OpenExplorerWithFile(utils.Config.BackupsPath, backupFileName) +func (b *BackupManager) Open(backupFileName string) error { + return utils.OpenExplorerWithFile(utils.Config.BackupsPath, backupFileName) } -func (b *BackupManager) Restore(backupFileName string) { - b.dedicatedServer.Stop() +func (b *BackupManager) Restore(backupFileName string) error { + err := servermanager.Stop() + if err != nil { + return err + } - utils.PrintEx(b.ctx, fmt.Sprintf("Restoring backup %s", backupFileName), "DEDICATED_SERVER") + utils.Log(fmt.Sprintf("Restoring backup %s", backupFileName)) split := strings.Split(backupFileName, "-") saveName := strings.Replace(split[2], ".zip", "", 1) worldPath := path.Join(utils.Config.ServerSaveDir, saveName) - err := os.RemoveAll(worldPath) + err = os.RemoveAll(worldPath) if err != nil { - panic(err) + return err } err = os.Mkdir(worldPath, 0755) if err != nil { - panic(err) + return err } archiver := archiver.NewZip() err = archiver.Unarchive(path.Join(utils.Config.BackupsPath, backupFileName), worldPath) if err != nil { - panic(err) + return err } - utils.PrintEx(b.ctx, "Backup restored", "DEDICATED_SERVER") + utils.Log("Backup restored") + + return nil } func (b *BackupManager) Dispose() { b.Stop() - utils.LogToFile("backups-manager.go: Dispose()") + utils.LogToFile("backups-manager.go: Dispose()", false) } diff --git a/server/client-init-handler.go b/server/client-init-handler.go new file mode 100644 index 0000000..812ad97 --- /dev/null +++ b/server/client-init-handler.go @@ -0,0 +1,54 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type ClientInitRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Data struct { + NewSaveName string `json:"saveName"` + } +} + +type ClientInitRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` + Data string `json:"data"` +} + +var clientInitEvent = "INIT" + +func ClientInitHandler(conn *websocket.Conn, data []byte) { + var message ClientInitRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(ClientInitRes{ + Event: clientInitEvent, + EventId: message.EventId, + Success: false, + }) + return + } + + serverRunning := servermanager.IsRunning() + currentState := "STOPPED" + if serverRunning { + currentState = "STARTED" + } + + conn.WriteJSON(ClientInitRes{ + Event: clientInitEvent, + EventId: message.EventId, + Success: true, + Data: currentState, + }) +} diff --git a/server/config-manager.go b/server/config-manager.go new file mode 100644 index 0000000..535dfc4 --- /dev/null +++ b/server/config-manager.go @@ -0,0 +1,98 @@ +package main + +import ( + "fmt" + "os" + "palworld-ds-gui-server/utils" + "regexp" + "strings" +) + +func ReadSaveName() string { + // If file doesn't exist yet, return empty string (user hasn't joined the server for the first time yet) + if _, err := os.Stat(utils.Config.ServerGameUserSettingsPath); os.IsNotExist(err) { + return "" + } + + settingsData, err := os.ReadFile(utils.Config.ServerGameUserSettingsPath) + if err != nil { + panic(err) + } + + settingsString := strings.TrimSpace(string(settingsData)) + re := regexp.MustCompile(`DedicatedServerName=([^\s]+)`) + match := re.FindStringSubmatch(settingsString) + + if len(match) == 2 { + return match[1] + } + + return "" +} + +func WriteSaveName(newSaveName string) error { + settingsData, err := os.ReadFile(utils.Config.ServerGameUserSettingsPath) + if err != nil { + return err + } + + settingsString := strings.TrimSpace(string(settingsData)) + re := regexp.MustCompile(`DedicatedServerName=([^\s]+)`) + settingsString = re.ReplaceAllString(settingsString, fmt.Sprintf("DedicatedServerName=%s", newSaveName)) + + err = os.WriteFile(utils.Config.ServerGameUserSettingsPath, []byte(settingsString), os.ModePerm) + if err != nil { + return err + } + + return nil +} + +func ReadConfig() string { + configPath := utils.Config.ServerConfigPath + + // If config file doesn't exist yet, use default config + if _, err := os.Stat(configPath); os.IsNotExist(err) { + configPath = utils.Config.ServerDefaultConfigPath + } + + configData, err := os.ReadFile(configPath) + if err != nil { + panic(err) + } + + configString := strings.TrimSpace(string(configData)) + isEmpty := len(configString) == 0 + + // if the config file is empty, use default config + if isEmpty { + configData, err := os.ReadFile(utils.Config.ServerDefaultConfigPath) + + if err != nil { + panic(err) + } + + return strings.TrimSpace(string(configData)) + } + + return configString +} + +func WriteConfig(configString string) { + if _, err := os.Stat(utils.Config.ServerConfigDir); os.IsNotExist(err) { + os.MkdirAll(utils.Config.ServerConfigDir, 0755) + } + + if _, err := os.Stat(utils.Config.ServerConfigPath); os.IsNotExist(err) { + + _, err := os.Create(utils.Config.ServerConfigPath) + if err != nil { + panic(err) + } + } + + err := os.WriteFile(utils.Config.ServerConfigPath, []byte(configString), 0644) + if err != nil { + panic(err) + } +} diff --git a/server/create-backup-handler.go b/server/create-backup-handler.go new file mode 100644 index 0000000..252862b --- /dev/null +++ b/server/create-backup-handler.go @@ -0,0 +1,50 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type CreateBackupRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` +} + +type CreateBackupRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` +} + +var createBackupEvent = "CREATE_BACKUP" + +func CreateBackupHandler(conn *websocket.Conn, data []byte) { + var message StartBackupsRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(StartBackupsRes{ + Event: startBackupsEvent, + EventId: message.EventId, + Success: false, + }) + return + } + + success := true + err = backupmanager.CreateBackup() + if err != nil { + utils.Log(err.Error()) + success = false + } + + conn.WriteJSON(CreateBackupRes{ + Event: createBackupEvent, + EventId: message.EventId, + Success: success, + }) +} diff --git a/server/delete-backup-handler.go b/server/delete-backup-handler.go new file mode 100644 index 0000000..a4dccde --- /dev/null +++ b/server/delete-backup-handler.go @@ -0,0 +1,61 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type DeleteBackupRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Data struct { + Filename string `json:"backupFileName"` + } +} + +type DeleteBackupRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` + Data []Backup `json:"data"` +} + +var deleteBackupEvent = "DELETE_BACKUP" + +func DeleteBackupHandler(conn *websocket.Conn, data []byte) { + var message DeleteBackupRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(DeleteBackupRes{ + Event: deleteBackupEvent, + EventId: message.EventId, + Success: false, + Error: err.Error(), + }) + return + } + + err = backupmanager.Delete(message.Data.Filename) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(DeleteBackupRes{ + Event: deleteBackupEvent, + EventId: message.EventId, + Success: false, + Error: err.Error(), + }) + return + } + conn.WriteJSON(CreateBackupRes{ + Event: createBackupEvent, + EventId: message.EventId, + Success: true, + }) + + backupmanager.EmitBackupList() +} diff --git a/server/get-backups-config-handler.go b/server/get-backups-config-handler.go new file mode 100644 index 0000000..2fabc96 --- /dev/null +++ b/server/get-backups-config-handler.go @@ -0,0 +1,46 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type GetBackupsConfigRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` +} + +type GetBackupsConfigRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` + Data utils.PersistedSettingsBackup `json:"data"` +} + +var getBackupsConfigEvent = "GET_BACKUPS_SETTINGS" + +func GetBackupsConfigHandler(conn *websocket.Conn, data []byte) { + var message GetBackupsConfigRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(GetBackupsConfigRes{ + Event: getBackupsConfigEvent, + EventId: message.EventId, + Success: false, + Error: err.Error(), + }) + return + } + + conn.WriteJSON(GetBackupsConfigRes{ + Event: getBackupsConfigEvent, + EventId: message.EventId, + Success: true, + Data: utils.Settings.Backup, + }) +} diff --git a/server/get-backups-list-handler.go b/server/get-backups-list-handler.go new file mode 100644 index 0000000..67946a1 --- /dev/null +++ b/server/get-backups-list-handler.go @@ -0,0 +1,58 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type GetBackupsRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` +} + +type GetBackupsRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` + Data []Backup `json:"data"` +} + +var getBackupsEvent = "GET_BACKUPS_LIST" + +func GetBackupsHandler(conn *websocket.Conn, data []byte) { + var message GetBackupsRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(GetBackupsRes{ + Event: getBackupsEvent, + EventId: message.EventId, + Success: false, + Error: err.Error(), + }) + return + } + + list, err := backupmanager.GetBackupsList() + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(GetBackupsRes{ + Event: getBackupsEvent, + EventId: message.EventId, + Success: false, + Error: err.Error(), + }) + return + } + + conn.WriteJSON(GetBackupsRes{ + Event: getBackupsEvent, + EventId: message.EventId, + Success: true, + Data: list, + }) +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..1624b60 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,24 @@ +module palworld-ds-gui-server + +go 1.21.3 + +require ( + github.com/gorilla/websocket v1.5.1 + github.com/mholt/archiver/v3 v3.5.1 + github.com/mitchellh/go-ps v1.0.0 +) + +require ( + github.com/andybalholm/brotli v1.0.1 // indirect + github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect + github.com/golang/snappy v0.0.2 // indirect + github.com/klauspost/compress v1.11.4 // indirect + github.com/klauspost/pgzip v1.2.5 // indirect + github.com/nwaples/rardecode v1.1.0 // indirect + github.com/pierrec/lz4/v4 v4.1.2 // indirect + github.com/robfig/cron v1.2.0 // indirect + github.com/ulikunitz/xz v0.5.9 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + golang.org/x/net v0.20.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..d1aea07 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,36 @@ +github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc= +github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw= +github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= +github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= +github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= +github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM= +github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I= +github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..c4a2944 --- /dev/null +++ b/server/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "flag" + "os" + "palworld-ds-gui-server/utils" +) + +var ( + steamcmd *SteamCMD + servermanager *ServerManager + backupmanager *BackupManager + api *Api +) + +func main() { + println(utils.GetCurrentDir()) + utils.Init() + + if utils.Launch.Help { + flag.PrintDefaults() + os.Exit(0) + } + + steamcmd = NewSteamCMD() + steamcmd.Init() + + servermanager = NewServerManager() + servermanager.Init() + + backupmanager = NewBackupManager() + backupmanager.Init() + + api = NewApi() + api.Init() +} diff --git a/server/read-config-handler.go b/server/read-config-handler.go new file mode 100644 index 0000000..ef0095a --- /dev/null +++ b/server/read-config-handler.go @@ -0,0 +1,50 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type ReadConfigRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Data struct { + LaunchParams []string `json:"launchParams"` + } +} + +type ReadConfigRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` + Data string `json:"data"` +} + +var readConfigEvent = "READ_CONFIG" + +func ReadConfigHandler(conn *websocket.Conn, data []byte) { + var message ReadConfigRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(ReadConfigRes{ + Event: readConfigEvent, + EventId: message.EventId, + Success: false, + }) + return + } + + config := ReadConfig() + + conn.WriteJSON(ReadConfigRes{ + Event: readConfigEvent, + EventId: message.EventId, + Success: true, + Data: config, + }) +} diff --git a/server/read-save-handler.go b/server/read-save-handler.go new file mode 100644 index 0000000..fb94ba5 --- /dev/null +++ b/server/read-save-handler.go @@ -0,0 +1,50 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type ReadSaveRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Data struct { + LaunchParams []string `json:"launchParams"` + } +} + +type ReadSaveRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` + Data string `json:"data"` +} + +var readSaveEvent = "READ_SAVE_NAME" + +func ReadSaveHandler(conn *websocket.Conn, data []byte) { + var message ReadSaveRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(ReadSaveRes{ + Event: readSaveEvent, + EventId: message.EventId, + Success: false, + }) + return + } + + config := ReadSaveName() + + conn.WriteJSON(ReadSaveRes{ + Event: readSaveEvent, + EventId: message.EventId, + Success: true, + Data: config, + }) +} diff --git a/server/restart-server-handler.go b/server/restart-server-handler.go new file mode 100644 index 0000000..eb3177c --- /dev/null +++ b/server/restart-server-handler.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type RestartServerRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Data struct { + LaunchParams []string `json:"launchParams"` + } +} + +type RestartServerRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` +} + +var restartServerEvent = "RESTART_SERVER" + +func RestartServerHandler(conn *websocket.Conn, data []byte) { + EmitServerStatus("RESTARTING", nil) + + var message RestartServerRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(RestartServerRes{ + Event: restartServerEvent, + EventId: message.EventId, + Success: false, + }) + return + } + + err = servermanager.Restart() + if err != nil { + utils.Log("Error restarting the server: " + err.Error()) + EmitServerStatus("STOPPED", nil) + return + } + + EmitServerStatus("STARTED", nil) +} diff --git a/server/restore-backup-handler.go b/server/restore-backup-handler.go new file mode 100644 index 0000000..23e065d --- /dev/null +++ b/server/restore-backup-handler.go @@ -0,0 +1,62 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type RestoreBackupRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Data struct { + Filename string `json:"backupFileName"` + } +} + +type RestoreBackupRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` + Data []Backup `json:"data"` +} + +var restoreBackupEvent = "RESTORE_BACKUP" + +func RestoreBackupHandler(conn *websocket.Conn, data []byte) { + var message RestoreBackupRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(RestoreBackupRes{ + Event: restoreBackupEvent, + EventId: message.EventId, + Success: false, + Error: err.Error(), + }) + return + } + + err = backupmanager.Restore(message.Data.Filename) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(RestoreBackupRes{ + Event: restoreBackupEvent, + EventId: message.EventId, + Success: false, + Error: err.Error(), + }) + return + } + + EmitServerStatus("STOPPED", nil) + + conn.WriteJSON(CreateBackupRes{ + Event: createBackupEvent, + EventId: message.EventId, + Success: true, + }) +} diff --git a/server/server-manager.go b/server/server-manager.go new file mode 100644 index 0000000..eeb2aab --- /dev/null +++ b/server/server-manager.go @@ -0,0 +1,214 @@ +package main + +import ( + "errors" + "os" + "os/exec" + "palworld-ds-gui-server/utils" + "time" + + "github.com/mitchellh/go-ps" +) + +type ServerManager struct { + cmd *exec.Cmd + serverCmd *exec.Cmd + serverPid int + launchParams []string +} + +func NewServerManager() *ServerManager { + return &ServerManager{} +} + +func (s *ServerManager) Init() { + proc, _ := utils.FindProcessByName(utils.Config.ServerProcessName) + + if proc != nil { + utils.Log("A server is already running, killing it...") + + err := utils.KillProcessByPid(proc.Pid()) + + if err != nil { + utils.Log("Error stopping server: " + err.Error()) + return + } + + utils.Log("Server killed successfully") + } + + if _, err := os.Stat(utils.Config.ServerPath); os.IsNotExist(err) { + utils.Log("Server directory not found, creating...") + utils.Log("If you already have a server, please place it in " + utils.Config.ServerPath) + os.Mkdir(utils.Config.ServerPath, 0755) + s.DownloadDedicatedServer() + } +} + +func (s *ServerManager) DownloadDedicatedServer() error { + utils.Log("Downloading dedicated server...") + + if s.IsRunning() { + utils.Log("Cannot update server while it's running. Stopping it...") + s.Stop() + } + + cmd := exec.Command(utils.Config.SteamCmdExe, + "+force_install_dir", utils.Config.ServerPath, + "+login", "anonymous", + "+app_update", utils.Config.AppId, "validate", + "+quit") + + cmd.Dir = utils.Config.SteamCmdPath + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + s.cmd = cmd + + err := cmd.Run() + if err != nil { + return err + } + + utils.Log("Server downloaded and updated successfully!") + + return nil +} + +func (s *ServerManager) IsRunning() bool { + if s.serverPid == 0 { + return false + } + + proc, _ := utils.FindProcessByPid(s.serverPid) + + return proc != nil +} + +func (s *ServerManager) MonitorServerProcess() { + for { + time.Sleep(4 * time.Second) + + // was killed via gui + if s.serverPid == 0 { + break + } + + proc, err := utils.FindProcessByPid(s.serverPid) + + if proc == nil || err != nil { + utils.Log("Server seems to have stopped (crashed?)") + EmitServerStatus("STOPPED", nil) + break + } + } +} + +func (s *ServerManager) Start() error { + utils.Log("Starting dedicated server...") + + s.serverCmd = exec.Command(utils.Config.ServerExe, s.launchParams...) + s.serverCmd.Dir = utils.Config.ServerPath + s.serverCmd.Stdout = os.Stdout + s.serverCmd.Stderr = os.Stderr + + err := s.serverCmd.Start() + if err != nil { + return err + } + + var attempts int = 10 + var proc ps.Process + + whileLoop := true + for whileLoop { + time.Sleep(1 * time.Second) + + proc, err = utils.FindProcessByName(utils.Config.ServerProcessName) + if err != nil { + continue + } + + if proc != nil { + whileLoop = false + } + + attempts-- + + if attempts <= 0 { + whileLoop = false + } + } + + if proc == nil { + return errors.New("server process not found") + } + + s.serverPid = proc.Pid() + utils.Log("Server started") + + go s.MonitorServerProcess() + + return nil +} + +func (s *ServerManager) Stop() error { + if !s.IsRunning() { + return nil + } + + utils.Log("Stopping dedicated server...") + + err := utils.KillProcessByPid(s.serverPid) + if err != nil { + return err + } + + if s.serverCmd != nil && s.serverCmd.Process != nil { + err := s.serverCmd.Process.Kill() + if err != nil { + return err + } + } + + utils.Log("Server stopped") + s.serverPid = 0 + + return nil +} + +func (s *ServerManager) Restart() error { + utils.Log("Restarting dedicated server...") + + err := s.Stop() + if err != nil { + return err + } + + err = s.Start() + if err != nil { + return err + } + + return nil +} + +func (s *ServerManager) Update() error { + err := s.DownloadDedicatedServer() + if err != nil { + return err + } + + return nil +} + +func (s *ServerManager) Dispose() { + if s.cmd != nil && s.cmd.Process != nil { + err := s.cmd.Process.Kill() + if err != nil { + utils.Log(err.Error()) + } + } + + s.Stop() + utils.Log("dedicated-server.go: Dispose()") +} diff --git a/server/start-backups-handler.go b/server/start-backups-handler.go new file mode 100644 index 0000000..f10763b --- /dev/null +++ b/server/start-backups-handler.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type StartBackupsRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Data struct { + Interval int `json:"interval"` + KeepCount int `json:"keepCount"` + } +} + +type StartBackupsRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` +} + +var startBackupsEvent = "START_BACKUPS" + +func StartBackupsHandler(conn *websocket.Conn, data []byte) { + var message StartBackupsRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(StartBackupsRes{ + Event: startBackupsEvent, + EventId: message.EventId, + Success: false, + }) + return + } + + backupmanager.Start(message.Data.Interval, message.Data.KeepCount) + + conn.WriteJSON(StartBackupsRes{ + Event: startBackupsEvent, + EventId: message.EventId, + Success: true, + }) + + EmitBackupSettings(nil) +} diff --git a/server/start-server-handler.go b/server/start-server-handler.go new file mode 100644 index 0000000..ebeb9d6 --- /dev/null +++ b/server/start-server-handler.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type StartServerRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Data struct { + LaunchParams []string `json:"launchParams"` + } +} + +type StartServerRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` +} + +var startServerEvent = "START_SERVER" + +func StartServerHandler(conn *websocket.Conn, data []byte) { + EmitServerStatus("STARTING", nil) + + var message StartServerRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(StartServerRes{ + Event: startServerEvent, + EventId: message.EventId, + Success: false, + }) + return + } + + err = servermanager.Start() + + if err != nil { + utils.Log(err.Error()) + EmitServerStatus("ERROR", nil) + } else { + EmitServerStatus("STARTED", nil) + } +} diff --git a/steamcmd/steamcmd.go b/server/steamcmd.go similarity index 50% rename from steamcmd/steamcmd.go rename to server/steamcmd.go index ff3d944..07c4905 100644 --- a/steamcmd/steamcmd.go +++ b/server/steamcmd.go @@ -1,44 +1,26 @@ -package steamcmd +package main import ( - "context" "os" - "palword-ds-gui/utils" + "palworld-ds-gui-server/utils" "path" "github.com/mholt/archiver/v3" - "github.com/wailsapp/wails/v2/pkg/runtime" ) type SteamCMD struct { } -var ctx context.Context -var currentConsoleId string = "STEAM_CMD" - func NewSteamCMD() *SteamCMD { return &SteamCMD{} } -func Print(message string) { - utils.PrintEx(ctx, message, currentConsoleId) -} - -func (s *SteamCMD) Init(srcCtx context.Context) { - ctx = srcCtx - - Print("Initializing SteamCMD...") - +func (s *SteamCMD) Init() { if _, err := os.Stat(utils.Config.SteamCmdPath); os.IsNotExist(err) { - Print("SteamCMD not found, downloading...") - runtime.EventsEmit(ctx, "SET_LOADING_STATUS", "INSTALLING_STEAMCMD") + utils.Log("SteamCMD not found, downloading...") os.Mkdir(utils.Config.SteamCmdPath, 0755) s.DownloadExecutable() - } else { - Print("SteamCMD is already installed") } - - Print("Done initializing SteamCMD") } func (s *SteamCMD) DownloadExecutable() { @@ -49,19 +31,19 @@ func (s *SteamCMD) DownloadExecutable() { panic(err) } - Print("Extracting SteamCMD...") + utils.Log("Extracting SteamCMD...") err = archiver.Unarchive(zipPath, utils.Config.SteamCmdPath) if err != nil { panic(err) } - Print("Removing zip file...") + utils.Log("Removing zip file...") err = os.Remove(zipPath) if err != nil { panic(err) } - Print("SteamCMD is ready") + utils.Log("SteamCMD downloaded and extracted successfully!") } diff --git a/server/stop-backups-handler.go b/server/stop-backups-handler.go new file mode 100644 index 0000000..292238a --- /dev/null +++ b/server/stop-backups-handler.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type StopBackupsRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Data struct { + Interval int `json:"interval"` + KeepCount int `json:"keepCount"` + } +} + +type StopBackupsRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` +} + +var stopBackupsEvent = "STOP_BACKUPS" + +func StopBackupsHandler(conn *websocket.Conn, data []byte) { + var message StopBackupsRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(StopBackupsRes{ + Event: stopBackupsEvent, + EventId: message.EventId, + Success: false, + }) + return + } + + backupmanager.Stop() + + conn.WriteJSON(StopBackupsRes{ + Event: stopBackupsEvent, + EventId: message.EventId, + Success: true, + }) + + EmitBackupSettings(nil) +} diff --git a/server/stop-server-handler.go b/server/stop-server-handler.go new file mode 100644 index 0000000..bd168e2 --- /dev/null +++ b/server/stop-server-handler.go @@ -0,0 +1,43 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type StopServerRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Data struct { + LaunchParams []string `json:"launchParams"` + } +} + +var stopServerEvent = "STOP_SERVER" + +func StopServerHandler(conn *websocket.Conn, data []byte) { + EmitServerStatus("STOPPING", nil) + + var message StopServerRequest + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(BaseResponse{ + Event: stopServerEvent, + EventId: message.EventId, + Success: false, + }) + return + } + + err = servermanager.Stop() + + if err != nil { + utils.Log(err.Error()) + EmitServerStatus("ERROR", nil) + } else { + EmitServerStatus("STOPPED", nil) + } +} diff --git a/server/update-server-handler.go b/server/update-server-handler.go new file mode 100644 index 0000000..66dbb86 --- /dev/null +++ b/server/update-server-handler.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type UpdateServerRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Data struct { + LaunchParams []string `json:"launchParams"` + } +} + +type UpdateServerRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` +} + +var updateServerEvent = "UPDATE_SERVER" + +func UpdateServerHandler(conn *websocket.Conn, data []byte) { + EmitServerStatus("UPDATING", nil) + + var message UpdateServerRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(UpdateServerRes{ + Event: updateServerEvent, + EventId: message.EventId, + Success: false, + }) + return + } + + err = servermanager.Update() + if err != nil { + utils.Log("Error updating the server: " + err.Error()) + EmitServerStatus("STOPPED", nil) + return + } + + EmitServerStatus("STOPPED", nil) +} diff --git a/utils/utils.go b/server/utils/utils.go similarity index 52% rename from utils/utils.go rename to server/utils/utils.go index e84dd30..d55ae63 100644 --- a/utils/utils.go +++ b/server/utils/utils.go @@ -1,9 +1,10 @@ package utils import ( - "context" + "flag" "fmt" "io" + "net" "net/http" "os" "os/exec" @@ -11,16 +12,11 @@ import ( "strings" "time" + "github.com/gorilla/websocket" "github.com/mitchellh/go-ps" - "github.com/wailsapp/wails/v2/pkg/runtime" + "gopkg.in/ini.v1" ) -type ConsoleEntry struct { - Message string - Timestamp int64 - MsgType string -} - type AppConfig struct { SteamCmdPath string SteamCmdUrl string @@ -35,6 +31,7 @@ type AppConfig struct { ServerProcessName string BackupsPath string LogsPath string + PersistedSettingsPath string AppId string } @@ -51,40 +48,135 @@ var Config AppConfig = AppConfig{ LogsPath: filepath.Join(GetCurrentDir(), "logs.txt"), ServerProcessName: "PalServer-Win64-Test-Cmd.exe", BackupsPath: filepath.Join(GetCurrentDir(), "backups"), + PersistedSettingsPath: filepath.Join(GetCurrentDir(), "gui-server-settings.ini"), SteamCmdUrl: "https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip", AppId: "2394010", } -func GetCurrentDir() string { - ex, err := os.Executable() +type PersistedSettingsBackup struct { + Enabled bool `ini:"enabled"` + Interval float32 `ini:"interval"` + KeepCount int `ini:"keepCount"` +} +type PersistedSettings struct { + General struct { + APIKey string `ini:"apiKey"` + } + Backup PersistedSettingsBackup +} + +var Settings PersistedSettings = PersistedSettings{ + General: struct { + APIKey string `ini:"apiKey"` + }{ + APIKey: "CHANGE_ME", + }, + Backup: struct { + Enabled bool `ini:"enabled"` + Interval float32 `ini:"interval"` + KeepCount int `ini:"keepCount"` + }{ + Enabled: false, + Interval: 1, + KeepCount: 24, + }, +} + +type LaunchParams struct { + ForceNewKey bool + ShowKey bool + Help bool + Port int +} + +var Launch LaunchParams = LaunchParams{ + ForceNewKey: false, + ShowKey: false, + Help: false, + Port: 21577, +} + +var EmitConsoleLog func(message string, excludeClient *websocket.Conn) + +func Init() { + logsFile, logErr := os.OpenFile(Config.LogsPath, os.O_RDWR|os.O_CREATE, 0666) + if logErr != nil { + panic(logErr) + } + defer logsFile.Close() + + settingsFile, settingsErr := os.OpenFile(Config.PersistedSettingsPath, os.O_RDWR|os.O_CREATE, 0666) + if settingsErr != nil { + panic(settingsErr) + } + defer settingsFile.Close() + + err := LoadSettings() if err != nil { panic(err) } - dir := filepath.Dir(ex) + SaveSettings() - return dir + flag.BoolVar(&Launch.ForceNewKey, "newkey", false, "Generate a new API key") + flag.BoolVar(&Launch.ShowKey, "showkey", false, "Show the current API key") + flag.BoolVar(&Launch.Help, "help", false, "Show help") + flag.IntVar(&Launch.Port, "port", 21577, "Port to run the server on") + + flag.Parse() } -func DownloadFile(url string, path string) error { - resp, err := http.Get(url) +func LoadSettings() error { + cfg, err := ini.Load(Config.PersistedSettingsPath) if err != nil { - return err + return fmt.Errorf("failed to read file: %v", err) } - defer resp.Body.Close() - out, err := os.Create(path) + if err = cfg.Section("General").MapTo(&Settings.General); err != nil { + return fmt.Errorf("failed to map General section: %v", err) + } + if err = cfg.Section("Backup").MapTo(&Settings.Backup); err != nil { + return fmt.Errorf("failed to map Backup section: %v", err) + } + + return nil +} + +func SaveSettings() error { + cfg := ini.Empty() + + if err := cfg.Section("General").ReflectFrom(&Settings.General); err != nil { + return fmt.Errorf("failed to reflect General section: %v", err) + } + if err := cfg.Section("Backup").ReflectFrom(&Settings.Backup); err != nil { + return fmt.Errorf("failed to reflect Backup section: %v", err) + } + + if err := cfg.SaveTo(Config.PersistedSettingsPath); err != nil { + return fmt.Errorf("failed to save file: %v", err) + } + + return nil +} + +func GetCurrentDir() string { + ex, err := os.Executable() + if err != nil { - return err + panic(err) } - defer out.Close() - _, err = io.Copy(out, resp.Body) - return err + dir := filepath.Dir(ex) + + return dir } -func LogToFile(message string) { +func LogToFile(message string, print bool) { + if print { + println(message) + } + logsFile, err := os.OpenFile(Config.LogsPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { panic(err) @@ -97,15 +189,29 @@ func LogToFile(message string) { logsFile.WriteString(formatedMessage + "\n") } -func PrintEx(ctx context.Context, message string, consoleId string) { - consoleEntry := ConsoleEntry{ - Message: message, - Timestamp: time.Now().Unix(), - MsgType: "stdout", +func Log(message string) { + LogToFile(message, true) + + if EmitConsoleLog != nil { + EmitConsoleLog(message, nil) } +} - runtime.EventsEmit(ctx, "ADD_CONSOLE_ENTRY", consoleId, consoleEntry) - LogToFile(message) +func DownloadFile(url string, path string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + out, err := os.Create(path) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err } func FindProcessByName(processName string) (ps.Process, error) { @@ -123,6 +229,20 @@ func FindProcessByName(processName string) (ps.Process, error) { return nil, fmt.Errorf("process with name %s not found", processName) } +func KillProcessByPid(pid int) error { + process, err := os.FindProcess(pid) + if err != nil { + return err + } + + err = process.Kill() + if err != nil { + return err + } + + return nil +} + func FindProcessByPid(pid int) (ps.Process, error) { processes, err := ps.Processes() if err != nil { @@ -138,18 +258,31 @@ func FindProcessByPid(pid int) (ps.Process, error) { return nil, fmt.Errorf("process with pid %d not found", pid) } -func KillProcessByPid(pid int) error { - process, err := os.FindProcess(pid) +func GetOutboundIP() net.IP { + conn, err := net.Dial("udp", "8.8.8.8:80") if err != nil { - return err + return nil } + defer conn.Close() - err = process.Kill() + localAddr := conn.LocalAddr().(*net.UDPAddr) + + return localAddr.IP +} + +func GetExternalIPv4() (string, error) { + resp, err := http.Get("https://text.ipv4.wtfismyip.com/") if err != nil { - return err + return "", err } + defer resp.Body.Close() - return nil + ipBytes, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return strings.TrimSpace(string(ipBytes)), nil } func OpenExplorerWithFile(folderPath, fileName string) error { diff --git a/server/websocket.go b/server/websocket.go new file mode 100644 index 0000000..a82f1bf --- /dev/null +++ b/server/websocket.go @@ -0,0 +1,186 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "palworld-ds-gui-server/utils" + "strings" + "sync" + + "github.com/gorilla/websocket" +) + +var ( + upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { return true }, + } + clients = make(map[*websocket.Conn]bool) + mutex = &sync.Mutex{} +) + +type BaseRequest struct { + Event string `json:"event"` +} + +type BaseResponse struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` + Data string `json:"data"` +} + +func handleWebSocket(w http.ResponseWriter, r *http.Request) { + authToken := r.URL.Query().Get("auth") + + if authToken != utils.Settings.General.APIKey { + utils.Log(fmt.Sprintf("Unauthorized connection attempt from %s", r.RemoteAddr)) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + utils.LogToFile(err.Error(), true) + return + } + + mutex.Lock() + clients[conn] = true + mutex.Unlock() + + defer func() { + mutex.Lock() + delete(clients, conn) + mutex.Unlock() + conn.Close() + utils.Log(fmt.Sprintf("%s disconnected from the GUI server", conn.RemoteAddr().String())) + }() + + utils.Log(fmt.Sprintf("%s connected to the GUI server", conn.RemoteAddr().String())) + + for { + messageType, p, err := conn.ReadMessage() + if err != nil { + utils.LogToFile(err.Error(), true) + break + } + + if messageType != websocket.TextMessage { + utils.LogToFile("Received non-text message", true) + continue + } + + var message BaseRequest + err = json.Unmarshal(p, &message) + if err != nil { + utils.LogToFile(err.Error(), true) + continue + } + + switch message.Event { + case startServerEvent: + StartServerHandler(conn, p) + case stopServerEvent: + StopServerHandler(conn, p) + case restartServerEvent: + RestartServerHandler(conn, p) + case readConfigEvent: + ReadConfigHandler(conn, p) + case readSaveEvent: + ReadSaveHandler(conn, p) + case writeSaveEvent: + WriteSaveHandler(conn, p) + case writeConfigEvent: + WriteConfigHandler(conn, p) + case clientInitEvent: + ClientInitHandler(conn, p) + case updateServerEvent: + UpdateServerHandler(conn, p) + case startBackupsEvent: + StartBackupsHandler(conn, p) + case stopBackupsEvent: + StopBackupsHandler(conn, p) + case createBackupEvent: + CreateBackupHandler(conn, p) + case getBackupsEvent: + GetBackupsHandler(conn, p) + case deleteBackupEvent: + DeleteBackupHandler(conn, p) + case restoreBackupEvent: + RestoreBackupHandler(conn, p) + case getBackupsConfigEvent: + GetBackupsConfigHandler(conn, p) + default: + utils.LogToFile(fmt.Sprintf("Unknown event: %s", message.Event), true) + } + } +} + +func BroadcastJSON(v interface{}, exclude *websocket.Conn) { + mutex.Lock() + defer mutex.Unlock() + for client := range clients { + if client == exclude { + continue + } + + err := client.WriteJSON(v) + if err != nil { + utils.Log(err.Error()) + client.Close() + delete(clients, client) + } + } +} + +func EmitServerStatus(status string, exclude *websocket.Conn) { + BroadcastJSON(BaseResponse{ + Event: "SERVER_STATUS_CHANGED", + Data: status, + Success: true, + }, exclude) +} + +func EmitServerConfig(config string, exclude *websocket.Conn) { + BroadcastJSON(BaseResponse{ + Event: "SERVER_CONFIG_CHANGED", + Success: true, + Data: config, + }, exclude) +} + +func EmitBackupSettings(exclude *websocket.Conn) { + type BackupSettingsResponse struct { + Event string `json:"event"` + Success bool `json:"success"` + Data utils.PersistedSettingsBackup `json:"data"` + } + + println("EmitBackupSettings") + + BroadcastJSON(BackupSettingsResponse{ + Event: "BACKUP_SETTINGS_CHANGED", + Success: true, + Data: utils.Settings.Backup, + }, exclude) +} + +func EmitSaveName(name string, exclude *websocket.Conn) { + BroadcastJSON(BaseResponse{ + Event: "SERVER_SAVE_NAME_CHANGED", + Data: name, + Success: true, + }, exclude) +} + +func LogToClient(message string, conn *websocket.Conn) { + BroadcastJSON(BaseResponse{ + Event: "ADD_CONSOLE_ENTRY", + Data: strings.TrimSpace(message), + Success: true, + }, conn) +} diff --git a/server/write-config-handler.go b/server/write-config-handler.go new file mode 100644 index 0000000..13a3d66 --- /dev/null +++ b/server/write-config-handler.go @@ -0,0 +1,53 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type WriteConfigRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Data struct { + NewConfig string `json:"config"` + } +} + +type WriteConfigRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` + Data string `json:"data"` +} + +var writeConfigEvent = "WRITE_CONFIG" + +func WriteConfigHandler(conn *websocket.Conn, data []byte) { + var message WriteConfigRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(WriteConfigRes{ + Event: writeConfigEvent, + EventId: message.EventId, + Success: false, + }) + return + } + + WriteConfig(message.Data.NewConfig) + + // this response is just to flag the client that the write was successful + // the emit below will send the new config to all clients + conn.WriteJSON(WriteConfigRes{ + Event: writeConfigEvent, + EventId: message.EventId, + Success: true, + }) + + EmitServerConfig(message.Data.NewConfig, nil) +} diff --git a/server/write-save-handler.go b/server/write-save-handler.go new file mode 100644 index 0000000..3fe1ad9 --- /dev/null +++ b/server/write-save-handler.go @@ -0,0 +1,62 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type WriteSaveRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Data struct { + NewSaveName string `json:"saveName"` + } +} + +type WriteSaveRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` + Data string `json:"data"` +} + +var writeSaveEvent = "WRITE_SAVE_NAME" + +func WriteSaveHandler(conn *websocket.Conn, data []byte) { + var message WriteSaveRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(WriteSaveRes{ + Event: writeSaveEvent, + EventId: message.EventId, + Success: false, + }) + return + } + + err = WriteSaveName(message.Data.NewSaveName) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(WriteSaveRes{ + Event: writeSaveEvent, + EventId: message.EventId, + Success: false, + }) + return + } + + // this response is just to flag the client that the write was successful + // the emit below will send the new save name to all clients + conn.WriteJSON(WriteSaveRes{ + Event: writeSaveEvent, + EventId: message.EventId, + Success: true, + }) + + EmitSaveName(message.Data.NewSaveName, nil) +} From 7ed3451455cc5ae9b624bbbb7f3483fed8b74d92 Mon Sep 17 00:00:00 2001 From: Diogo Martino Date: Wed, 14 Feb 2024 00:27:11 +0000 Subject: [PATCH 02/15] chore: server side launch params --- app/frontend/src/actions/app.ts | 3 +- app/frontend/src/actions/socket.ts | 26 +++++--- .../src/components/socket-provider/index.tsx | 4 ++ app/frontend/src/screens/home/index.tsx | 35 ++++++++-- app/frontend/src/selectors/app.ts | 2 +- app/frontend/src/server.ts | 55 ++++++++++------ app/frontend/src/store/app-slice.ts | 9 ++- app/frontend/src/types/index.ts | 10 ++- server/api.go | 2 +- server/client-init-handler.go | 19 ++++-- server/main.go | 1 - server/save-launch-params-handler.go | 64 +++++++++++++++++++ server/start-server-handler.go | 3 - server/utils/utils.go | 24 ++++--- server/websocket.go | 12 +++- 15 files changed, 197 insertions(+), 72 deletions(-) create mode 100644 server/save-launch-params-handler.go diff --git a/app/frontend/src/actions/app.ts b/app/frontend/src/actions/app.ts index f0b3ecd..fcbf5d3 100644 --- a/app/frontend/src/actions/app.ts +++ b/app/frontend/src/actions/app.ts @@ -37,7 +37,8 @@ export const saveSettings = (settings?: TSettings) => { localStorage.setItem('settings', JSON.stringify(targetSettings)); }; -export const initApp = async () => { +export const fetchServerInfo = async () => { + // TODO:: all this data should be returned when SocketAction.INIT is calle await ServerAPI.fetchConfig(); await ServerAPI.fetchSaveName(); await ServerAPI.backups.fetchCurrentSettings(); diff --git a/app/frontend/src/actions/socket.ts b/app/frontend/src/actions/socket.ts index f2b34d3..a78793d 100644 --- a/app/frontend/src/actions/socket.ts +++ b/app/frontend/src/actions/socket.ts @@ -5,12 +5,13 @@ import { ServerStatus, TBackup, TBackupSettings, + TClientInitedData, TConsoleEntry } from '../types'; -import { consolesSliceActions } from '../store/console-slice'; -import { initApp } from './app'; -import { setStatus } from './server'; +import { fetchServerInfo, setLaunchParams } from './app'; +import { setBackupsList, setConfig, setSaveName, setStatus } from './server'; import { parseConfig } from '../helpers/config-parser'; +import { addConsoleEntry } from './console'; export const setSocket = (socket: WebSocket) => { store.dispatch(socketSliceActions.setSocket(socket)); @@ -51,14 +52,15 @@ export const onAddConsoleEntry = (message: string) => { timestamp: Date.now() }; - store.dispatch(consolesSliceActions.addConsoleEntry(entry)); + addConsoleEntry(entry); }; -export const onClientInited = (data) => { - setStatus(data); +export const onClientInited = (data: TClientInitedData) => { + setStatus(data.currentServerStatus); + setLaunchParams(data.currentLaunchParams); setSocketInited(true); setSocketConnecting(false); - initApp(); + fetchServerInfo(); }; export const onBackupListUpdated = (data) => { @@ -69,7 +71,7 @@ export const onBackupListUpdated = (data) => { timestamp: backup.Timestamp })); - store.dispatch(serverSliceActions.setBackupsList(backups)); + setBackupsList(backups); }; export const onBackupSettingsUpdated = (data) => { @@ -82,12 +84,16 @@ export const onBackupSettingsUpdated = (data) => { store.dispatch(serverSliceActions.setBackupSettings(backupsSettings)); }; +export const onLaunchParamsChanged = (launchParams: string) => { + setLaunchParams(launchParams); +}; + export const onServerConfigChanged = (configStr: string) => { const config = parseConfig(configStr); - store.dispatch(serverSliceActions.setConfig(config)); + setConfig(config); }; export const onServerSaveNameChanged = (saveName: string) => { - store.dispatch(serverSliceActions.setSaveName(saveName)); + setSaveName(saveName); }; diff --git a/app/frontend/src/components/socket-provider/index.tsx b/app/frontend/src/components/socket-provider/index.tsx index 370bc0b..94a9b56 100644 --- a/app/frontend/src/components/socket-provider/index.tsx +++ b/app/frontend/src/components/socket-provider/index.tsx @@ -4,6 +4,7 @@ import { onAddConsoleEntry, onBackupListUpdated, onBackupSettingsUpdated, + onLaunchParamsChanged, onServerConfigChanged, onServerSaveNameChanged, onServerStatusChanged, @@ -91,6 +92,9 @@ const SocketProvider = ({ children }: TSocketProviderProps) => { case SocketEvent.BACKUP_SETTINGS_CHANGED: onBackupSettingsUpdated(message.data); break; + case SocketEvent.LAUNCH_PARAMS_CHANGED: + onLaunchParamsChanged(message.data); + break; default: break; } diff --git a/app/frontend/src/screens/home/index.tsx b/app/frontend/src/screens/home/index.tsx index 9de563c..aeeee81 100644 --- a/app/frontend/src/screens/home/index.tsx +++ b/app/frontend/src/screens/home/index.tsx @@ -1,8 +1,9 @@ -import { Button, Input } from '@nextui-org/react'; +import { Button, Input, Tooltip } from '@nextui-org/react'; import Layout from '../../components/layout'; import useServerConfig from '../../hooks/use-server-config'; import { IconCloudDownload, + IconDeviceFloppy, IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react'; @@ -17,6 +18,7 @@ import { DesktopAPI } from '../../desktop'; import useLaunchParams from '../../hooks/use-launch-params'; import { setLaunchParams } from '../../actions/app'; import { ServerAPI } from '../../server'; +import { useState } from 'react'; const statusDict = { [ServerStatus.STARTED]: 'Server is running', @@ -32,6 +34,7 @@ const Home = () => { const status = useServerStatus(); const consoleEntries = useConsolesById(); const launchParams = useLaunchParams(); + const [saving, setSaving] = useState(false); const startDisabled = status !== ServerStatus.STOPPED; const stopDisabled = status !== ServerStatus.STARTED; @@ -42,6 +45,12 @@ const Home = () => { setLaunchParams(e.target.value || ''); }; + const onSaveLaunchParamsClick = async () => { + setSaving(true); + await ServerAPI.saveLaunchParams(launchParams || ''); + setSaving(false); + }; + return ( { - +
+ + + + +
diff --git a/app/frontend/src/selectors/app.ts b/app/frontend/src/selectors/app.ts index cc72033..40e15dd 100644 --- a/app/frontend/src/selectors/app.ts +++ b/app/frontend/src/selectors/app.ts @@ -11,7 +11,7 @@ export const steamImagesCacheSelector = (state: IRootState) => state.app.steamImagesCache; export const launchParamsSelector = (state: IRootState) => - state.app.settings.launchParams; + state.app.launchParams; export const rconCredentialsSelector = (state: IRootState) => state.app.rconCredentials; diff --git a/app/frontend/src/server.ts b/app/frontend/src/server.ts index f158383..3c6ec11 100644 --- a/app/frontend/src/server.ts +++ b/app/frontend/src/server.ts @@ -3,12 +3,7 @@ import { parseConfig, serializeConfig } from './helpers/config-parser'; import { setConfig, setSaveName } from './actions/server'; import { ConfigKey, TConfig } from './types/server-config'; import { store } from './store'; -import { - notifyError, - notifySuccess, - saveSettings, - setRconCredentials -} from './actions/app'; +import { notifyError, notifySuccess, setRconCredentials } from './actions/app'; import { socketStateSelector } from './selectors/socket'; import { onBackupListUpdated, @@ -17,6 +12,8 @@ import { } from './actions/socket'; import { DesktopAPI } from './desktop'; +const TIMEOUT_MS = 10000; + export const ServerAPI = { send: async ( event: SocketAction, @@ -26,8 +23,7 @@ export const ServerAPI = { return new Promise((resolve, reject) => { const state = store.getState(); const { socket } = socketStateSelector(state); - - // TODO: add timeout + let timeoutId: number | undefined = undefined; try { const eventId = Math.random().toString(36).substring(2); @@ -43,6 +39,7 @@ export const ServerAPI = { const response = JSON.parse(event.data); if (response.eventId === eventId) { + clearTimeout(timeoutId); socket.removeEventListener('message', onMessage); delete response.eventId; @@ -59,8 +56,14 @@ export const ServerAPI = { } }; + timeoutId = setTimeout(() => { + socket.removeEventListener('message', onMessage); + reject(new Error('Request timed out')); + }, TIMEOUT_MS); + socket.addEventListener('message', onMessage); } catch (error) { + clearTimeout(timeoutId); DesktopAPI.logToFile('Unknown error while handling socket event'); reject(error ?? 'Unknown error'); } @@ -79,13 +82,16 @@ export const ServerAPI = { ); }, writeConfig: async (config: TConfig) => { - const serializedConfig = serializeConfig(config); - await ServerAPI.send(SocketAction.WRITE_CONFIG, { - config: serializedConfig - }); - - // // Read again to confirm and update the store - // await ServerAPI.fetchConfig(); + try { + const serializedConfig = serializeConfig(config); + await ServerAPI.send(SocketAction.WRITE_CONFIG, { + config: serializedConfig + }); + + notifySuccess('Config saved'); + } catch { + notifyError('Could not save config'); + } }, fetchSaveName: async () => { const { data: saveNameString } = await ServerAPI.send( @@ -95,14 +101,15 @@ export const ServerAPI = { setSaveName(saveNameString); }, writeSaveName: async (saveName: string) => { - await ServerAPI.send(SocketAction.WRITE_SAVE_NAME, { saveName }); - - // Read again to confirm and update the store - await ServerAPI.fetchSaveName(); + try { + await ServerAPI.send(SocketAction.WRITE_SAVE_NAME, { saveName }); + notifySuccess('Save name saved'); + } catch { + notifyError('Could not save save name'); + } }, start: async () => { ServerAPI.send(SocketAction.START_SERVER); - saveSettings(); }, stop: () => { ServerAPI.send(SocketAction.STOP_SERVER); @@ -118,6 +125,14 @@ export const ServerAPI = { onClientInited(data); }, + saveLaunchParams: async (launchParams: string) => { + try { + await ServerAPI.send(SocketAction.SAVE_LAUNCH_PARAMS, { launchParams }); + notifySuccess('Launch params saved'); + } catch { + notifyError('Could not save launch params'); + } + }, backups: { start: async (interval: number, keepCount: number) => { try { diff --git a/app/frontend/src/store/app-slice.ts b/app/frontend/src/store/app-slice.ts index 60245cb..c7d3e6f 100644 --- a/app/frontend/src/store/app-slice.ts +++ b/app/frontend/src/store/app-slice.ts @@ -8,9 +8,6 @@ const getStoredSettings = () => { return { theme: stored.theme ?? 'dark', - launchParams: - stored.launchParams ?? - '-useperfthreads -NoAsyncLoadingThread -UseMultithreadForDS', serverCredentials: { host: stored.serverCredentials?.host ?? '127.0.0.1:21577', apiKey: stored.serverCredentials?.apiKey ?? '' @@ -27,6 +24,7 @@ export interface IAppState { password: string; }; socket: WebSocket | undefined; + launchParams: string; } const initialState: IAppState = { @@ -37,7 +35,8 @@ const initialState: IAppState = { rconCredentials: { host: '', password: '' - } + }, + launchParams: '' }; export const appSlice = createSlice({ @@ -62,7 +61,7 @@ export const appSlice = createSlice({ state.steamImagesCache[action.payload.steamId] = action.payload.imageUrl; }, setLaunchParams: (state, action) => { - state.settings.launchParams = action.payload; + state.launchParams = action.payload; }, setRconCredentials: (state, action) => { state.rconCredentials = action.payload; diff --git a/app/frontend/src/types/index.ts b/app/frontend/src/types/index.ts index 4f960ec..c5aa3bd 100644 --- a/app/frontend/src/types/index.ts +++ b/app/frontend/src/types/index.ts @@ -42,7 +42,8 @@ export enum SocketAction { OPEN_BACKUP = 'OPEN_BACKUP', RESTORE_BACKUP = 'RESTORE_BACKUP', DOWNLOAD_BACKUP = 'DOWNLOAD_BACKUP', - GET_BACKUPS_SETTINGS = 'GET_BACKUPS_SETTINGS' + GET_BACKUPS_SETTINGS = 'GET_BACKUPS_SETTINGS', + SAVE_LAUNCH_PARAMS = 'SAVE_LAUNCH_PARAMS' } export enum SocketEvent { @@ -51,6 +52,7 @@ export enum SocketEvent { SERVER_SAVE_NAME_CHANGED = 'SERVER_SAVE_NAME_CHANGED', BACKUP_LIST_CHANGED = 'BACKUP_LIST_CHANGED', BACKUP_SETTINGS_CHANGED = 'BACKUP_SETTINGS_CHANGED', + LAUNCH_PARAMS_CHANGED = 'LAUNCH_PARAMS_CHANGED', CUSTOM_ERROR = 'CUSTOM_ERROR', ADD_CONSOLE_ENTRY = 'ADD_CONSOLE_ENTRY' } @@ -72,10 +74,14 @@ export type TServerCredentials = { apiKey: string; }; +export type TClientInitedData = { + currentServerStatus: ServerStatus; + currentLaunchParams: string; +}; + export type TSettings = { theme: 'light' | 'dark'; serverCredentials: TServerCredentials; - launchParams: string | undefined; }; export type TSteamImageMap = { diff --git a/server/api.go b/server/api.go index f4fe21a..8061316 100644 --- a/server/api.go +++ b/server/api.go @@ -36,7 +36,7 @@ func (a *Api) Init() { internalIp := utils.GetOutboundIP() externalIp, err := utils.GetExternalIPv4() if err != nil { - fmt.Println("Failed to get external IP:", err) + utils.LogToFile("Failed to get external IP: "+err.Error(), true) fmt.Printf("Server is running on %s:%d\n", internalIp, utils.Launch.Port) } else { fmt.Printf("Server is running on %s:%d (Local IP: %s:%d)\n", externalIp, utils.Launch.Port, internalIp, utils.Launch.Port) diff --git a/server/client-init-handler.go b/server/client-init-handler.go index 812ad97..27cf800 100644 --- a/server/client-init-handler.go +++ b/server/client-init-handler.go @@ -15,12 +15,17 @@ type ClientInitRequest struct { } } +type ClientInitResData struct { + CurrentServerStatus string `json:"currentServerStatus"` + CurrentLaunchParams string `json:"currentLaunchParams"` +} + type ClientInitRes struct { - Event string `json:"event"` - EventId string `json:"eventId"` - Success bool `json:"success"` - Error string `json:"error"` - Data string `json:"data"` + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` + Data ClientInitResData `json:"data"` } var clientInitEvent = "INIT" @@ -49,6 +54,8 @@ func ClientInitHandler(conn *websocket.Conn, data []byte) { Event: clientInitEvent, EventId: message.EventId, Success: true, - Data: currentState, + Data: ClientInitResData{CurrentServerStatus: currentState, CurrentLaunchParams: utils.Settings.General.LaunchParams}, }) + + // TODO: might be a good idea to send all the needed state to the client (config, backup settings, save name, etc.) instead of making multiple requests } diff --git a/server/main.go b/server/main.go index c4a2944..3b6a303 100644 --- a/server/main.go +++ b/server/main.go @@ -14,7 +14,6 @@ var ( ) func main() { - println(utils.GetCurrentDir()) utils.Init() if utils.Launch.Help { diff --git a/server/save-launch-params-handler.go b/server/save-launch-params-handler.go new file mode 100644 index 0000000..e8c766b --- /dev/null +++ b/server/save-launch-params-handler.go @@ -0,0 +1,64 @@ +package main + +import ( + "encoding/json" + "palworld-ds-gui-server/utils" + + "github.com/gorilla/websocket" +) + +type SaveLaunchParamsRequest struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Data struct { + LaunchParams string `json:"launchParams"` + } +} + +type SaveLaunchParamsRes struct { + Event string `json:"event"` + EventId string `json:"eventId"` + Success bool `json:"success"` + Error string `json:"error"` + Data string `json:"data"` +} + +var saveLaunchParamsEvent = "SAVE_LAUNCH_PARAMS" + +func SaveLaunchParamsHandler(conn *websocket.Conn, data []byte) { + var message SaveLaunchParamsRequest + + err := json.Unmarshal(data, &message) + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(SaveLaunchParamsRes{ + Event: saveLaunchParamsEvent, + EventId: message.EventId, + Success: false, + }) + return + } + + utils.Settings.General.LaunchParams = message.Data.LaunchParams + + err = utils.SaveSettings() + if err != nil { + utils.Log(err.Error()) + conn.WriteJSON(SaveLaunchParamsRes{ + Event: saveLaunchParamsEvent, + EventId: message.EventId, + Success: false, + }) + return + } + + // this response is just to flag the client that the write was successful + // the emit below will send the new save name to all clients + conn.WriteJSON(SaveLaunchParamsRes{ + Event: saveLaunchParamsEvent, + EventId: message.EventId, + Success: true, + }) + + EmitLaunchParams(nil) +} diff --git a/server/start-server-handler.go b/server/start-server-handler.go index ebeb9d6..617e17e 100644 --- a/server/start-server-handler.go +++ b/server/start-server-handler.go @@ -10,9 +10,6 @@ import ( type StartServerRequest struct { Event string `json:"event"` EventId string `json:"eventId"` - Data struct { - LaunchParams []string `json:"launchParams"` - } } type StartServerRes struct { diff --git a/server/utils/utils.go b/server/utils/utils.go index d55ae63..b315bda 100644 --- a/server/utils/utils.go +++ b/server/utils/utils.go @@ -59,24 +59,22 @@ type PersistedSettingsBackup struct { KeepCount int `ini:"keepCount"` } +type PersistedSettingsGeneral struct { + APIKey string `ini:"apiKey"` + LaunchParams string `ini:"launchParams"` +} + type PersistedSettings struct { - General struct { - APIKey string `ini:"apiKey"` - } - Backup PersistedSettingsBackup + General PersistedSettingsGeneral + Backup PersistedSettingsBackup } var Settings PersistedSettings = PersistedSettings{ - General: struct { - APIKey string `ini:"apiKey"` - }{ - APIKey: "CHANGE_ME", + General: PersistedSettingsGeneral{ + APIKey: "CHANGE_ME", + LaunchParams: "-useperfthreads -NoAsyncLoadingThread -UseMultithreadForDS", }, - Backup: struct { - Enabled bool `ini:"enabled"` - Interval float32 `ini:"interval"` - KeepCount int `ini:"keepCount"` - }{ + Backup: PersistedSettingsBackup{ Enabled: false, Interval: 1, KeepCount: 24, diff --git a/server/websocket.go b/server/websocket.go index a82f1bf..6776053 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -114,6 +114,8 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) { RestoreBackupHandler(conn, p) case getBackupsConfigEvent: GetBackupsConfigHandler(conn, p) + case saveLaunchParamsEvent: + SaveLaunchParamsHandler(conn, p) default: utils.LogToFile(fmt.Sprintf("Unknown event: %s", message.Event), true) } @@ -160,8 +162,6 @@ func EmitBackupSettings(exclude *websocket.Conn) { Data utils.PersistedSettingsBackup `json:"data"` } - println("EmitBackupSettings") - BroadcastJSON(BackupSettingsResponse{ Event: "BACKUP_SETTINGS_CHANGED", Success: true, @@ -177,6 +177,14 @@ func EmitSaveName(name string, exclude *websocket.Conn) { }, exclude) } +func EmitLaunchParams(exclude *websocket.Conn) { + BroadcastJSON(BaseResponse{ + Event: "SERVER_SAVE_NAME_CHANGED", + Data: utils.Settings.General.LaunchParams, + Success: true, + }, exclude) +} + func LogToClient(message string, conn *websocket.Conn) { BroadcastJSON(BaseResponse{ Event: "ADD_CONSOLE_ENTRY", From bb30d2f1ee74b3d5a33e8a6625eaf745f0a12c95 Mon Sep 17 00:00:00 2001 From: Diogo Martino Date: Wed, 14 Feb 2024 19:03:59 +0000 Subject: [PATCH 03/15] chore: optimize client init logic --- app/frontend/package.json | 1 + app/frontend/pnpm-lock.yaml | 26 +++++++++++++++++++++++--- app/frontend/src/actions/app.ts | 8 -------- app/frontend/src/actions/server.ts | 15 ++++++++++++++- app/frontend/src/actions/socket.ts | 16 +++++++++++----- app/frontend/src/server.ts | 2 +- app/frontend/src/store/index.ts | 17 ++++++++++++++++- app/frontend/src/types/index.ts | 4 ++++ server/client-init-handler.go | 25 ++++++++++++++++++++----- 9 files changed, 90 insertions(+), 24 deletions(-) diff --git a/app/frontend/package.json b/app/frontend/package.json index a165ad5..86952fc 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -20,6 +20,7 @@ "ws": "^8.16.0" }, "devDependencies": { + "@types/node": "^20.11.17", "@types/react": "^18.2.34", "@types/react-dom": "^18.2.14", "@typescript-eslint/eslint-plugin": "^6.9.1", diff --git a/app/frontend/pnpm-lock.yaml b/app/frontend/pnpm-lock.yaml index 39979c5..998efd6 100644 --- a/app/frontend/pnpm-lock.yaml +++ b/app/frontend/pnpm-lock.yaml @@ -49,6 +49,9 @@ dependencies: version: 8.16.0 devDependencies: + '@types/node': + specifier: ^20.11.17 + version: 20.11.17 '@types/react': specifier: ^18.2.34 version: 18.2.34 @@ -99,7 +102,7 @@ devDependencies: version: 5.2.2 vite: specifier: ^4.5.1 - version: 4.5.1 + version: 4.5.1(@types/node@20.11.17) packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -3522,6 +3525,15 @@ packages: } dev: true + /@types/node@20.11.17: + resolution: + { + integrity: sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw== + } + dependencies: + undici-types: 5.26.5 + dev: true + /@types/prop-types@15.7.9: resolution: { @@ -3737,7 +3749,7 @@ packages: vite: ^4 dependencies: '@swc/core': 1.3.95 - vite: 4.5.1 + vite: 4.5.1(@types/node@20.11.17) transitivePeerDependencies: - '@swc/helpers' dev: true @@ -6160,6 +6172,13 @@ packages: hasBin: true dev: true + /undici-types@5.26.5: + resolution: + { + integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + } + dev: true + /untildify@4.0.0: resolution: { @@ -6289,7 +6308,7 @@ packages: integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== } - /vite@4.5.1: + /vite@4.5.1(@types/node@20.11.17): resolution: { integrity: sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA== @@ -6320,6 +6339,7 @@ packages: terser: optional: true dependencies: + '@types/node': 20.11.17 esbuild: 0.18.20 postcss: 8.4.31 rollup: 3.29.4 diff --git a/app/frontend/src/actions/app.ts b/app/frontend/src/actions/app.ts index fcbf5d3..e705998 100644 --- a/app/frontend/src/actions/app.ts +++ b/app/frontend/src/actions/app.ts @@ -37,14 +37,6 @@ export const saveSettings = (settings?: TSettings) => { localStorage.setItem('settings', JSON.stringify(targetSettings)); }; -export const fetchServerInfo = async () => { - // TODO:: all this data should be returned when SocketAction.INIT is calle - await ServerAPI.fetchConfig(); - await ServerAPI.fetchSaveName(); - await ServerAPI.backups.fetchCurrentSettings(); - await ServerAPI.backups.fetchList(); -}; - export const changeBackupSettings = async ( enabled: boolean, intervalHours: number, diff --git a/app/frontend/src/actions/server.ts b/app/frontend/src/actions/server.ts index 95748f0..df40eb2 100644 --- a/app/frontend/src/actions/server.ts +++ b/app/frontend/src/actions/server.ts @@ -1,10 +1,23 @@ import { serverSliceActions } from '../store/server-slice'; import { store } from '../store'; -import { TConfig } from '../types/server-config'; +import { ConfigKey, TConfig } from '../types/server-config'; import { ServerStatus } from '../types'; import { consolesSliceActions } from '../store/console-slice'; +import { rconCredentialsSelector } from '../selectors/app'; +import { setRconCredentials } from './app'; export const setConfig = (config: TConfig) => { + const state = store.getState(); + const rconCredentials = rconCredentialsSelector(state); + + // auto fill rcon credentials if they are not set + if (!rconCredentials.host && !rconCredentials.password) { + setRconCredentials( + `127.0.0.1:${config[ConfigKey.RCONPort]}`, + config[ConfigKey.AdminPassword] + ); + } + store.dispatch(serverSliceActions.setConfig(config)); }; diff --git a/app/frontend/src/actions/socket.ts b/app/frontend/src/actions/socket.ts index a78793d..9e0d5f7 100644 --- a/app/frontend/src/actions/socket.ts +++ b/app/frontend/src/actions/socket.ts @@ -8,8 +8,8 @@ import { TClientInitedData, TConsoleEntry } from '../types'; -import { fetchServerInfo, setLaunchParams } from './app'; -import { setBackupsList, setConfig, setSaveName, setStatus } from './server'; +import { setLaunchParams } from './app'; +import { setBackupsList, setConfig, setSaveName } from './server'; import { parseConfig } from '../helpers/config-parser'; import { addConsoleEntry } from './console'; @@ -56,11 +56,17 @@ export const onAddConsoleEntry = (message: string) => { }; export const onClientInited = (data: TClientInitedData) => { - setStatus(data.currentServerStatus); - setLaunchParams(data.currentLaunchParams); + console.log('! onClientInited', data); + + onServerStatusChanged(data.currentServerStatus); + onLaunchParamsChanged(data.currentLaunchParams); + onServerConfigChanged(data.currentConfig); + onServerSaveNameChanged(data.currentSaveName); + onBackupSettingsUpdated(data.currentBackupsSettings); + onBackupListUpdated(data.currentBackupsList); + setSocketInited(true); setSocketConnecting(false); - fetchServerInfo(); }; export const onBackupListUpdated = (data) => { diff --git a/app/frontend/src/server.ts b/app/frontend/src/server.ts index 3c6ec11..32a5333 100644 --- a/app/frontend/src/server.ts +++ b/app/frontend/src/server.ts @@ -23,7 +23,7 @@ export const ServerAPI = { return new Promise((resolve, reject) => { const state = store.getState(); const { socket } = socketStateSelector(state); - let timeoutId: number | undefined = undefined; + let timeoutId: NodeJS.Timeout | undefined = undefined; try { const eventId = Math.random().toString(36).substring(2); diff --git a/app/frontend/src/store/index.ts b/app/frontend/src/store/index.ts index 675b9be..3da76e8 100644 --- a/app/frontend/src/store/index.ts +++ b/app/frontend/src/store/index.ts @@ -5,6 +5,21 @@ import consoleSlice from './console-slice'; import serverSlice from './server-slice'; import socketSlice from './socket-slice'; +// Logger middleware (only in development) +const loggerMiddleware = (store) => (next) => (action) => { + const prevState = store.getState(); + const result = next(action); + const nextState = store.getState(); + + console.debug('[STORE DEBUG]', action.type, { + action, + prevState, + nextState + }); + + return result; +}; + export const store = configureStore({ reducer: { app: appSlice, @@ -16,7 +31,7 @@ export const store = configureStore({ middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false - }) + }).concat(process.env.NODE_ENV !== 'production' ? [loggerMiddleware] : []) }); export type IRootState = ReturnType; diff --git a/app/frontend/src/types/index.ts b/app/frontend/src/types/index.ts index c5aa3bd..dbc2b74 100644 --- a/app/frontend/src/types/index.ts +++ b/app/frontend/src/types/index.ts @@ -77,6 +77,10 @@ export type TServerCredentials = { export type TClientInitedData = { currentServerStatus: ServerStatus; currentLaunchParams: string; + currentConfig: string; + currentSaveName: string; + currentBackupsSettings: TBackupSettings; + currentBackupsList: TBackup[]; }; export type TSettings = { diff --git a/server/client-init-handler.go b/server/client-init-handler.go index 27cf800..fa4cdb2 100644 --- a/server/client-init-handler.go +++ b/server/client-init-handler.go @@ -16,8 +16,12 @@ type ClientInitRequest struct { } type ClientInitResData struct { - CurrentServerStatus string `json:"currentServerStatus"` - CurrentLaunchParams string `json:"currentLaunchParams"` + CurrentServerStatus string `json:"currentServerStatus"` + CurrentLaunchParams string `json:"currentLaunchParams"` + CurrentConfig string `json:"currentConfig"` + CurrentSaveName string `json:"currentSaveName"` + CurrentBackupsSettings utils.PersistedSettingsBackup `json:"currentBackupsSettings"` + CurrentBackupsList []Backup `json:"currentBackupsList"` } type ClientInitRes struct { @@ -50,12 +54,23 @@ func ClientInitHandler(conn *websocket.Conn, data []byte) { currentState = "STARTED" } + backupsList, err := backupmanager.GetBackupsList() + if err != nil { + utils.Log(err.Error()) + backupsList = []Backup{} + } + conn.WriteJSON(ClientInitRes{ Event: clientInitEvent, EventId: message.EventId, Success: true, - Data: ClientInitResData{CurrentServerStatus: currentState, CurrentLaunchParams: utils.Settings.General.LaunchParams}, + Data: ClientInitResData{ + CurrentServerStatus: currentState, + CurrentLaunchParams: utils.Settings.General.LaunchParams, + CurrentConfig: ReadConfig(), + CurrentSaveName: ReadSaveName(), + CurrentBackupsSettings: utils.Settings.Backup, + CurrentBackupsList: backupsList, + }, }) - - // TODO: might be a good idea to send all the needed state to the client (config, backup settings, save name, etc.) instead of making multiple requests } From 325dcc5bd117bdab8516ea2d6556c8b9316ebb9e Mon Sep 17 00:00:00 2001 From: Diogo Martino Date: Wed, 14 Feb 2024 19:41:53 +0000 Subject: [PATCH 04/15] chore: add version mismatch warning --- app/frontend/src/actions/server.ts | 4 + app/frontend/src/actions/socket.ts | 19 ++++- .../modals/confirm-action/index.tsx | 75 ++++++++++--------- app/frontend/src/components/modals/index.tsx | 6 +- .../modals/version-mismatch/index.tsx | 70 +++++++++++++++++ app/frontend/src/main.css | 3 +- app/frontend/src/store/server-slice.ts | 7 +- app/frontend/src/types/index.ts | 4 +- server/client-init-handler.go | 2 + server/go.mod | 3 + server/go.sum | 6 ++ server/main.go | 6 +- server/server.json | 7 ++ server/utils/utils.go | 11 ++- 14 files changed, 176 insertions(+), 47 deletions(-) create mode 100644 app/frontend/src/components/modals/version-mismatch/index.tsx create mode 100644 server/server.json diff --git a/app/frontend/src/actions/server.ts b/app/frontend/src/actions/server.ts index df40eb2..d9d4e50 100644 --- a/app/frontend/src/actions/server.ts +++ b/app/frontend/src/actions/server.ts @@ -37,3 +37,7 @@ export const clearServerState = () => { export const setBackupsList = (backupsList: any) => { store.dispatch(serverSliceActions.setBackupsList(backupsList)); }; + +export const setServerVersion = (version: string) => { + store.dispatch(serverSliceActions.setVersion(version)); +}; diff --git a/app/frontend/src/actions/socket.ts b/app/frontend/src/actions/socket.ts index 9e0d5f7..314251a 100644 --- a/app/frontend/src/actions/socket.ts +++ b/app/frontend/src/actions/socket.ts @@ -2,6 +2,7 @@ import { socketSliceActions } from '../store/socket-slice'; import { serverSliceActions } from '../store/server-slice'; import { store } from '../store'; import { + Modal, ServerStatus, TBackup, TBackupSettings, @@ -9,9 +10,15 @@ import { TConsoleEntry } from '../types'; import { setLaunchParams } from './app'; -import { setBackupsList, setConfig, setSaveName } from './server'; +import { + setBackupsList, + setConfig, + setSaveName, + setServerVersion +} from './server'; import { parseConfig } from '../helpers/config-parser'; import { addConsoleEntry } from './console'; +import { openModal } from './modal'; export const setSocket = (socket: WebSocket) => { store.dispatch(socketSliceActions.setSocket(socket)); @@ -56,8 +63,6 @@ export const onAddConsoleEntry = (message: string) => { }; export const onClientInited = (data: TClientInitedData) => { - console.log('! onClientInited', data); - onServerStatusChanged(data.currentServerStatus); onLaunchParamsChanged(data.currentLaunchParams); onServerConfigChanged(data.currentConfig); @@ -65,8 +70,16 @@ export const onClientInited = (data: TClientInitedData) => { onBackupSettingsUpdated(data.currentBackupsSettings); onBackupListUpdated(data.currentBackupsList); + setServerVersion(data.serverVersion); setSocketInited(true); setSocketConnecting(false); + + if (data.serverVersion !== APP_VERSION) { + openModal(Modal.ACTION_CONFIRMATION, { + clientVersion: APP_VERSION, + serverVersion: data.serverVersion + }); + } }; export const onBackupListUpdated = (data) => { diff --git a/app/frontend/src/components/modals/confirm-action/index.tsx b/app/frontend/src/components/modals/confirm-action/index.tsx index 5142bfb..90769e3 100644 --- a/app/frontend/src/components/modals/confirm-action/index.tsx +++ b/app/frontend/src/components/modals/confirm-action/index.tsx @@ -8,58 +8,63 @@ import { } from '@nextui-org/react'; import { closeModals } from '../../../actions/modal'; import useModalsInfo from '../../../hooks/use-modals-info'; -import { useMemo } from 'react'; +import { DesktopAPI } from '../../../desktop'; -type TConfirmActionModalProps = { - onCancel?: () => void; - onConfirm?: () => void; - title?: string; - message?: string | React.ReactNode; - confirmLabel?: string; - cancelLabel?: string; - variant?: 'danger' | 'info'; +type TVersionMismatchModalProps = { + clientVersion: string; + serverVersion: string; }; -const ConfirmActionModal = ({ - onCancel, - onConfirm, - title, - message, - confirmLabel, - cancelLabel, - variant -}: TConfirmActionModalProps) => { +const VersionMismatchModal = ({ + clientVersion, + serverVersion +}: TVersionMismatchModalProps) => { const { isModalOpen } = useModalsInfo(); - const buttonColor = useMemo( - () => (variant === 'danger' ? 'danger' : 'primary'), - [variant] - ); return ( { - onCancel?.(); closeModals(); }} scrollBehavior="inside" > - {title ?? 'Please confirm your action.'} +

Version Mismatch

- {message ?? 'Are you sure?'} - - -
@@ -67,4 +72,4 @@ const ConfirmActionModal = ({ ); }; -export default ConfirmActionModal; +export default VersionMismatchModal; diff --git a/app/frontend/src/components/modals/index.tsx b/app/frontend/src/components/modals/index.tsx index 246c331..b80b2ed 100644 --- a/app/frontend/src/components/modals/index.tsx +++ b/app/frontend/src/components/modals/index.tsx @@ -2,9 +2,11 @@ import { createElement } from 'react'; import useModalsInfo from '../../hooks/use-modals-info'; import { Modal } from '../../types'; import ConfirmActionModal from './confirm-action'; +import VersionMismatchModal from './confirm-action'; const ModalsMap = { - [Modal.ACTION_CONFIRMATION]: ConfirmActionModal + [Modal.ACTION_CONFIRMATION]: ConfirmActionModal, + [Modal.VERSION_MISMATCH]: VersionMismatchModal }; const ModalsProvider = () => { @@ -12,7 +14,7 @@ const ModalsProvider = () => { if (!openModal || !ModalsMap[openModal]) return null; - return createElement(ModalsMap[openModal], modalProps); + return createElement(ModalsMap[openModal], modalProps); }; export default ModalsProvider; diff --git a/app/frontend/src/components/modals/version-mismatch/index.tsx b/app/frontend/src/components/modals/version-mismatch/index.tsx new file mode 100644 index 0000000..58555c4 --- /dev/null +++ b/app/frontend/src/components/modals/version-mismatch/index.tsx @@ -0,0 +1,70 @@ +import { + Button, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader +} from '@nextui-org/react'; +import { closeModals } from '../../../actions/modal'; +import useModalsInfo from '../../../hooks/use-modals-info'; +import { useMemo } from 'react'; + +type TConfirmActionModalProps = { + onCancel?: () => void; + onConfirm?: () => void; + title?: string; + message?: string | React.ReactNode; + confirmLabel?: string; + cancelLabel?: string; + variant?: 'danger' | 'info'; +}; + +const ConfirmActionModal = ({ + onCancel, + onConfirm, + title, + message, + confirmLabel, + cancelLabel, + variant +}: TConfirmActionModalProps) => { + const { isModalOpen } = useModalsInfo(); + const buttonColor = useMemo( + () => (variant === 'danger' ? 'danger' : 'primary'), + [variant] + ); + + return ( + { + onCancel?.(); + closeModals(); + }} + scrollBehavior="inside" + > + + +

{title ?? 'Please confirm your action.'}

+
+ {message ?? 'Are you sure?'} + + + + +
+
+ ); +}; + +export default ConfirmActionModal; diff --git a/app/frontend/src/main.css b/app/frontend/src/main.css index 84dea19..1527fe0 100644 --- a/app/frontend/src/main.css +++ b/app/frontend/src/main.css @@ -16,7 +16,8 @@ body, height: 100%; } -p { +p, +span { cursor: default; user-select: none; } diff --git a/app/frontend/src/store/server-slice.ts b/app/frontend/src/store/server-slice.ts index bd1c5ff..4c1e5f6 100644 --- a/app/frontend/src/store/server-slice.ts +++ b/app/frontend/src/store/server-slice.ts @@ -8,6 +8,7 @@ export interface IServerState { status: ServerStatus; backupsList: TBackup[]; backupSettings: TBackupSettings; + version: string | undefined; } const initialState: IServerState = { @@ -19,7 +20,8 @@ const initialState: IServerState = { enabled: false, intervalHours: 1, keepCount: 24 - } + }, + version: undefined }; export const serverSlice = createSlice({ @@ -45,6 +47,9 @@ export const serverSlice = createSlice({ }, setBackupSettings: (state, action) => { state.backupSettings = action.payload; + }, + setVersion: (state, action) => { + state.version = action.payload; } } }); diff --git a/app/frontend/src/types/index.ts b/app/frontend/src/types/index.ts index dbc2b74..be1a692 100644 --- a/app/frontend/src/types/index.ts +++ b/app/frontend/src/types/index.ts @@ -7,7 +7,8 @@ export type TGenericObject = { export type TGenericFunction = (...args: any[]) => any; export enum Modal { - ACTION_CONFIRMATION = 'ACTION_CONFIRMATION' + ACTION_CONFIRMATION = 'ACTION_CONFIRMATION', + VERSION_MISMATCH = 'VERSION_MISMATCH' } export enum ServerStatus { @@ -81,6 +82,7 @@ export type TClientInitedData = { currentSaveName: string; currentBackupsSettings: TBackupSettings; currentBackupsList: TBackup[]; + serverVersion: string; }; export type TSettings = { diff --git a/server/client-init-handler.go b/server/client-init-handler.go index fa4cdb2..f3346d9 100644 --- a/server/client-init-handler.go +++ b/server/client-init-handler.go @@ -22,6 +22,7 @@ type ClientInitResData struct { CurrentSaveName string `json:"currentSaveName"` CurrentBackupsSettings utils.PersistedSettingsBackup `json:"currentBackupsSettings"` CurrentBackupsList []Backup `json:"currentBackupsList"` + ServerVersion string `json:"serverVersion"` } type ClientInitRes struct { @@ -71,6 +72,7 @@ func ClientInitHandler(conn *websocket.Conn, data []byte) { CurrentSaveName: ReadSaveName(), CurrentBackupsSettings: utils.Settings.Backup, CurrentBackupsList: backupsList, + ServerVersion: utils.Config.ServerVersion, }, }) } diff --git a/server/go.mod b/server/go.mod index 1624b60..f2ec6a7 100644 --- a/server/go.mod +++ b/server/go.mod @@ -17,6 +17,9 @@ require ( github.com/nwaples/rardecode v1.1.0 // indirect github.com/pierrec/lz4/v4 v4.1.2 // indirect github.com/robfig/cron v1.2.0 // indirect + github.com/tidwall/gjson v1.17.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/ulikunitz/xz v0.5.9 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect golang.org/x/net v0.20.0 // indirect diff --git a/server/go.sum b/server/go.sum index d1aea07..a2dccda 100644 --- a/server/go.sum +++ b/server/go.sum @@ -24,6 +24,12 @@ github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM= github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I= github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= diff --git a/server/main.go b/server/main.go index 3b6a303..c747faa 100644 --- a/server/main.go +++ b/server/main.go @@ -1,6 +1,7 @@ package main import ( + _ "embed" "flag" "os" "palworld-ds-gui-server/utils" @@ -13,8 +14,11 @@ var ( api *Api ) +//go:embed server.json +var serverJSON string + func main() { - utils.Init() + utils.Init(serverJSON) if utils.Launch.Help { flag.PrintDefaults() diff --git a/server/server.json b/server/server.json new file mode 100644 index 0000000..ba07a39 --- /dev/null +++ b/server/server.json @@ -0,0 +1,7 @@ +{ + "companyName": "DiogoMartino", + "productName": "Palworld Dedicated Server GUI Server", + "productVersion": "0.0.7", + "copyright": "2024", + "comments": "https://github.com/diogomartino/palworld-ds-gui" +} diff --git a/server/utils/utils.go b/server/utils/utils.go index b315bda..aab4144 100644 --- a/server/utils/utils.go +++ b/server/utils/utils.go @@ -14,6 +14,7 @@ import ( "github.com/gorilla/websocket" "github.com/mitchellh/go-ps" + "github.com/tidwall/gjson" "gopkg.in/ini.v1" ) @@ -33,6 +34,7 @@ type AppConfig struct { LogsPath string PersistedSettingsPath string AppId string + ServerVersion string } var Config AppConfig = AppConfig{ @@ -51,6 +53,7 @@ var Config AppConfig = AppConfig{ PersistedSettingsPath: filepath.Join(GetCurrentDir(), "gui-server-settings.ini"), SteamCmdUrl: "https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip", AppId: "2394010", + ServerVersion: "0.0.7", } type PersistedSettingsBackup struct { @@ -97,7 +100,9 @@ var Launch LaunchParams = LaunchParams{ var EmitConsoleLog func(message string, excludeClient *websocket.Conn) -func Init() { +func Init(serverJSON string) { + Config.ServerVersion = gjson.Get(serverJSON, "productVersion").String() + logsFile, logErr := os.OpenFile(Config.LogsPath, os.O_RDWR|os.O_CREATE, 0666) if logErr != nil { panic(logErr) @@ -115,14 +120,14 @@ func Init() { panic(err) } - SaveSettings() - flag.BoolVar(&Launch.ForceNewKey, "newkey", false, "Generate a new API key") flag.BoolVar(&Launch.ShowKey, "showkey", false, "Show the current API key") flag.BoolVar(&Launch.Help, "help", false, "Show help") flag.IntVar(&Launch.Port, "port", 21577, "Port to run the server on") flag.Parse() + + LogToFile("utils.go: Init() - Palword Dedicated Server GUI v"+Config.ServerVersion, false) } func LoadSettings() error { From 6eb2c364d720fdd6cf5c4baf81d5c38a281b8704 Mon Sep 17 00:00:00 2001 From: Diogo Martino Date: Wed, 14 Feb 2024 20:20:58 +0000 Subject: [PATCH 05/15] chore: download backups --- app/app.go | 41 +++++++++++++++++++ app/frontend/src/desktop.ts | 3 ++ app/frontend/src/screens/backups/index.tsx | 23 ++++++++++- .../src/screens/initializing/index.tsx | 1 + app/frontend/src/wailsjs/go/main/App.d.ts | 2 + app/frontend/src/wailsjs/go/main/App.js | 4 ++ app/utils/utils.go | 19 --------- server/api.go | 28 +++++++++++++ 8 files changed, 101 insertions(+), 20 deletions(-) diff --git a/app/app.go b/app/app.go index 4cc2bb3..d291ed0 100644 --- a/app/app.go +++ b/app/app.go @@ -3,8 +3,12 @@ package main import ( "context" "fmt" + "io" + "net/http" + "os" rconclient "palword-ds-gui/rcon-client" "palword-ds-gui/utils" + "path" "github.com/gocolly/colly/v2" "github.com/wailsapp/wails/v2/pkg/runtime" @@ -81,3 +85,40 @@ func (a *App) SaveLog(log string) { func (a *App) OpenLogs() { utils.OpenExplorerWithFile(utils.Config.AppDataDir, "logs.txt") } + +func (a *App) DownloadFile(url string, filename string, authToken string) error { + path := path.Join(utils.GetCurrentDir(), filename) + utils.LogToFile(fmt.Sprintf("Downloading %s to %s", url, path)) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + + req.Header.Set("Authorization", authToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("server returned non-200 status: %d %s", resp.StatusCode, resp.Status) + } + + out, err := os.Create(path) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + if err != nil { + return err + } + + utils.OpenExplorerWithFile(utils.GetCurrentDir(), filename) + + return nil +} diff --git a/app/frontend/src/desktop.ts b/app/frontend/src/desktop.ts index 1bc89fc..a50a08e 100644 --- a/app/frontend/src/desktop.ts +++ b/app/frontend/src/desktop.ts @@ -39,6 +39,9 @@ export const DesktopAPI = { openLogFile: async () => { await App.OpenLogs(); }, + downloadFile: async (url: string, fileName: string, apiKey: string) => { + await App.DownloadFile(url, fileName, apiKey); + }, rcon: { execute: async (command: string) => { const state = store.getState(); diff --git a/app/frontend/src/screens/backups/index.tsx b/app/frontend/src/screens/backups/index.tsx index d1aba8d..f11bf4e 100644 --- a/app/frontend/src/screens/backups/index.tsx +++ b/app/frontend/src/screens/backups/index.tsx @@ -27,10 +27,12 @@ import { } from '@tabler/icons-react'; import { requestConfirmation } from '../../actions/modal'; import { TGenericObject } from '../../types'; -import { changeBackupSettings } from '../../actions/app'; +import { changeBackupSettings, notifySuccess } from '../../actions/app'; import { ServerAPI } from '../../server'; import useBackupsList from '../../hooks/use-backups-list'; import useBackupSettings from '../../hooks/use-backup-settings'; +import useServerCredentials from '../../hooks/use-server-credentials'; +import { DesktopAPI } from '../../desktop'; const columns = [ { @@ -58,6 +60,8 @@ type TBackupActionsProps = { }; const BackupActions = ({ backup }: TBackupActionsProps) => { + const serverCredentials = useServerCredentials(); + const onRestoreClick = async () => { await requestConfirmation({ title: 'Confirmation', @@ -83,6 +87,16 @@ const BackupActions = ({ backup }: TBackupActionsProps) => { }); }; + const onDownloadClick = () => { + DesktopAPI.downloadFile( + `http://${serverCredentials.host}/backups/${backup.originalName}`, + backup.originalName, + serverCredentials.apiKey + ); + + notifySuccess('Backup download started.'); + }; + return ( @@ -91,6 +105,13 @@ const BackupActions = ({ backup }: TBackupActionsProps) => { + } + onClick={onDownloadClick} + > + Download + } diff --git a/app/frontend/src/screens/initializing/index.tsx b/app/frontend/src/screens/initializing/index.tsx index 081e957..05fe33e 100644 --- a/app/frontend/src/screens/initializing/index.tsx +++ b/app/frontend/src/screens/initializing/index.tsx @@ -51,6 +51,7 @@ const Initializing = () => { diff --git a/app/frontend/src/wailsjs/go/main/App.d.ts b/app/frontend/src/wailsjs/go/main/App.d.ts index da6ccd2..25ff198 100644 --- a/app/frontend/src/wailsjs/go/main/App.d.ts +++ b/app/frontend/src/wailsjs/go/main/App.d.ts @@ -1,6 +1,8 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT +export function DownloadFile(arg1:string,arg2:string,arg3:string):Promise; + export function GetSteamProfileURL(arg1:string):Promise; export function InitApp():Promise; diff --git a/app/frontend/src/wailsjs/go/main/App.js b/app/frontend/src/wailsjs/go/main/App.js index b076159..641d9d1 100644 --- a/app/frontend/src/wailsjs/go/main/App.js +++ b/app/frontend/src/wailsjs/go/main/App.js @@ -2,6 +2,10 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT +export function DownloadFile(arg1, arg2, arg3) { + return window['go']['main']['App']['DownloadFile'](arg1, arg2, arg3); +} + export function GetSteamProfileURL(arg1) { return window['go']['main']['App']['GetSteamProfileURL'](arg1); } diff --git a/app/utils/utils.go b/app/utils/utils.go index 2b2d1d6..ab82506 100644 --- a/app/utils/utils.go +++ b/app/utils/utils.go @@ -2,8 +2,6 @@ package utils import ( "fmt" - "io" - "net/http" "os" "os/exec" "path/filepath" @@ -43,23 +41,6 @@ func GetAppDataDir() string { return filepath.Join(appDataDir, "PalworldDSGUI") } -func DownloadFile(url string, path string) error { - resp, err := http.Get(url) - if err != nil { - return err - } - defer resp.Body.Close() - - out, err := os.Create(path) - if err != nil { - return err - } - defer out.Close() - - _, err = io.Copy(out, resp.Body) - return err -} - func LogToFile(message string) { logsFile, err := os.OpenFile(Config.LogsPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { diff --git a/server/api.go b/server/api.go index 8061316..a103c57 100644 --- a/server/api.go +++ b/server/api.go @@ -6,7 +6,9 @@ import ( "encoding/hex" "fmt" "net/http" + "os" "palworld-ds-gui-server/utils" + "path" ) type Api struct { @@ -45,6 +47,32 @@ func (a *Api) Init() { utils.EmitConsoleLog = LogToClient http.HandleFunc("/ws", handleWebSocket) + http.HandleFunc("/backups/", func(w http.ResponseWriter, r *http.Request) { + apiKey := r.Header.Get("Authorization") + + fmt.Printf("API Key: %s\n", apiKey) + + if apiKey != utils.Settings.General.APIKey { + utils.Log(fmt.Sprintf("Unauthorized backup download from %s", r.RemoteAddr)) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Extract the specific file path from the URL. + filePath := r.URL.Path[len("/backups/"):] + + // Safely join the base path with the requested file path to avoid directory traversal attacks. + safeFilePath := path.Join(utils.Config.BackupsPath, filePath) + + // Ensure the file exists and is not a directory before serving. + if info, err := os.Stat(safeFilePath); err != nil || info.IsDir() { + http.NotFound(w, r) + return + } + + // Serve the requested file. + http.ServeFile(w, r, safeFilePath) + }) http.ListenAndServe(fmt.Sprintf(":%d", utils.Launch.Port), nil) } From e1627b3139f2794d1c2bdddb1a13075a1719060f Mon Sep 17 00:00:00 2001 From: Diogo Martino Date: Thu, 15 Feb 2024 00:57:13 +0000 Subject: [PATCH 06/15] chore: ci update --- .github/workflows/wails-build.yaml | 55 ++++++++++++++++++------------ 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/.github/workflows/wails-build.yaml b/.github/workflows/wails-build.yaml index 06e0121..71be294 100644 --- a/.github/workflows/wails-build.yaml +++ b/.github/workflows/wails-build.yaml @@ -34,7 +34,7 @@ jobs: id: extract_version shell: bash run: | - version=$(jq -r '.info.productVersion' ./wails.json) + version=$(jq -r '.info.productVersion' ./app/wails.json) echo "Version extracted from wails.json: $version" echo "CURRENT_VERSION=$version" >> "$GITHUB_OUTPUT" @@ -53,14 +53,18 @@ jobs: echo "Bumped version to: $new_version" echo "BUILD_VERSION=$new_version" >> "$GITHUB_OUTPUT" - jq --arg new_version "$new_version" '.info.productVersion = $new_version' ./wails.json > temp_wails.json - mv temp_wails.json ./wails.json + jq --arg new_version "$new_version" '.info.productVersion = $new_version' ./app/wails.json > temp_wails.json + mv temp_wails.json ./app/wails.json - - name: Commit and Push bump - run: | - git add wails.json - git commit -m "Bump version to v${{ steps.bump_version.outputs.BUILD_VERSION }}" - git push + jq --arg new_version "$new_version" '.productVersion = $new_version' ./server/server.json > temp_wails.json + mv temp_wails.json ./server/server.json + + # - name: Commit and Push bump + # run: | + # git add ./app/wails.json + # git add ./server/server.json + # git commit -m "Bump version to v${{ steps.bump_version.outputs.BUILD_VERSION }}" + # git push - name: Setup GoLang uses: actions/setup-go@v4 @@ -73,23 +77,32 @@ jobs: shell: bash - name: Build Windows App - working-directory: . + working-directory: ./app run: wails build --clean --platform windows/amd64 -o PalworldDSGui.exe shell: bash + - name: Build Server + working-directory: ./server + run: go build -o PalworldDSGUI_Server.exe + shell: bash + - name: Upload Artifact uses: actions/upload-artifact@v3 with: name: PalworldDSGui.exe - path: ./build/bin/PalworldDSGui.exe - - - name: Create Release - id: create_release - uses: softprops/action-gh-release@v1 - with: - files: ./build/bin/PalworldDSGui.exe - tag_name: v${{ steps.bump_version.outputs.BUILD_VERSION }} - name: Release v${{ steps.bump_version.outputs.BUILD_VERSION }} - body: | - ## Changelog - No changelog available yet. + path: | + ./app/build/bin/PalworldDSGui.exe + ./server/PalworldDSGUI_Server.exe + + # - name: Create Release + # id: create_release + # uses: softprops/action-gh-release@v1 + # with: + # files: | + # ./app/build/bin/PalworldDSGui.exe + # ./server/PalworldDSGUI_Server.exe + # tag_name: v${{ steps.bump_version.outputs.BUILD_VERSION }} + # name: Release v${{ steps.bump_version.outputs.BUILD_VERSION }} + # body: | + # ## Changelog + # No changelog available yet. From 8dbd12220435d252bb8505961c471b70c9404fea Mon Sep 17 00:00:00 2001 From: Diogo Martino Date: Thu, 15 Feb 2024 01:16:09 +0000 Subject: [PATCH 07/15] fix: disconnect icon --- app/frontend/src/components/sidebar/index.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/frontend/src/components/sidebar/index.tsx b/app/frontend/src/components/sidebar/index.tsx index 4a4e3b5..c357caf 100644 --- a/app/frontend/src/components/sidebar/index.tsx +++ b/app/frontend/src/components/sidebar/index.tsx @@ -48,10 +48,6 @@ const Sidebar = () => { label="Disconnect" onClick={disconnect} iconComponent={IconDoorExit} - iconProps={{ - color: hasUpdates ? '#3b82f6' : '#a0a0a0', - className: hasUpdates && 'animate-ping duration-1000' - }} /> From a8f097511e3389aae4cfa93d45359cb2cc733e4b Mon Sep 17 00:00:00 2001 From: Diogo Martino Date: Thu, 15 Feb 2024 01:16:17 +0000 Subject: [PATCH 08/15] chore: finished ci for app and server --- .github/workflows/wails-build.yaml | 37 +++++++++++++++--------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/.github/workflows/wails-build.yaml b/.github/workflows/wails-build.yaml index 71be294..8325fef 100644 --- a/.github/workflows/wails-build.yaml +++ b/.github/workflows/wails-build.yaml @@ -59,12 +59,12 @@ jobs: jq --arg new_version "$new_version" '.productVersion = $new_version' ./server/server.json > temp_wails.json mv temp_wails.json ./server/server.json - # - name: Commit and Push bump - # run: | - # git add ./app/wails.json - # git add ./server/server.json - # git commit -m "Bump version to v${{ steps.bump_version.outputs.BUILD_VERSION }}" - # git push + - name: Commit and Push bump + run: | + git add ./app/wails.json + git add ./server/server.json + git commit -m "Bump version to v${{ steps.bump_version.outputs.BUILD_VERSION }}" + git push - name: Setup GoLang uses: actions/setup-go@v4 @@ -94,15 +94,16 @@ jobs: ./app/build/bin/PalworldDSGui.exe ./server/PalworldDSGUI_Server.exe - # - name: Create Release - # id: create_release - # uses: softprops/action-gh-release@v1 - # with: - # files: | - # ./app/build/bin/PalworldDSGui.exe - # ./server/PalworldDSGUI_Server.exe - # tag_name: v${{ steps.bump_version.outputs.BUILD_VERSION }} - # name: Release v${{ steps.bump_version.outputs.BUILD_VERSION }} - # body: | - # ## Changelog - # No changelog available yet. + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v1 + with: + files: | + ./app/build/bin/PalworldDSGui.exe + ./server/PalworldDSGUI_Server.exe + tag_name: v${{ steps.bump_version.outputs.BUILD_VERSION }} + name: Release v${{ steps.bump_version.outputs.BUILD_VERSION }} + draft: true + body: | + ## Changelog + No changelog available yet. From 13fb85d8c9a21207606dd2e43a9996e84576db74 Mon Sep 17 00:00:00 2001 From: Diogo Martino Date: Thu, 15 Feb 2024 18:45:16 +0000 Subject: [PATCH 09/15] chore: test linux build --- .github/workflows/wails-build.yaml | 53 +++++++++++++++++------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/.github/workflows/wails-build.yaml b/.github/workflows/wails-build.yaml index 8325fef..c5e47da 100644 --- a/.github/workflows/wails-build.yaml +++ b/.github/workflows/wails-build.yaml @@ -59,12 +59,12 @@ jobs: jq --arg new_version "$new_version" '.productVersion = $new_version' ./server/server.json > temp_wails.json mv temp_wails.json ./server/server.json - - name: Commit and Push bump - run: | - git add ./app/wails.json - git add ./server/server.json - git commit -m "Bump version to v${{ steps.bump_version.outputs.BUILD_VERSION }}" - git push + # - name: Commit and Push bump + # run: | + # git add ./app/wails.json + # git add ./server/server.json + # git commit -m "Bump version to v${{ steps.bump_version.outputs.BUILD_VERSION }}" + # git push - name: Setup GoLang uses: actions/setup-go@v4 @@ -81,9 +81,14 @@ jobs: run: wails build --clean --platform windows/amd64 -o PalworldDSGui.exe shell: bash - - name: Build Server + - name: Build Windows Server + working-directory: ./server + run: go build -o PalworldDSGUI_WinServer.exe + shell: bash + + - name: Build Linux Server working-directory: ./server - run: go build -o PalworldDSGUI_Server.exe + run: GOOS=linux GOARCH=amd64 go build -o PalworldDSGUI_LinuxServer shell: bash - name: Upload Artifact @@ -92,18 +97,20 @@ jobs: name: PalworldDSGui.exe path: | ./app/build/bin/PalworldDSGui.exe - ./server/PalworldDSGUI_Server.exe - - - name: Create Release - id: create_release - uses: softprops/action-gh-release@v1 - with: - files: | - ./app/build/bin/PalworldDSGui.exe - ./server/PalworldDSGUI_Server.exe - tag_name: v${{ steps.bump_version.outputs.BUILD_VERSION }} - name: Release v${{ steps.bump_version.outputs.BUILD_VERSION }} - draft: true - body: | - ## Changelog - No changelog available yet. + ./server/PalworldDSGUI_WinServer.exe + ./server/PalworldDSGUI_LinuxServer + + # - name: Create Release + # id: create_release + # uses: softprops/action-gh-release@v1 + # with: + # files: | + # ./app/build/bin/PalworldDSGui.exe + # ./server/PalworldDSGUI_WinServer.exe + # ./server/PalworldDSGUI_LinuxServer + # tag_name: v${{ steps.bump_version.outputs.BUILD_VERSION }} + # name: Release v${{ steps.bump_version.outputs.BUILD_VERSION }} + # draft: true + # body: | + # ## Changelog + # No changelog available yet. From eba902228145b44da6535af907aef3722bac3e30 Mon Sep 17 00:00:00 2001 From: Diogo Martino Date: Thu, 15 Feb 2024 18:52:18 +0000 Subject: [PATCH 10/15] chore: finished ci script --- .github/workflows/wails-build.yaml | 50 ++++++++++++++---------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/.github/workflows/wails-build.yaml b/.github/workflows/wails-build.yaml index c5e47da..f47e94f 100644 --- a/.github/workflows/wails-build.yaml +++ b/.github/workflows/wails-build.yaml @@ -59,12 +59,12 @@ jobs: jq --arg new_version "$new_version" '.productVersion = $new_version' ./server/server.json > temp_wails.json mv temp_wails.json ./server/server.json - # - name: Commit and Push bump - # run: | - # git add ./app/wails.json - # git add ./server/server.json - # git commit -m "Bump version to v${{ steps.bump_version.outputs.BUILD_VERSION }}" - # git push + - name: Commit and Push bump + run: | + git add ./app/wails.json + git add ./server/server.json + git commit -m "Bump version to v${{ steps.bump_version.outputs.BUILD_VERSION }}" + git push - name: Setup GoLang uses: actions/setup-go@v4 @@ -86,10 +86,10 @@ jobs: run: go build -o PalworldDSGUI_WinServer.exe shell: bash - - name: Build Linux Server - working-directory: ./server - run: GOOS=linux GOARCH=amd64 go build -o PalworldDSGUI_LinuxServer - shell: bash + # - name: Build Linux Server + # working-directory: ./server + # run: GOOS=linux GOARCH=amd64 go build -o PalworldDSGUI_LinuxServer + # shell: bash - name: Upload Artifact uses: actions/upload-artifact@v3 @@ -98,19 +98,17 @@ jobs: path: | ./app/build/bin/PalworldDSGui.exe ./server/PalworldDSGUI_WinServer.exe - ./server/PalworldDSGUI_LinuxServer - - # - name: Create Release - # id: create_release - # uses: softprops/action-gh-release@v1 - # with: - # files: | - # ./app/build/bin/PalworldDSGui.exe - # ./server/PalworldDSGUI_WinServer.exe - # ./server/PalworldDSGUI_LinuxServer - # tag_name: v${{ steps.bump_version.outputs.BUILD_VERSION }} - # name: Release v${{ steps.bump_version.outputs.BUILD_VERSION }} - # draft: true - # body: | - # ## Changelog - # No changelog available yet. + + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v1 + with: + files: | + ./app/build/bin/PalworldDSGui.exe + ./server/PalworldDSGUI_WinServer.exe + tag_name: v${{ steps.bump_version.outputs.BUILD_VERSION }} + name: Release v${{ steps.bump_version.outputs.BUILD_VERSION }} + draft: true + body: | + ## Changelog + No changelog available yet. From 8f8b2b9305dce71889f1996fbe6e4af6d30d9217 Mon Sep 17 00:00:00 2001 From: Diogo Martino Date: Thu, 15 Feb 2024 19:28:03 +0000 Subject: [PATCH 11/15] chore: force save when generating new api key --- server/api.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/api.go b/server/api.go index a103c57..67a3451 100644 --- a/server/api.go +++ b/server/api.go @@ -29,6 +29,7 @@ func PrintApiKey() { func (a *Api) Init() { if !HasApiKey() || utils.Launch.ForceNewKey { GenerateApiKey() + utils.SaveSettings() } if utils.Launch.ShowKey { From 7e640c5fce71504a8a6eeb5aa779cfa4ae07e1f3 Mon Sep 17 00:00:00 2001 From: Diogo Martino Date: Thu, 15 Feb 2024 19:28:30 +0000 Subject: [PATCH 12/15] chore: docs stuff --- HOW_TO_USE.md | 47 +++++++++++++++++++ README.md | 29 ++---------- .../src/screens/initializing/index.tsx | 13 ++++- 3 files changed, 64 insertions(+), 25 deletions(-) create mode 100644 HOW_TO_USE.md diff --git a/HOW_TO_USE.md b/HOW_TO_USE.md new file mode 100644 index 0000000..503de93 --- /dev/null +++ b/HOW_TO_USE.md @@ -0,0 +1,47 @@ +## Quick Start Guide + +1. Download the **GUI Server** `PalworldDSGUI_Server.exe` from the [releases page](https://github.com/diogomartino/palworld-ds-gui/releases). +2. Place the executable in a folder of your choice. +3. Run the executable. +4. Wait until the server is downloaded from the Steam servers. +5. An API key will be shown on the console. Save it, you will need it to connect to the server. +6. Download the **GUI Client** `PalworldDSGui.exe` from the [releases page](https://github.com/diogomartino/palworld-ds-gui/releases). +7. Open the client and use the API key to connect to the server. + +## Remote Server + +If you want to run the server on a remote machine (eg: a VPS), the steps are the same as above, you just need to download and execute the server on the remote machine, and then use the client to connect to it. You may need to open the port **21577** on the remote machine to be able to access it from the client. + +## Using an existing server + +If you already have a Palworld server, you can still use this tool, you just need to make sure that the folder structure is correct, which should look like this: + +```plaintext +SomeRandomFolder/ +├── server/ <------- This is your existing server folder (the one with the **PalServer.exe** executable) +├── PalworldDSGUI_Server.ex +``` + +The `server` folder should contain the `PalServer.exe` executable and all the other files that come with the server. The `PalworldDSGUI_Server.exe` should be in the same folder as the `server` folder. Then you can run the `PalworldDSGUI_Server.exe` and it will detect the existing server. + +## GUI server parameters + + flag.BoolVar(&Launch.ForceNewKey, "newkey", false, "Generate a new API key") + flag.BoolVar(&Launch.ShowKey, "showkey", false, "Show the current API key") + flag.BoolVar(&Launch.Help, "help", false, "Show help") + flag.IntVar(&Launch.Port, "port", 21577, "Port to run the server on") + +| Param | Description | Default | +| -------- | ------------------------- | ------- | +| -newkey | Generate a new API key | +| -showkey | Show the current API key | +| -help | Show help | +| -port | Port to run the server on | 21577 | + +## Running the server on Linux + +Coming soon. + +## Running it in Docker + +Coming soon. diff --git a/README.md b/README.md index a9461dd..ec2e4ab 100644 --- a/README.md +++ b/README.md @@ -4,41 +4,24 @@ This is a GUI for the Palworld Dedicated Server. Configure and manage your serve ## Download -You can download the latest version from the [releases page](https://github.com/diogomartino/palworld-ds-gui/releases). +You can downloads the latest versions from the [releases page](https://github.com/diogomartino/palworld-ds-gui/releases). [WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) is required. You most likely already have it installed, so you don't need to worry. > [!WARNING] > Be aware that this software is still in early development and may contain bugs. Please report any issues you find. +[Quick Start Guide](HOW_TO_USE.md) + ## Screenshots +![Connecting](https://i.imgur.com/e5rSvBE.png) ![Home](https://i.imgur.com/157panY.png) ![Settings](https://i.imgur.com/gu0x0PS.png) ![Admin](https://i.imgur.com/49giAIK.png) ![Backups](https://i.imgur.com/3IboT0o.png) -## Instructions - -1. Open the app -2. Wait for the dedicated server to be downloaded from Steam -3. Configure your server -4. Hit the "Start" button - -The default settings mirror the official configurations, but users have the flexibility to customize them according to their preferences. - -## Development - -### Future plans - -- Add support for Linux and macOS -- Create RCON management interface -- Automated backups (both local and remote) -- Automatic server updates -- Multiple map management -- User profiles management -- Automated imports of local worlds -- Resource usage graphs (CPU, RAM, etc) +## Developmen ### Requirements @@ -51,8 +34,6 @@ The default settings mirror the official configurations, but users have the flex wails dev -s ``` -This will launch the application in development mode. The interface will also run on http://localhost:34115 in case you want to run it in a browser. - ## Building ``` diff --git a/app/frontend/src/screens/initializing/index.tsx b/app/frontend/src/screens/initializing/index.tsx index 05fe33e..a97a404 100644 --- a/app/frontend/src/screens/initializing/index.tsx +++ b/app/frontend/src/screens/initializing/index.tsx @@ -5,6 +5,7 @@ import { IconInfoCircle } from '@tabler/icons-react'; import { useState } from 'react'; import { setServerCredentials } from '../../actions/app'; import useServerCredentials from '../../hooks/use-server-credentials'; +import { DesktopAPI } from '../../desktop'; const Initializing = () => { const serverCredentials = useServerCredentials(); @@ -89,7 +90,17 @@ const Initializing = () => {

- If you need help, please read the quick start guide here. + If you need help, please create an issue{' '} + + DesktopAPI.openUrl( + 'https://github.com/diogomartino/palworld-ds-gui/issues' + ) + } + > + here. +

From d3d81c01b0ce7fcc9b1ed855c942a32cbedcf8ed Mon Sep 17 00:00:00 2001 From: Diogo Martino Date: Thu, 15 Feb 2024 19:29:37 +0000 Subject: [PATCH 13/15] chore: bump major --- app/wails.json | 2 +- server/server.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/wails.json b/app/wails.json index 1704212..6817336 100644 --- a/app/wails.json +++ b/app/wails.json @@ -15,7 +15,7 @@ "info": { "companyName": "DiogoMartino", "productName": "Palworld Dedicated Server GUI", - "productVersion": "0.0.7", + "productVersion": "1.0.0", "copyright": "2024", "comments": "https://github.com/diogomartino/palworld-ds-gui" } diff --git a/server/server.json b/server/server.json index ba07a39..43e690c 100644 --- a/server/server.json +++ b/server/server.json @@ -1,7 +1,7 @@ { "companyName": "DiogoMartino", "productName": "Palworld Dedicated Server GUI Server", - "productVersion": "0.0.7", + "productVersion": "1.0.0", "copyright": "2024", "comments": "https://github.com/diogomartino/palworld-ds-gui" } From 9ce7152a1a46f8e629c844537c861be7da7419b5 Mon Sep 17 00:00:00 2001 From: Diogo Martino Date: Thu, 15 Feb 2024 19:31:52 +0000 Subject: [PATCH 14/15] chore: fail --- HOW_TO_USE.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/HOW_TO_USE.md b/HOW_TO_USE.md index 503de93..ee41bbc 100644 --- a/HOW_TO_USE.md +++ b/HOW_TO_USE.md @@ -26,11 +26,6 @@ The `server` folder should contain the `PalServer.exe` executable and all the ot ## GUI server parameters - flag.BoolVar(&Launch.ForceNewKey, "newkey", false, "Generate a new API key") - flag.BoolVar(&Launch.ShowKey, "showkey", false, "Show the current API key") - flag.BoolVar(&Launch.Help, "help", false, "Show help") - flag.IntVar(&Launch.Port, "port", 21577, "Port to run the server on") - | Param | Description | Default | | -------- | ------------------------- | ------- | | -newkey | Generate a new API key | From 466912eed42fa394aa2626e0474f3b210099bc4e Mon Sep 17 00:00:00 2001 From: Diogo Martino Date: Thu, 15 Feb 2024 19:33:23 +0000 Subject: [PATCH 15/15] chore: docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ec2e4ab..5303431 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ You can downloads the latest versions from the [releases page](https://github.co [WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) is required. You most likely already have it installed, so you don't need to worry. +### [How To Use - Click here](HOW_TO_USE.md) + > [!WARNING] > Be aware that this software is still in early development and may contain bugs. Please report any issues you find. -[Quick Start Guide](HOW_TO_USE.md) - ## Screenshots ![Connecting](https://i.imgur.com/e5rSvBE.png)