From fc0c1c7e7a89e072d6b5d3da813132c73cbb586b Mon Sep 17 00:00:00 2001 From: zontreck Date: Thu, 23 May 2024 22:18:59 -0700 Subject: [PATCH] Got more things functional --- bin/server.dart | 7 + lib/game.dart | 58 +------ lib/main.dart | 26 +-- lib/packets/ClientPackets.dart | 235 +++++++++++++++++++++++++- lib/pages/GameServerPage.dart | 173 +++++++------------- lib/pages/ModManager.dart | 264 +++++++++++++++--------------- lib/pages/Proton.dart | 46 ------ lib/pages/ServerSettings.dart | 196 +++++++++++----------- lib/pages/autorestart.dart | 192 +++++++++++----------- lib/pages/credentials_prompt.dart | 97 +++++++++++ lib/pages/dialogbox.dart | 4 +- lib/pages/home.dart | 49 ++++-- lib/pages/steamcmd.dart | 220 ------------------------- lib/proton.dart | 9 +- lib/statemachine.dart | 146 +++++++++++++++-- lib/structs/SessionData.dart | 18 ++ lib/structs/autorestarts.dart | 24 ++- lib/structs/credentials.dart | 20 +-- lib/structs/mod.dart | 30 +++- lib/structs/settings.dart | 93 +++++++++-- lib/structs/settingsEntry.dart | 34 ++-- pubspec.yaml | 2 +- test/rcon_command.dart | 13 ++ 23 files changed, 1080 insertions(+), 876 deletions(-) delete mode 100644 lib/pages/Proton.dart create mode 100644 lib/pages/credentials_prompt.dart delete mode 100644 lib/pages/steamcmd.dart create mode 100644 lib/structs/SessionData.dart create mode 100644 test/rcon_command.dart diff --git a/bin/server.dart b/bin/server.dart index dbd8c75..662eec5 100644 --- a/bin/server.dart +++ b/bin/server.dart @@ -35,10 +35,15 @@ void main() async { print("Initializing SteamCMD"); await settings.initializeSteamCmd(); + await settings.initializeSteamCmd2FA(); print("Initialized Steamcmd and Proton"); print("Checking for game server updates..."); await settings.RunUpdate(); + await settings.createModFolderIfNotExists(); + await settings.createServerModFolderIfNotExists(); + await settings.createModJailFolderIfNotExists(); + await settings.writeOutModListFile(); print("Finished checking for game server updates..."); if (settings.FTS) { @@ -50,6 +55,8 @@ void main() async { print("Scanning mods..."); settings.inst!.mods = await doScanMods(); + + await settings.subsys.startScheduler(); } print("Starting up server manager server wrapper"); diff --git a/lib/game.dart b/lib/game.dart index f99d79d..d739929 100644 --- a/lib/game.dart +++ b/lib/game.dart @@ -8,17 +8,6 @@ import 'package:servermanager/structs/settings.dart'; Future doDownloadMods(bool jail) async { Settings settings = Settings(); - if (!settings.inst!.downloadMods) return; - - // Now, invoke SteamCmd to download the workshop mods. This is an authenticated action, and does require Scmd2fa - String code = ""; - - if (settings.inst!.steam_creds!.has_2fa) { - var result = await Process.run(settings.getSteamCmd2FA(), - ["--raw", "--secret", settings.inst!.steam_creds!.secret]); - code = result.stdout as String; - } - // Build download command List manifest = [ "+@sSteamCmdForcePlatformType", @@ -26,9 +15,7 @@ Future doDownloadMods(bool jail) async { "+force_install_dir", jail ? settings.getModJailPath() : settings.getModPath(), "+login", - settings.inst!.steam_creds!.username, - settings.inst!.steam_creds!.password, - if (settings.inst!.steam_creds!.has_2fa) code.trim() + "anonymous", ]; for (Mod M in settings.inst!.mods) { manifest.add("+workshop_download_item"); @@ -37,6 +24,7 @@ Future doDownloadMods(bool jail) async { } await settings.createModFolderIfNotExists(); + await settings.createModJailFolderIfNotExists(); manifest.add("+quit"); @@ -48,44 +36,6 @@ Future doDownloadMods(bool jail) async { print(result.stdout); } -Future doMigrateMods() async { - // Migrate the mods from the configured SteamLibrary path to the mods folder - Settings settings = Settings(); - await settings.createModFolderIfNotExists(); - - if (settings.inst!.downloadMods) return; - - // Copy the mods to their destination - for (Mod M in settings.inst!.mods) { - var ph = PathHelper.builder(settings.game_path) - .resolve("mods") - .resolve("steamapps") - .resolve("workshop") - .resolve("content") - .resolve("440900") - .resolve("${M.mod_id}") - .removeDir() - .mkdir(); - - var ph2 = PathHelper.builder(settings.inst!.conanExilesLibraryPath) - .resolve("steamapps") - .resolve("workshop") - .resolve("content") - .resolve("440900") - .resolve("${M.mod_id}"); - - Directory dir = new Directory(ph2.build()); - await for (var f in dir.list()) { - if (f is File && f.path.endsWith("pak")) { - String modDest = - ph.resolve(f.path.split(Platform.pathSeparator).last).build(); - - await f.copy(modDest); - } - } - } -} - Future> doScanMods() async { Settings settings = Settings(); @@ -114,8 +64,8 @@ Future> doScanMods() async { M.mod_pak = name; M.mod_hash = hash; - print("Discovered mod file: ${name}"); - print("Hash: ${hash}"); + print("Discovered mod file: $name"); + print("Hash: $hash"); // Update the mod instance, and retain the original modlist order ret.add(Mod( diff --git a/lib/main.dart b/lib/main.dart index 50a8068..620dcf2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,17 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:libac_flutter/nbt/NbtUtils.dart'; import 'package:libac_flutter/packets/packets.dart'; -import 'package:libac_flutter/utils/uuid/UUID.dart'; import 'package:servermanager/packets/ClientPackets.dart'; import 'package:servermanager/pages/Constants.dart'; import 'package:servermanager/pages/GameServerPage.dart'; import 'package:servermanager/pages/ModManager.dart'; import 'package:servermanager/pages/autorestart.dart'; +import 'package:servermanager/pages/credentials_prompt.dart'; import 'package:servermanager/pages/home.dart'; -import 'package:servermanager/pages/steamcmd.dart'; import 'package:servermanager/structs/settings.dart'; -import 'pages/Proton.dart'; import 'pages/ServerSettings.dart'; Future main() async { @@ -30,16 +27,12 @@ class MyApp extends StatelessWidget { routes: { "/": (context) => ServerPage(), "/home": (context) => HomePage(settings: appSettings), - "/proton": (context) => Proton(settings: appSettings), - "/steamcmd": (context) => SteamCMD( - settings: appSettings, - ), + "/creds": (context) => CredentialsPage(), "/server": (context) => GameServerPage(settings: appSettings), "/server/autorestart": (context) => AutoRestartPage(), "/server/ports": (context) => ServerSettingsPage(), "/server/mods": (context) => ModManager(settings: appSettings), "/server/mods/edit": (context) => ModPage(), - "/steamcmd/creds": (context) => CredentialsPrompt() }); } } @@ -68,10 +61,17 @@ class ServerPage extends StatelessWidget { login.password = password.text; S2CResponse response = await settings.client!.send(login); - bool valid = NbtUtils.readBoolean(response.contents, "valid"); - if (valid) { - settings.remoteLoginToken = - UUID.parse(response.contents.get("token")!.asString()); + S2CLoginReply loginResponse = S2CLoginReply(); + loginResponse.decodeTag(response.contents); + loginResponse.handleClientPacket(); + + if (loginResponse.valid) { + S2CResponse settingsData = + await settings.client!.send(C2SRequestSettingsPacket()); + C2SRequestSettingsPacket settingsBack = + C2SRequestSettingsPacket(); + settingsBack.decodeTag(settingsData.contents); + settingsBack.handleClientPacket(); Navigator.pushNamed(context, "/home"); } else { diff --git a/lib/packets/ClientPackets.dart b/lib/packets/ClientPackets.dart index ce6d1a5..06cb93e 100644 --- a/lib/packets/ClientPackets.dart +++ b/lib/packets/ClientPackets.dart @@ -1,12 +1,16 @@ import 'dart:convert'; import 'package:libac_flutter/nbt/NbtUtils.dart'; +import 'package:libac_flutter/nbt/Stream.dart'; import 'package:libac_flutter/nbt/Tag.dart'; import 'package:libac_flutter/nbt/impl/CompoundTag.dart'; import 'package:libac_flutter/nbt/impl/StringTag.dart'; import 'package:libac_flutter/packets/packets.dart'; +import 'package:libac_flutter/utils/Hashing.dart'; import 'package:libac_flutter/utils/uuid/NbtUUID.dart'; import 'package:libac_flutter/utils/uuid/UUID.dart'; +import 'package:servermanager/statemachine.dart'; +import 'package:servermanager/structs/SessionData.dart'; import 'package:servermanager/structs/settings.dart'; class ClientPackets { @@ -15,6 +19,15 @@ class ClientPackets { reg.register(C2SLoginPacket(), () { return C2SLoginPacket(); }); + reg.register(S2CLoginReply(), () { + return S2CLoginReply(); + }); + reg.register(C2SRequestSettingsPacket(), () { + return C2SRequestSettingsPacket(); + }); + reg.register(C2SUploadSettingsPacket(), () { + return C2SUploadSettingsPacket(); + }); } } @@ -48,7 +61,7 @@ class C2SLoginPacket implements IPacket { Tag encodeTag() { CompoundTag tag = CompoundTag(); tag.put("username", StringTag.valueOf(username)); - tag.put("password", StringTag.valueOf(password)); + tag.put("password", StringTag.valueOf(Hashing.sha256Hash(password))); return tag; } @@ -67,27 +80,233 @@ class C2SLoginPacket implements IPacket { @override Future handleServerPacket() async { S2CResponse response = S2CResponse(); + S2CLoginReply loginReply = S2CLoginReply(); // Attempt to log in. Settings settings = Settings(); if (settings.serverLoginCreds.username == username && - settings.serverLoginCreds.password == password) { - NbtUtils.writeBoolean(response.contents, "valid", true); + Hashing.sha256Hash(settings.serverLoginCreds.password) == password) { settings.remoteLoginToken = UUID.generate(4); - NbtUtils.writeUUID(response.contents, "token", - NbtUUID.fromUUID(settings.remoteLoginToken)); + loginReply.valid = true; + loginReply.token = settings.remoteLoginToken; } else { - NbtUtils.writeBoolean(response.contents, "valid", false); + //print( + // "Login failure\n${settings.serverLoginCreds.username}:${username}\n${Hashing.sha256Hash(settings.serverLoginCreds.password)}:${password}"); + loginReply.valid = false; } + response.contents = loginReply.encodeTag().asCompoundTag(); + return PacketResponse(replyDataTag: response.encodeTag().asCompoundTag()); + } + + @override + Map toJson() { + return {"username": username, "password": Hashing.sha256Hash(password)}; + } + + @override + Future handleClientPacket() async {} +} + +class S2CLoginReply implements IPacket { + bool valid = false; + UUID token = UUID.ZERO; + + @override + void decodeJson(String params) { + fromJson(json.decode(params)); + } + + @override + void decodeTag(Tag tag) { + print("Decoding S2C LoginReply"); + StringBuilder sb = StringBuilder(); + Tag.writeStringifiedNamedTag(tag, sb, 0); + + print(sb); + CompoundTag ct = tag as CompoundTag; + if (ct.containsKey("valid")) valid = NbtUtils.readBoolean(ct, "valid"); + if (ct.containsKey("token")) { + token = NbtUtils.readUUID(ct, "token").toUUID(); + } + } + + @override + NetworkDirection direction() { + return NetworkDirection.ServerToClient; + } + + @override + String encodeJson() { + return json.encode(toJson()); + } + + @override + Tag encodeTag() { + CompoundTag tag = CompoundTag(); + NbtUtils.writeBoolean(tag, "valid", valid); + NbtUtils.writeUUID(tag, "token", NbtUUID.fromUUID(token)); + return tag; + } + + @override + void fromJson(Map js) { + valid = js['valid'] as bool; + token = UUID.parse(js['token'] as String); + } + + @override + String getChannelID() { + return "LoginReply"; + } + + @override + Future handleClientPacket() async { + // Handle login finalization related stuff + Settings settings = Settings(); + settings.remoteLoginToken = token; + } + + @override + Future handleServerPacket() async { + return PacketResponse.nil; // We only operate on the client + } + + @override + Map toJson() { + return {"valid": valid, "token": token.toString()}; + } +} + +class C2SRequestSettingsPacket implements IPacket { + CompoundTag serverSettings = CompoundTag(); + + @override + void decodeJson(String params) { + throw UnsupportedError("Json is unsupported by LibACNBT at this time"); + } + + @override + void decodeTag(Tag tag) { + CompoundTag ct = tag as CompoundTag; + serverSettings = ct.get("settings")!.asCompoundTag(); + } + + @override + NetworkDirection direction() { + return NetworkDirection.ClientToServer; + } + + @override + String encodeJson() { + return ""; + } + + @override + Tag encodeTag() { + CompoundTag ct = CompoundTag(); + ct.put("settings", Settings().serialize()); + + return ct; + } + + @override + void fromJson(Map js) {} + + @override + String getChannelID() { + return "C2SRequestSettings"; + } + + @override + Future handleClientPacket() async { + Settings settings = Settings(); + settings.deserialize(serverSettings); + settings.server = false; + } + + @override + Future handleServerPacket() async { + S2CResponse response = S2CResponse(); + Settings settings = Settings(); + + serverSettings = settings.serialize(); + response.contents = encodeTag().asCompoundTag(); + return PacketResponse(replyDataTag: response.encodeTag().asCompoundTag()); } @override Map toJson() { - return {"username": username, "password": password}; + return {}; + } +} + +class C2SUploadSettingsPacket implements IPacket { + CompoundTag srvSettings = CompoundTag(); + + @override + void decodeJson(String params) {} + + @override + void decodeTag(Tag tag) { + srvSettings = tag.asCompoundTag().get("settings")!.asCompoundTag(); } @override - Future handleClientPacket() async {} + NetworkDirection direction() { + return NetworkDirection.ClientToServer; + } + + @override + String encodeJson() { + return ""; + } + + @override + Tag encodeTag() { + CompoundTag tag = CompoundTag(); + tag.put("settings", Settings().serialize()); + + return tag; + } + + @override + void fromJson(Map js) {} + + @override + String getChannelID() { + return "C2SUploadSettings"; + } + + @override + Future handleClientPacket() async { + // No client response or handling needed + } + + @override + Future handleServerPacket() async { + Settings settings = Settings(); + settings.deserialize(srvSettings); + settings.Write(); + + // Check if server is running, if not, stop immediately + // If server is running, schedule restart for 1 minute and send a alert to all players, then perform stop or restart depending on if running in Pterodactyl Compatibility mode + SessionData.shutdownMessage = "Server wrapper updated. Restart required."; + SessionData.timer.apply(60); + SessionData.CURRENT_INTERVAL = WarnIntervals.NONE; + + if (settings.subsys.currentState == States.Inactive) { + SessionData.shutdownPending = true; + // Stop packet server + PacketServer.socket!.close(); + } + + return PacketResponse.nil; + } + + @override + Map toJson() { + return {}; + } } diff --git a/lib/pages/GameServerPage.dart b/lib/pages/GameServerPage.dart index 795d80c..8153c9f 100644 --- a/lib/pages/GameServerPage.dart +++ b/lib/pages/GameServerPage.dart @@ -1,7 +1,5 @@ -import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; -import '../statemachine.dart'; import '../structs/autorestarts.dart'; import '../structs/serversettings.dart'; import '../structs/settings.dart'; @@ -28,120 +26,73 @@ class GameServerPageState extends State { title: Text("Conan Exiles Server Manager - Game Server"), backgroundColor: Color.fromARGB(255, 100, 0, 0), ), - body: WillPopScope( - onWillPop: () async { - if (downloading) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Wait until the download completes"))); + body: SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Column( + children: [ + ListTile( + title: Text("Mods"), + leading: Icon(Icons.build), + subtitle: Text("Server Mod Management"), + onTap: () { + if (downloading) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("Wait until the download completes"))); - return false; - } else { - return true; - } - }, - child: SingleChildScrollView( - padding: EdgeInsets.all(16), - child: Column( - children: [ - ListTile( - title: Text("Mods"), - leading: Icon(Icons.build), - subtitle: Text("Server Mod Management"), - onTap: () { - if (downloading) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text("Wait until the download completes"))); + return; + } + Navigator.pushNamed(context, "/server/mods"); + }, + ), + ListTile( + title: Text("Configure AutoRestart"), + leading: Icon(Icons.timer), + onTap: () async { + if (downloading) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("Wait until the download completes"))); - return; - } - Navigator.pushNamed(context, "/server/mods"); - }, - ), - if (!settings.inst!.downloadMods) - ListTile( - title: Text("Conan Exiles Install Path"), - subtitle: - Text("Set the Steam Library location of Conan Exiles"), - leading: Icon(Icons.folder), - onTap: () async { - // Open the folder select prompt - var path = await getDirectoryPath(); - setState(() { - settings.inst!.conanExilesLibraryPath = - path ?? settings.inst!.conanExilesLibraryPath; + return; + } + var reply = await Navigator.pushNamed( + context, "/server/autorestart", + arguments: settings.inst!.timer); - if (path == null) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - "You must select a valid SteamLibrary folder"))); - } - }); - }, - ), - ListTile( - title: Text("Configure AutoRestart"), - leading: Icon(Icons.timer), - onTap: () async { - if (downloading) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text("Wait until the download completes"))); + if (reply == null) return; // No change was made - return; - } - var reply = await Navigator.pushNamed( - context, "/server/autorestart", - arguments: settings.inst!.timer); + setState(() { + settings.inst!.timer = reply as AutomaticRestartInfo; + }); + }, + ), + ListTile( + title: Text("Configure Server Ports"), + leading: Icon(Icons.numbers), + onTap: () async { + var reply = await Navigator.pushNamed( + context, "/server/ports", + arguments: settings.inst!.serverSettings); - setState(() { - settings.inst!.timer = reply as AutomaticRestartInfo; - }); - }, - ), - SwitchListTile( - value: settings.inst!.downloadMods, - onChanged: (value) { - setState(() { - settings.inst!.downloadMods = value; - }); - }, - title: Text("Automatic Download of Mods"), - subtitle: Text( - "If enabled, downloads mods using steamcmd, if disabled, you must configure the SteamLibrary folder location"), - ), - ListTile( - title: Text("Configure Server Ports"), - leading: Icon(Icons.numbers), - onTap: () async { - var reply = await Navigator.pushNamed( - context, "/server/ports", - arguments: settings.inst!.serverSettings); - setState(() { - settings.inst!.serverSettings = reply as ServerSettings; - settings.Write(); - }); - }, - ), - ListTile( - title: Text("Status:"), - subtitle: Text(settings.subsys.currentState != States.Inactive - ? "Active" - : "Not Running"), - leading: Icon(settings.subsys.currentState != States.Inactive - ? Icons.play_arrow - : Icons.error), - onTap: () async { - // Toggle state - setState(() { - if (settings.subsys.currentState == States.Inactive) { - settings.subsys.changeState(States.Starting); - } else - settings.subsys.changeState(States.FullStop); - }); - }, - ), - ], - )), - ), + if (reply != null) return; // If null, no change. + + setState(() { + settings.inst!.serverSettings = reply as ServerSettings; + }); + }, + ), + SwitchListTile( + value: settings.inst!.pterodactylMode, + onChanged: (B) { + setState(() { + settings.inst!.pterodactylMode = B; + }); + }, + title: Text("Pterodactyl Mode"), + subtitle: Text( + "This mode changes restart behavior. Instead of restarting the process and keeping the wrapper running, the wrapper will stop once the server stops, allowing for Pterodactyl to reset the container process and update the uptime."), + ) + ], + )), ); } } diff --git a/lib/pages/ModManager.dart b/lib/pages/ModManager.dart index e645175..8bed01d 100644 --- a/lib/pages/ModManager.dart +++ b/lib/pages/ModManager.dart @@ -22,69 +22,64 @@ class ModManagerState extends State { title: Text("Conan Exiles Server Manager - Mod Manager"), backgroundColor: Color.fromARGB(255, 100, 0, 0), ), - body: WillPopScope( - child: ReorderableListView.builder( - onReorder: (oldIndex, newIndex) { - if (oldIndex < newIndex) { - // From top to Bottom - int end = newIndex - 1; - Mod item = settings.inst!.mods[oldIndex]; - int i = 0; - int local = oldIndex; - do { - settings.inst!.mods[local] = settings.inst!.mods[++local]; - i++; - } while (i < end - oldIndex); - settings.inst!.mods[end] = item; - } else if (oldIndex > newIndex) { - //From bottom to top - Mod item = settings.inst!.mods[oldIndex]; - for (int i = oldIndex; i > newIndex; i--) { - settings.inst!.mods[i] = settings.inst!.mods[i - 1]; - } - settings.inst!.mods[newIndex] = item; + body: ReorderableListView.builder( + onReorder: (oldIndex, newIndex) { + if (oldIndex < newIndex) { + // From top to Bottom + int end = newIndex - 1; + Mod item = settings.inst!.mods[oldIndex]; + int i = 0; + int local = oldIndex; + do { + settings.inst!.mods[local] = settings.inst!.mods[++local]; + i++; + } while (i < end - oldIndex); + settings.inst!.mods[end] = item; + } else if (oldIndex > newIndex) { + //From bottom to top + Mod item = settings.inst!.mods[oldIndex]; + for (int i = oldIndex; i > newIndex; i--) { + settings.inst!.mods[i] = settings.inst!.mods[i - 1]; } - setState(() { - settings.Write(); - }); - }, - itemBuilder: (ctx, idx) { - Mod mod = settings.inst!.mods[idx]; - return Padding( - key: Key(mod.mod_instance_id()), - padding: EdgeInsets.all(12), - child: ListTile( - title: Text(mod.mod_name), - subtitle: Text("ID: ${mod.mod_id}"), - onTap: () async { - final reply = await Navigator.pushNamed( - context, "/server/mods/edit", - arguments: Mod( - mod_id: mod.mod_id, - mod_name: mod.mod_name, - mod_pak: mod.mod_pak, - mod_hash: mod.mod_hash, - newMod: false)); + settings.inst!.mods[newIndex] = item; + } + setState(() { + settings.Write(); + }); + }, + itemBuilder: (ctx, idx) { + Mod mod = settings.inst!.mods[idx]; + return Padding( + key: Key(mod.mod_instance_id()), + padding: EdgeInsets.all(12), + child: ListTile( + title: Text(mod.mod_name), + subtitle: Text("ID: ${mod.mod_id}\nLoad Order: ${idx}"), + onTap: () async { + final reply = await Navigator.pushNamed( + context, "/server/mods/edit", + arguments: Mod( + mod_id: mod.mod_id, + mod_name: mod.mod_name, + mod_pak: mod.mod_pak, + mod_hash: mod.mod_hash, + newMod: false)); - if (reply != null) { - setState(() { - settings.inst!.mods[idx] = reply as Mod; - }); - } else { + if (reply != null) { + if (reply is bool) { setState(() { settings.inst!.mods.removeAt(idx); }); } - }, - ), - ); - }, - itemCount: settings.inst!.mods.length, - ), - onWillPop: () async { - Navigator.pop(context); - return true; + setState(() { + settings.inst!.mods[idx] = reply as Mod; + }); + } + }, + ), + ); }, + itemCount: settings.inst!.mods.length, ), floatingActionButton: ElevatedButton( child: Icon(Icons.add), @@ -93,11 +88,10 @@ class ModManagerState extends State { final reply = await Navigator.pushNamed(context, "/server/mods/edit", arguments: Mod(newMod: true)); - if (reply != null) { + if (reply != null && reply is! bool) { Mod mod = reply as Mod; setState(() { settings.inst!.mods.add(mod); - settings.Write(); }); } }, @@ -137,96 +131,94 @@ class ModPage extends StatelessWidget { title: Text("Mod Editor"), backgroundColor: Color.fromARGB(255, 100, 0, 0), ), - body: WillPopScope( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column(children: [ - Row( - children: [ - SizedBox( - width: 150, - child: ListTile( - leading: Icon(Icons.abc_rounded), - title: Text("Mod Name"), - )), - Expanded( - child: TextField( - controller: name, - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(4)))), - ) - ], - ), - SizedBox( - height: 16, - ), - Row( - children: [ - SizedBox( - width: 150, - child: ListTile( - leading: Icon(Icons.perm_identity), - title: Text("Mod ID")), - ), - Expanded( - child: TextField( - controller: id, - keyboardType: TextInputType.number, - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(4))), - )) - ], - ), - SizedBox( - height: 16, - ), - ListTile( - title: Text("Mod Instance ID"), - subtitle: Text(instance), - ), - ListTile( - title: Text("Mod Pak File: $pak"), - subtitle: - Text("Mod pak file name as detected during downloading"), - ), - ListTile( - title: Text("Mod Hash"), - subtitle: Text("$hash"), - ), - if (!isNewMod) - ElevatedButton( - onPressed: () { - willDelete = true; - Navigator.pop(context); - }, - child: Row( - children: [ - Icon(Icons.delete), - SizedBox( - width: 4, - ), - Text("Remove Mod") - ], - )) - ]), - ), - onWillPop: () async { + floatingActionButton: ElevatedButton( + child: Text("Save"), + onPressed: () { int idVal = 0; try { idVal = int.parse(id.text); } catch (E) {} if (willDelete) { - Navigator.pop(context, null); + Navigator.pop(context, true); } else { Navigator.pop(context, Mod(mod_id: idVal, mod_name: name.text, newMod: false)); } - return true; }, ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column(children: [ + Row( + children: [ + SizedBox( + width: 150, + child: ListTile( + leading: Icon(Icons.abc_rounded), + title: Text("Mod Name"), + )), + Expanded( + child: TextField( + controller: name, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4)))), + ) + ], + ), + SizedBox( + height: 16, + ), + Row( + children: [ + SizedBox( + width: 150, + child: ListTile( + leading: Icon(Icons.perm_identity), title: Text("Mod ID")), + ), + Expanded( + child: TextField( + controller: id, + keyboardType: TextInputType.number, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4))), + )) + ], + ), + SizedBox( + height: 16, + ), + ListTile( + title: Text("Mod Instance ID"), + subtitle: Text(instance), + ), + ListTile( + title: Text("Mod Pak File: $pak"), + subtitle: Text("Mod pak file name as detected during downloading"), + ), + ListTile( + title: Text("Mod Hash"), + subtitle: Text(hash), + ), + if (!isNewMod) + ElevatedButton( + onPressed: () { + willDelete = true; + Navigator.pop(context); + }, + child: Row( + children: [ + Icon(Icons.delete), + SizedBox( + width: 4, + ), + Text("Remove Mod") + ], + )) + ]), + ), ); } } diff --git a/lib/pages/Proton.dart b/lib/pages/Proton.dart deleted file mode 100644 index f105297..0000000 --- a/lib/pages/Proton.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -import '../proton.dart'; -import '../structs/settings.dart'; - -class Proton extends StatefulWidget { - Settings settings; - Proton({super.key, required this.settings}); - - @override - ProtonState createState() => ProtonState(settings: settings); -} - -class ProtonState extends State { - Settings settings; - ProtonState({required this.settings}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("Proton Manager"), - backgroundColor: Color.fromARGB(255, 100, 0, 0), - ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - ListTile( - title: Text("Init Prefix"), - subtitle: Text("Resets proton prefix"), - leading: Icon(CupertinoIcons.folder_fill), - onTap: () { - runProton( - "echo", ["hello"]); // Change to "cmd" to execute 'cmd' - }, - ) - ], - ), - ), - ), - ); - } -} diff --git a/lib/pages/ServerSettings.dart b/lib/pages/ServerSettings.dart index de60f6a..efa70f2 100644 --- a/lib/pages/ServerSettings.dart +++ b/lib/pages/ServerSettings.dart @@ -34,105 +34,109 @@ class ServerSettingsState extends State { title: Text("Server Settings"), backgroundColor: Color.fromARGB(255, 100, 0, 0), ), - body: PopScope( - onPopInvoked: (v) async { - Navigator.pop( - context, - ServerSettings( - RconPassword: passwordController.text, - RconPort: rconPort, - GamePort: gPort, - QueryPort: qPort)); - }, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: SingleChildScrollView( - child: Column( - children: [ - Row( - children: [ - SizedBox( - width: 256, - child: ListTile(title: Text("Rcon Password")), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: Column( + children: [ + Row( + children: [ + SizedBox( + width: 256, + child: ListTile(title: Text("Rcon Password")), + ), + Expanded( + child: TextField( + controller: passwordController, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8))), + )) + ], + ), + Row( + children: [ + SizedBox( + width: 256, + child: ListTile( + title: Text("Rcon Port"), + subtitle: Text("$rconPort"), ), - Expanded( - child: TextField( - controller: passwordController, - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8))), - )) - ], - ), - Row( - children: [ - SizedBox( - width: 256, - child: ListTile( - title: Text("Rcon Port"), - subtitle: Text("${rconPort}"), - ), + ), + Expanded( + child: Slider( + value: rconPort.toDouble(), + onChanged: (value) { + setState(() { + rconPort = value.toInt(); + }); + }, + min: 5000, + max: 8000, + )) + ], + ), + Row( + children: [ + SizedBox( + width: 256, + child: ListTile( + title: Text("Game Port"), + subtitle: Text("$gPort"), ), - Expanded( - child: Slider( - value: rconPort.toDouble(), - onChanged: (value) { - setState(() { - rconPort = value.toInt(); - }); + ), + Expanded( + child: Slider( + value: gPort.toDouble(), + onChanged: (value) { + setState(() { + gPort = value.toInt(); + }); + }, + min: 5000, + max: 8000, + )) + ], + ), + Row( + children: [ + SizedBox( + width: 256, + child: ListTile( + title: Text("Query Port"), + subtitle: Text("$qPort"), + ), + ), + Expanded( + child: Slider( + value: qPort.toDouble(), + onChanged: (value) { + setState(() { + qPort = value.toInt(); + }); + }, + min: 5000, + max: 8000, + )) + ], + ), + Row( + children: [ + ElevatedButton( + onPressed: () { + Navigator.pop( + context, + ServerSettings( + RconPassword: passwordController.text, + RconPort: rconPort, + GamePort: gPort, + QueryPort: qPort)); }, - min: 5000, - max: 8000, - )) - ], - ), - Row( - children: [ - SizedBox( - width: 256, - child: ListTile( - title: Text("Game Port"), - subtitle: Text("${gPort}"), - ), - ), - Expanded( - child: Slider( - value: gPort.toDouble(), - onChanged: (value) { - setState(() { - gPort = value.toInt(); - }); - }, - min: 5000, - max: 8000, - )) - ], - ), - Row( - children: [ - SizedBox( - width: 256, - child: ListTile( - title: Text("Query Port"), - subtitle: Text("${qPort}"), - ), - ), - Expanded( - child: Slider( - value: qPort.toDouble(), - onChanged: (value) { - setState(() { - qPort = value.toInt(); - }); - }, - min: 5000, - max: 8000, - )) - ], - ) - ], - )), - ), + child: Text("Submit")) + ], + ) + ], + )), ), ); } diff --git a/lib/pages/autorestart.dart b/lib/pages/autorestart.dart index a24ceb2..c82325c 100644 --- a/lib/pages/autorestart.dart +++ b/lib/pages/autorestart.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:libac_flutter/utils/TimeUtils.dart'; import 'package:servermanager/structs/autorestarts.dart'; import 'package:servermanager/structs/settings.dart'; @@ -14,9 +15,7 @@ class AutoRestartState extends State { bool firstDisplay = true; bool enabled = false; - int seconds = 0; - int minutes = 0; - int hours = 0; + Time time = Time(hours: 0, minutes: 0, seconds: 0); @override Widget build(BuildContext context) { @@ -24,9 +23,7 @@ class AutoRestartState extends State { var args = ModalRoute.of(context)!.settings.arguments as AutomaticRestartInfo; enabled = args.enabled; - seconds = args.seconds; - minutes = args.minutes; - hours = args.hours; + time = args.time.copy(); firstDisplay = false; } @@ -35,101 +32,100 @@ class AutoRestartState extends State { title: Text("Automatic Restart"), backgroundColor: Color.fromARGB(255, 100, 0, 0), ), - body: PopScope( - onPopInvoked: (v) async { - Navigator.pop( - context, - AutomaticRestartInfo( - enabled: enabled, - hours: hours, - minutes: minutes, - seconds: seconds)); - }, - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column(children: [ - SwitchListTile( - value: enabled, - onChanged: (val) { - setState(() { - enabled = !enabled; - }); - }, - title: Text("Enabled"), - ), - Row( - children: [ - SizedBox( - width: 256, - child: ListTile( - title: Text("Hours"), - subtitle: Text("${hours}"), - ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column(children: [ + SwitchListTile( + value: enabled, + onChanged: (val) { + setState(() { + enabled = !enabled; + }); + }, + title: Text("Enabled"), + ), + Row( + children: [ + SizedBox( + width: 256, + child: ListTile( + title: Text("Hours"), + subtitle: Text("${time.hours}"), ), - Expanded( - child: Slider( - max: 24, - min: 0, - value: hours.toDouble(), - onChanged: (value) { - setState(() { - hours = value.toInt(); - }); - }, - ), - ) - ], - ), - Row( - children: [ - SizedBox( - width: 256, - child: ListTile( - title: Text("Minutes"), - subtitle: Text("${minutes}"), - ), + ), + Expanded( + child: Slider( + max: 24, + min: 0, + value: time.hours.toDouble(), + onChanged: (value) { + setState(() { + time.hours = value.toInt(); + }); + }, ), - Expanded( - child: Slider( - max: 60, - min: 0, - value: minutes.toDouble(), - onChanged: (value) { - setState(() { - minutes = value.toInt(); - }); - }, - ), - ) - ], - ), - Row( - children: [ - SizedBox( - width: 256, - child: ListTile( - title: Text("Seconds"), - subtitle: Text("${seconds}"), - ), + ) + ], + ), + Row( + children: [ + SizedBox( + width: 256, + child: ListTile( + title: Text("Minutes"), + subtitle: Text("${time.minutes}"), ), - Expanded( - child: Slider( - max: 60, - min: 0, - value: seconds.toDouble(), - onChanged: (value) { - setState(() { - seconds = value.toInt(); - }); - }, - ), - ) - ], - ) - ]), - )), - ), + ), + Expanded( + child: Slider( + max: 60, + min: 0, + value: time.minutes.toDouble(), + onChanged: (value) { + setState(() { + time.minutes = value.toInt(); + }); + }, + ), + ) + ], + ), + Row( + children: [ + SizedBox( + width: 256, + child: ListTile( + title: Text("Seconds"), + subtitle: Text("${time.seconds}"), + ), + ), + Expanded( + child: Slider( + max: 60, + min: 0, + value: time.seconds.toDouble(), + onChanged: (value) { + setState(() { + time.seconds = value.toInt(); + }); + }, + ), + ) + ], + ), + Row( + children: [ + ElevatedButton( + onPressed: () { + Navigator.pop(context, + AutomaticRestartInfo(enabled: enabled, time: time)); + }, + child: Text("Submit")) + ], + ) + ]), + )), ); } } diff --git a/lib/pages/credentials_prompt.dart b/lib/pages/credentials_prompt.dart new file mode 100644 index 0000000..b27cc61 --- /dev/null +++ b/lib/pages/credentials_prompt.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:servermanager/structs/credentials.dart'; + +class CredentialsPage extends StatefulWidget { + @override + CredentialsPrompt createState() => CredentialsPrompt(); +} + +// Returns a Credentials Object +class CredentialsPrompt extends State { + TextEditingController username = TextEditingController(); + TextEditingController password = TextEditingController(); + bool initialInitDone = false; + + @override + void initState() { + final args = ModalRoute.of(context)!.settings.arguments as Credentials?; + + if (args != null) { + if (!initialInitDone) { + username.text = args.username; + password.text = args.password; + initialInitDone = true; + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Conan Exiles Server Manager - Credentials"), + backgroundColor: Color.fromARGB(255, 100, 0, 0), + ), + floatingActionButton: ElevatedButton( + child: Text("Save"), + onPressed: () { + Navigator.pop( + context, + Credentials( + username: username.text, + password: password.text, + )); + }, + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Column( + children: [ + Row( + children: [ + SizedBox( + width: 150, + child: Row( + children: [ + Icon(Icons.person), + Text("Username:"), + ], + )), + Expanded( + child: TextField( + controller: username, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4))), + ), + ) + ], + ), + SizedBox( + height: 16, + ), + Row( + children: [ + SizedBox( + width: 150, + child: Row( + children: [ + Icon(Icons.key), + Text("Password:"), + ], + )), + Expanded( + child: TextField( + controller: password, + keyboardType: TextInputType.visiblePassword, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4))), + ), + ) + ], + ), + ], + ))); + } +} diff --git a/lib/pages/dialogbox.dart b/lib/pages/dialogbox.dart index 7c54353..7c8142a 100644 --- a/lib/pages/dialogbox.dart +++ b/lib/pages/dialogbox.dart @@ -44,7 +44,7 @@ class InputBox extends StatelessWidget { Navigator.of(context).pop(); }, style: ButtonStyle( - backgroundColor: MaterialStateColor.resolveWith( + backgroundColor: WidgetStateColor.resolveWith( (states) => const Color.fromARGB(255, 0, 83, 3))), child: hasInputField ? Text("Submit") : Text("OK"), ), @@ -54,7 +54,7 @@ class InputBox extends StatelessWidget { Navigator.of(context).pop(); }, style: ButtonStyle( - backgroundColor: MaterialStateColor.resolveWith( + backgroundColor: WidgetStateColor.resolveWith( (states) => const Color.fromARGB(255, 109, 7, 0))), child: Text("Cancel")) ], diff --git a/lib/pages/home.dart b/lib/pages/home.dart index e0b72c7..29b33c6 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -1,8 +1,8 @@ import 'dart:io'; -import 'package:file_selector/file_selector.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:servermanager/packets/ClientPackets.dart'; import 'package:servermanager/pages/Constants.dart'; import 'package:servermanager/structs/settings.dart'; @@ -57,12 +57,7 @@ class HomePageState extends State { ListTile( title: Text("Proton"), leading: Icon(CupertinoIcons.gear), - subtitle: Text("Linux Proton"), - onTap: () { - if (settings.steamcmd_path.isNotEmpty) { - Navigator.pushNamed(context, "/proton"); - } - }, + subtitle: Text("Linux Proton: ${settings.proton_path}"), ), // Not yet implemented ListTile( title: Text("SteamCMD"), @@ -82,20 +77,38 @@ class HomePageState extends State { subtitle: settings.game_path.isEmpty ? Text("Not Set") : Text(settings.game_path), - onTap: () async { - var path = await getDirectoryPath(); + ), + SwitchListTile( + value: settings.FTS, + onChanged: (B) { setState(() { - if (path != null && path.isNotEmpty) { - settings.game_path = path; - settings.steamcmd_path = - "$path${Platform.pathSeparator}scmd"; - - Directory.current = Directory(settings.game_path); - } + settings.FTS = B; }); + }, + title: Text("First Time Setup Mode"), + subtitle: Text( + "Enabling this will disable server startup, leaving only the wrapper interface to interact with."), + ), + ListTile( + title: Text("Manager Credentials"), + subtitle: Text("Edit ServerManager credentials"), + leading: Icon(Icons.key), + onTap: () { + Navigator.pushNamed(context, "/creds", + arguments: settings.serverLoginCreds); + }, + ), + ListTile( + title: Text("Save Changes"), + subtitle: Text( + "This will upload the settings to the server and trigger a restart"), + onTap: () async { + Settings settings = Settings(); - await settings.Open(); - settings.Read(); + C2SUploadSettingsPacket upload = C2SUploadSettingsPacket(); + upload.srvSettings = settings.serialize(); + + settings.client!.send(upload); }, ) ], diff --git a/lib/pages/steamcmd.dart b/lib/pages/steamcmd.dart deleted file mode 100644 index 8f9c0d5..0000000 --- a/lib/pages/steamcmd.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'dart:io'; - -import 'package:dio/dio.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:servermanager/pages/dialogbox.dart'; -import 'package:servermanager/structs/credentials.dart'; -import 'package:servermanager/structs/settings.dart'; - -class SteamCMD extends StatefulWidget { - Settings settings; - SteamCMD({super.key, required this.settings}); - - @override - SteamCMDState createState() => SteamCMDState(settings: settings); -} - -class SteamCMDState extends State { - Settings settings = Settings(); - SteamCMDState({required this.settings}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("Conan Exiles Server Manager - Steam Command"), - backgroundColor: Color.fromARGB(255, 100, 0, 0), - ), - body: SingleChildScrollView( - child: Column( - children: [ - ListTile( - title: Text("Download/Initialize SteamCmd"), - leading: Icon(CupertinoIcons.cloud_download), - subtitle: Text( - "Creates the steamcmd folder, and downloads the steamcmd bootstrap. Then performs the initial update."), - onTap: () { - showDialog( - context: context, - builder: (X) => InputBox( - "", - promptText: - "This action will delete any existing copy of SteamCMD and download a fresh copy. \nAre you sure?", - changed: (X) {}, - onSubmit: () async { - settings.initializeSteamCmd(); - }, - onCancel: () {}, - isDefault: false, - hasInputField: false, - )); - }, - ), - ListTile( - title: Text("Download SteamCmd-2fa"), - leading: Icon(CupertinoIcons.lock_shield_fill), - subtitle: Text( - "Downloads a modified version of steamcmd-2fa from https://github.com/zontreck/steamcmd-2fa"), - onTap: () async { - final dio = Dio(); - await dio.download( - settings.Base2FAPath + (Platform.isWindows ? ".exe" : ""), - settings.steamcmd_path + - Platform.pathSeparator + - (Platform.isWindows - ? "steamcmd-2fa.exe" - : "steamcmd-2fa")); - if (!Platform.isWindows) { - var proc = await Process.start("chmod", [ - "+x", - "${settings.steamcmd_path}${Platform.pathSeparator}steamcmd-2fa" - ]); - } - }), - ListTile( - title: Text("Credentials"), - leading: Icon(Icons.key_sharp), - subtitle: Text("Steam Credentials"), - onTap: () async { - var creds = await Navigator.pushNamed(context, "/steamcmd/creds", - arguments: settings.inst!.steam_creds); - if (creds != null) { - Credentials cred = creds as Credentials; - setState(() { - settings.inst!.steam_creds = cred; - settings.Write(); - }); - } - }, - ), - ], - )), - ); - } -} - -// Returns a Credentials Object -class CredentialsPrompt extends StatelessWidget { - TextEditingController username = TextEditingController(); - TextEditingController password = TextEditingController(); - TextEditingController secret = TextEditingController(); - bool initialInitDone = false; - bool z2fa = false; - - @override - Widget build(BuildContext context) { - final args = ModalRoute.of(context)!.settings.arguments as Credentials?; - - if (args != null) { - if (!initialInitDone) { - username.text = args.username; - password.text = args.password; - secret.text = args.secret; - initialInitDone = true; - } - } - - return Scaffold( - appBar: AppBar( - title: - Text("Conan Exiles Server Manager - Steam Command - Credentials"), - backgroundColor: Color.fromARGB(255, 100, 0, 0), - ), - body: WillPopScope( - child: SingleChildScrollView( - padding: EdgeInsets.all(16), - child: Column( - children: [ - Row( - children: [ - SizedBox( - width: 150, - child: Row( - children: [ - Icon(Icons.person), - Text("Username:"), - ], - )), - Expanded( - child: TextField( - controller: username, - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(4))), - ), - ) - ], - ), - SizedBox( - height: 16, - ), - Row( - children: [ - SizedBox( - width: 150, - child: Row( - children: [ - Icon(Icons.key), - Text("Password:"), - ], - )), - Expanded( - child: TextField( - controller: password, - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(4))), - ), - ) - ], - ), - SizedBox( - height: 16, - ), - Row( - children: [ - SizedBox( - width: 150, - child: Row( - children: [ - Icon(Icons.dangerous), - Text("Secret:"), - ], - )), - Expanded( - child: TextField( - controller: secret, - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(4)), - hintText: - "2FA Shared Secret Code (Do Not Share With Others!)"), - )) - ], - ), - SwitchListTile( - value: z2fa, - title: Text("Enable 2FA"), - subtitle: Text( - "This may or may not be broken... steamcmd is shit!"), - onChanged: (value) { - z2fa = value; - }) - ], - )), - onWillPop: () async { - Navigator.pop( - context, - Credentials( - username: username.text, - password: password.text, - secret: secret.text, - has_2fa: z2fa)); - return true; - }, - )); - } -} diff --git a/lib/proton.dart b/lib/proton.dart index 6dd8cfb..812efb4 100644 --- a/lib/proton.dart +++ b/lib/proton.dart @@ -1,11 +1,13 @@ import 'dart:io'; +import 'package:libac_flutter/utils/IOTools.dart'; +import 'package:servermanager/statemachine.dart'; import 'package:servermanager/structs/settings.dart'; Future runProton(String command, List argx) async { Settings settings = Settings(); Directory dir = - Directory("${settings.game_path}${Platform.pathSeparator}pfx"); + Directory(PathHelper.builder(settings.base_path).resolve("pfx").build()); if (dir.existsSync()) { await dir.delete(recursive: true); @@ -37,7 +39,7 @@ Future runDetachedProton( String command, List argx, String workingDir) async { Settings settings = Settings(); Directory dir = - Directory("${settings.game_path}${Platform.pathSeparator}pfx"); + Directory(PathHelper.builder(settings.base_path).resolve("pfx").build()); if (dir.existsSync()) { await dir.delete(recursive: true); @@ -52,7 +54,8 @@ Future runDetachedProton( List args = ["run", command]; args.addAll(argx); - Process.start("proton", args, // Run arbitrary command with arguments + StateMachine.PROC = await Process.start( + "proton", args, // Run arbitrary command with arguments environment: env, workingDirectory: workingDir); } catch (e) { diff --git a/lib/statemachine.dart b/lib/statemachine.dart index 54a6332..a0d8bec 100644 --- a/lib/statemachine.dart +++ b/lib/statemachine.dart @@ -1,14 +1,16 @@ import 'dart:async'; import 'dart:io'; +import 'package:libac_flutter/packets/packets.dart'; import 'package:libac_flutter/utils/IOTools.dart'; -import 'package:rcon/rcon.dart'; import 'package:servermanager/game.dart'; import 'package:servermanager/proton.dart'; +import 'package:servermanager/structs/SessionData.dart'; import 'package:servermanager/structs/settings.dart'; enum States { Idle, // For when the state machine is waiting for a state change + PreStart, // Backup task Starting, // For when the server is starting up ModUpdateCheck, // For when the mods are being checked in the jail folder against the live mods WarnPlayersNonIntrusive, // Gives a non-instrusive warning about the upcoming restart @@ -18,7 +20,59 @@ enum States { Inactive // The inactive state will be used for when the state machine is not supposed to be doing anything. } +enum WarnType { Intrusive, NonIntrusive } + +enum WarnIntervals { + FIVE_SECONDS( + seconds: 5, type: WarnType.NonIntrusive, warning: "Tiiiiiimber!"), + TEN_SECONDS( + seconds: 10, + type: WarnType.Intrusive, + warning: "The server has 10 seconds until restart"), + TWENTY_SECONDS( + seconds: 20, + type: WarnType.Intrusive, + warning: "The server is about to go down in 20 seconds"), + THIRTY_SECONDS( + seconds: 30, + type: WarnType.NonIntrusive, + warning: "The server will restart in 30 seconds"), + ONE_MINUTE( + seconds: 60, + type: WarnType.NonIntrusive, + warning: "The server will restart in 1 minute"), + FIVE_MIN( + seconds: (5 * 60), + type: WarnType.NonIntrusive, + warning: "The server will restart in 5 minutes"), + TEN_MIN( + seconds: (10 * 60), + type: WarnType.NonIntrusive, + warning: "The server will restart in 10 minutes"), + ONE_HOUR( + seconds: (1 * 60 * 60), + type: WarnType.NonIntrusive, + warning: "The server will restart in 1 hour"), + TWO_HOURS( + seconds: (2 * 60 * 60), + type: WarnType.NonIntrusive, + warning: "The server will restart in 2 hours"), + NONE( + seconds: -1, + type: WarnType.NonIntrusive, + warning: + ""); // -1 is a impossible value, this makes this one a good default value + + final int seconds; + final WarnType type; + final String warning; + + const WarnIntervals( + {required this.seconds, required this.type, required this.warning}); +} + class StateMachine { + static Process? PROC; var _currentState = States.Inactive; StreamController _stateController = StreamController.broadcast(); Stream get stateChanges => _stateController.stream; @@ -35,21 +89,15 @@ class StateMachine { return; // Nothing to do here } else if (currentState == States.FullStop) { Settings settings = Settings(); - Client cli = await Client.create( - "127.0.0.1", settings.inst!.serverSettings.RconPort); - cli.login(settings.inst!.serverSettings.RconPassword); - - cli.send(Message.create(cli, PacketType.command, "shutdown")); + await settings.sendRconCommand("shutdown"); changeState(States.Inactive); } else if (currentState == States.Starting) { // Server startup in progress Settings settings = Settings(); await settings.RunUpdate(valid: false); - if (settings.inst!.downloadMods) - await doDownloadMods(false); - else - await doMigrateMods(); + await doDownloadMods(false); + settings.inst!.mods = await doScanMods(); await settings.writeOutModListFile(); @@ -64,7 +112,7 @@ class StateMachine { ]; // Start the server now if (Platform.isWindows) { - Process.start( + PROC = await Process.start( PathHelper.combine( settings.getServerPath(), "ConanSandboxServer.exe"), conanArgs, @@ -78,6 +126,11 @@ class StateMachine { } changeState(States.Idle); + } else if (currentState == States.PreStart) { + // Perform Backup Task + + // Finally, allow the server to start up + changeState(States.Starting); } } @@ -86,4 +139,75 @@ class StateMachine { await runTask(); }); } + + Timer? task; + + /// You should only start this task once the server is ready to be started. + /// This task will start the server on first-start + /// + /// It will monitor the states for shutdown, it will also monitor the process handle + /// + /// This is what runs the automatic restart timers as well. All state change logic that should happen automatically is contained in here. + Future startScheduler() async { + Settings settings = Settings(); + SessionData.timer = settings.inst!.timer.time.copy(); + changeState(States.PreStart); + + // Schedule the server task + task = Timer.periodic(Duration(seconds: 1), (timer) { + switch (currentState) { + case States.Inactive: + { + timer.cancel(); + + //Settings settings = Settings(); + if (settings.inst!.pterodactylMode) { + // Shut down the server processes now + PacketServer.socket!.close(); + } + break; + } + case States.Idle: + { + // Restart timers and such + SessionData.timer.tickDown(); + SessionData.operating_time.tickUp(); + + // Check if we should send an alert + int sec = SessionData.timer.getTotalSeconds(); + WarnIntervals current = SessionData.CURRENT_INTERVAL; + bool send = false; + for (WarnIntervals WI in WarnIntervals.values) { + if (WI.seconds <= sec && WI.seconds < current.seconds) { + current = WI; + send = true; + + break; + } + } + + if (send) { + // Send the alert message + } + + // Check Shutdown Pending + if (SessionData.shutdownPending) { + // Shut down the server + changeState(States.FullStop); + SessionData.shutdownPending = false; + } + + // Check Total Seconds + if (SessionData.timer.getTotalSeconds() == 0) { + SessionData.shutdownPending = true; + } + break; + } + default: + { + break; + } + } + }); + } } diff --git a/lib/structs/SessionData.dart b/lib/structs/SessionData.dart new file mode 100644 index 0000000..904c8a4 --- /dev/null +++ b/lib/structs/SessionData.dart @@ -0,0 +1,18 @@ +import 'package:libac_flutter/utils/TimeUtils.dart'; +import 'package:servermanager/statemachine.dart'; + +class SessionData { + static bool shutdownPending = false; + + /// This flag will track the number of seconds until restart + /// + /// This is initialized initially based on the AutomaticRestart timer if enabled. + /// + /// This may be updated to one to two minutes by a Mod Update, or Wrapper update + static Time timer = Time(hours: 0, minutes: 0, seconds: 0); + static Time operating_time = Time(hours: 0, minutes: 0, seconds: 0); + + static String shutdownMessage = ""; + + static WarnIntervals CURRENT_INTERVAL = WarnIntervals.NONE; +} diff --git a/lib/structs/autorestarts.dart b/lib/structs/autorestarts.dart index 7ad66b0..3e55a50 100644 --- a/lib/structs/autorestarts.dart +++ b/lib/structs/autorestarts.dart @@ -1,32 +1,28 @@ import 'package:libac_flutter/nbt/NbtUtils.dart'; import 'package:libac_flutter/nbt/impl/CompoundTag.dart'; import 'package:libac_flutter/nbt/impl/IntTag.dart'; +import 'package:libac_flutter/utils/TimeUtils.dart'; class AutomaticRestartInfo { - final int hours; - final int minutes; - final int seconds; + Time time = Time(hours: 0, minutes: 0, seconds: 0); final bool enabled; - const AutomaticRestartInfo( - {this.hours = 0, - this.minutes = 0, - this.seconds = 0, - this.enabled = false}); + AutomaticRestartInfo({required this.time, this.enabled = false}); static AutomaticRestartInfo deserialize(CompoundTag tag) { return AutomaticRestartInfo( - hours: tag.get(TAG_HOURS)?.asInt() ?? 12, - minutes: tag.get(TAG_MINUTES)?.asInt() ?? 0, - seconds: tag.get(TAG_SECONDS)?.asInt() ?? 0, + time: Time( + hours: tag.get(TAG_HOURS)?.asInt() ?? 12, + minutes: tag.get(TAG_MINUTES)?.asInt() ?? 0, + seconds: tag.get(TAG_SECONDS)?.asInt() ?? 0), enabled: NbtUtils.readBoolean(tag, TAG_ENABLED)); } CompoundTag serialize() { CompoundTag tag = CompoundTag(); - tag.put(TAG_HOURS, IntTag.valueOf(hours)); - tag.put(TAG_MINUTES, IntTag.valueOf(minutes)); - tag.put(TAG_SECONDS, IntTag.valueOf(seconds)); + tag.put(TAG_HOURS, IntTag.valueOf(time.hours)); + tag.put(TAG_MINUTES, IntTag.valueOf(time.minutes)); + tag.put(TAG_SECONDS, IntTag.valueOf(time.seconds)); NbtUtils.writeBoolean(tag, TAG_ENABLED, enabled); return tag; diff --git a/lib/structs/credentials.dart b/lib/structs/credentials.dart index cb5f919..5326025 100644 --- a/lib/structs/credentials.dart +++ b/lib/structs/credentials.dart @@ -1,25 +1,19 @@ -import 'package:libac_flutter/nbt/NbtUtils.dart'; import 'package:libac_flutter/nbt/impl/CompoundTag.dart'; import 'package:libac_flutter/nbt/impl/StringTag.dart'; class Credentials { String username; String password; - String secret; - bool has_2fa = false; - Credentials( - {required this.username, - required this.password, - required this.secret, - required this.has_2fa}); + Credentials({ + required this.username, + required this.password, + }); CompoundTag save() { CompoundTag tag = CompoundTag(); tag.put(TAG_USERNAME, StringTag.valueOf(username)); tag.put(TAG_PASSWORD, StringTag.valueOf(password)); - tag.put(TAG_SECRET, StringTag.valueOf(secret)); - NbtUtils.writeBoolean(tag, TAG_2FA, has_2fa); return tag; } @@ -27,14 +21,10 @@ class Credentials { static Credentials deserialize(CompoundTag tag) { return Credentials( username: tag.get(TAG_USERNAME)?.asString() ?? "", - password: tag.get(TAG_PASSWORD)?.asString() ?? "", - secret: tag.get(TAG_SECRET)?.asString() ?? "", - has_2fa: NbtUtils.readBoolean(tag, TAG_2FA)); + password: tag.get(TAG_PASSWORD)?.asString() ?? ""); } static const TAG_NAME = "credentials"; static const TAG_USERNAME = "username"; static const TAG_PASSWORD = "password"; - static const TAG_SECRET = "secret"; - static const TAG_2FA = "2fa"; } diff --git a/lib/structs/mod.dart b/lib/structs/mod.dart index 696841f..cd70a34 100644 --- a/lib/structs/mod.dart +++ b/lib/structs/mod.dart @@ -1,4 +1,6 @@ -import 'package:libac_flutter/utils/uuid/NbtUUID.dart'; +import 'package:libac_flutter/nbt/impl/CompoundTag.dart'; +import 'package:libac_flutter/nbt/impl/IntTag.dart'; +import 'package:libac_flutter/nbt/impl/StringTag.dart'; import 'package:libac_flutter/utils/uuid/UUID.dart'; class Mod { @@ -8,10 +10,10 @@ class Mod { String mod_hash = ""; bool newMod = false; - NbtUUID _id = NbtUUID.ZERO; + UUID _id = UUID.ZERO; String mod_instance_id() { - if (_id.toString() == NbtUUID.ZERO) { - _id = NbtUUID.fromUUID(UUID.generate(4)); + if (_id.toString() == UUID.ZERO.toString()) { + _id = UUID.generate(4); } return _id.toString(); @@ -23,4 +25,24 @@ class Mod { this.newMod = false, this.mod_pak = "Not Initialized", this.mod_hash = ""}); + + CompoundTag serialize() { + CompoundTag tag = CompoundTag(); + tag.put("name", StringTag.valueOf(mod_name)); + tag.put("id", IntTag.valueOf(mod_id)); + tag.put("pak", StringTag.valueOf(mod_pak)); + tag.put("hash", StringTag.valueOf(mod_hash)); + + return tag; + } + + static Mod deserialize(CompoundTag tag) { + CompoundTag ct = tag as CompoundTag; + + return Mod( + mod_name: ct.get("name")!.asString(), + mod_id: ct.get("id")!.asInt(), + mod_pak: ct.get("pak")!.asString(), + mod_hash: ct.get("hash")!.asString()); + } } diff --git a/lib/structs/settings.dart b/lib/structs/settings.dart index 90d3749..4130058 100644 --- a/lib/structs/settings.dart +++ b/lib/structs/settings.dart @@ -5,14 +5,19 @@ import 'package:dio/dio.dart'; import 'package:libac_flutter/nbt/NbtIo.dart'; import 'package:libac_flutter/nbt/NbtUtils.dart'; import 'package:libac_flutter/nbt/impl/CompoundTag.dart'; +import 'package:libac_flutter/nbt/impl/StringTag.dart'; import 'package:libac_flutter/packets/packets.dart'; import 'package:libac_flutter/utils/IOTools.dart'; +import 'package:libac_flutter/utils/uuid/NbtUUID.dart'; import 'package:libac_flutter/utils/uuid/UUID.dart'; +import 'package:rcon/rcon.dart'; import 'package:servermanager/statemachine.dart'; import 'package:servermanager/structs/credentials.dart'; import 'package:servermanager/structs/mod.dart'; import 'package:servermanager/structs/settingsEntry.dart'; +import '../proton.dart'; + class Settings { final String windows = "https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip"; @@ -28,13 +33,15 @@ class Settings { Settings._(); static final Settings Instance = Settings._(); + bool server = true; + String steamcmd_path = ""; String game_path = ""; String proton_path = ""; String base_path = ""; bool FTS = true; - Credentials serverLoginCreds = Credentials( - username: "admin", password: "changeMe123", secret: "", has_2fa: false); + Credentials serverLoginCreds = + Credentials(username: "admin", password: "changeMe123"); UUID remoteLoginToken = UUID.ZERO; PacketClient? client; @@ -45,9 +52,50 @@ class Settings { return Instance; } + CompoundTag serialize() { + CompoundTag tag = CompoundTag(); + tag.put("steamcmd", StringTag.valueOf(steamcmd_path)); + tag.put("game", StringTag.valueOf(game_path)); + tag.put("proton", StringTag.valueOf(proton_path)); + tag.put("base", StringTag.valueOf(base_path)); + NbtUtils.writeBoolean(tag, "fts", FTS); + + tag.put("server_creds", serverLoginCreds.save()); + NbtUtils.writeUUID(tag, "token", NbtUUID.fromUUID(remoteLoginToken)); + + if (inst != null) tag.put("main", inst!.serialize()); + + return tag; + } + + void deserialize(CompoundTag tag) { + // Verify Remote Login Token + UUID ID = NbtUtils.readUUID(tag, "token").toUUID(); + if (ID.toString() != remoteLoginToken.toString()) { + // Invalid session + print( + "Invalid login session detected, or two admins are connected at once"); + } + + steamcmd_path = tag.get("steamcmd")!.asString(); + game_path = tag.get("game")!.asString(); + proton_path = tag.get("proton")!.asString(); + base_path = tag.get("proton")!.asString(); + FTS = NbtUtils.readBoolean(tag, "fts"); // First Time Setup. + // FTS should be disabled by the client when sending it back to the server in a C2SApplySettingsPacket + + serverLoginCreds = + Credentials.deserialize(tag.get("server_creds")!.asCompoundTag()); + + if (tag.containsKey("main")) { + inst = SettingsEntry.deserialize(tag.get("main")!.asCompoundTag()); + } + } + SettingsEntry? inst; Future Read() async { + if (!server) return; try { var tag = await NbtIo.read("settings.dat"); @@ -58,19 +106,23 @@ class Settings { } catch (E) { print("No existing settings file found, initializing default settings"); inst = SettingsEntry(); - inst!.steam_creds = - Credentials(username: "", password: "", secret: "", has_2fa: false); + inst!.steam_creds = Credentials( + username: "", + password: "", + ); serverLoginCreds = Credentials( - username: "admin", - password: "changeMe123", - secret: "", - has_2fa: false); + username: "admin", + password: "changeMe123", + ); FTS = true; } } void Write() { + if (!server) { + return; // safeguard against writing to a settings file on the client + } if (inst == null) return; CompoundTag tag = CompoundTag(); tag.put("entry", inst!.serialize()); @@ -110,10 +162,6 @@ class Settings { return steamcmd_path; } - String getSteamCmd2FA() { - return "$steamcmd_path${Platform.pathSeparator}steamcmd-2fa${Platform.isWindows ? ".exe" : ""}"; - } - String getModPath() { return PathHelper(pth: base_path).resolve("mods").build(); } @@ -180,8 +228,9 @@ class Settings { if (await dir.exists()) { return; - } else + } else { await dir.create(recursive: true); + } } File getModListFile() { @@ -204,7 +253,7 @@ class Settings { .resolve("content") .resolve("440900") .resolve("${mod.mod_id}") - .resolve("${mod.mod_pak}") + .resolve(mod.mod_pak) .build(); if (Platform.isWindows) { paths.add(pth); @@ -220,6 +269,18 @@ class Settings { flush: true, mode: FileMode.writeOnly); } + Future initializeProtonPrefix() async { + runProton("echo", ["hello"]); + } + + Future sendRconCommand(String command) async { + Client cli = + await Client.create("127.0.0.1", inst!.serverSettings.RconPort); + Message msg = Message.create(cli, PacketType.command, command); + + return cli.send(msg).payload; + } + Future initializeProton() async { Dio dio = Dio(); print("Downloading proton..."); @@ -257,7 +318,7 @@ class Settings { if (Platform.isWindows) { // Download zip file - final path = "${steamcmd_path}${Platform.pathSeparator}windows.zip"; + final path = "$steamcmd_path${Platform.pathSeparator}windows.zip"; final reply = await dio.download(windows, path); final bytes = File(path).readAsBytesSync(); @@ -283,7 +344,7 @@ class Settings { print("Completed."); } else { // Download tgz file - final path = "${steamcmd_path}${Platform.pathSeparator}linux.tgz"; + final path = "$steamcmd_path${Platform.pathSeparator}linux.tgz"; final reply = await dio.download(linux, path); final bytes = File(path).readAsBytesSync(); diff --git a/lib/structs/settingsEntry.dart b/lib/structs/settingsEntry.dart index b571b01..45b224f 100644 --- a/lib/structs/settingsEntry.dart +++ b/lib/structs/settingsEntry.dart @@ -1,6 +1,8 @@ import 'package:libac_flutter/nbt/NbtUtils.dart'; +import 'package:libac_flutter/nbt/Tag.dart'; import 'package:libac_flutter/nbt/impl/CompoundTag.dart'; -import 'package:libac_flutter/nbt/impl/StringTag.dart'; +import 'package:libac_flutter/nbt/impl/ListTag.dart'; +import 'package:libac_flutter/utils/TimeUtils.dart'; import 'package:servermanager/structs/autorestarts.dart'; import 'package:servermanager/structs/credentials.dart'; import 'package:servermanager/structs/mod.dart'; @@ -9,29 +11,37 @@ import 'package:servermanager/structs/serversettings.dart'; class SettingsEntry { List mods = []; Credentials? steam_creds; - AutomaticRestartInfo timer = AutomaticRestartInfo(); + bool pterodactylMode = true; // Default is to be compatible + AutomaticRestartInfo timer = + AutomaticRestartInfo(time: Time(hours: 0, minutes: 0, seconds: 0)); ServerSettings serverSettings = ServerSettings( RconPassword: "Password01234", RconPort: 7779, GamePort: 7780, QueryPort: 7782); - bool downloadMods = true; - String conanExilesLibraryPath = ""; - static SettingsEntry deserialize(CompoundTag tag) { SettingsEntry st = SettingsEntry(); - if (tag.containsKey(Credentials.TAG_NAME)) + if (tag.containsKey(Credentials.TAG_NAME)) { st.steam_creds = Credentials.deserialize(tag.get(Credentials.TAG_NAME) as CompoundTag); + } st.timer = AutomaticRestartInfo.deserialize( tag.get(AutomaticRestartInfo.TAG_NAME) as CompoundTag); st.serverSettings = ServerSettings.deserialize( tag.get(ServerSettings.TAG_NAME) as CompoundTag); - st.downloadMods = NbtUtils.readBoolean(tag, "download"); - st.conanExilesLibraryPath = tag.get("libpath")?.asString() ?? ""; + if (tag.containsKey("pterodactyl")) { + st.pterodactylMode = NbtUtils.readBoolean(tag, "pterodactyl"); + } + + st.mods.clear(); + ListTag lMods = tag.get("mods") as ListTag; + for (Tag tag in lMods.value) { + CompoundTag cTag = tag.asCompoundTag(); + st.mods.add(Mod.deserialize(cTag)); + } return st; } @@ -41,9 +51,13 @@ class SettingsEntry { if (steam_creds != null) tag.put(Credentials.TAG_NAME, steam_creds!.save()); tag.put(AutomaticRestartInfo.TAG_NAME, timer.serialize()); tag.put(ServerSettings.TAG_NAME, serverSettings.serialize()); - NbtUtils.writeBoolean(tag, "download", downloadMods); + NbtUtils.writeBoolean(tag, "pterodactyl", pterodactylMode); - tag.put("libpath", StringTag.valueOf(conanExilesLibraryPath)); + ListTag lMods = ListTag(); + for (Mod mod in mods) { + lMods.add(mod.serialize()); + } + tag.put("mods", lMods); return tag; } diff --git a/pubspec.yaml b/pubspec.yaml index 4088c7e..4b474f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,7 +41,7 @@ dependencies: crypto: libac_flutter: hosted: https://git.zontreck.com/api/packages/AriasCreations/pub/ - version: 1.0.12 + version: 1.0.20 rcon: ^1.0.0 dev_dependencies: diff --git a/test/rcon_command.dart b/test/rcon_command.dart new file mode 100644 index 0000000..9846a78 --- /dev/null +++ b/test/rcon_command.dart @@ -0,0 +1,13 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:servermanager/structs/settings.dart'; +import 'package:servermanager/structs/settingsEntry.dart'; + +void main() { + test("Send rcon command", () async { + Settings settings = Settings(); + settings.server = false; + settings.inst = SettingsEntry(); + + await settings.sendRconCommand("help"); + }); +}