diff --git a/lib/game.dart b/lib/game.dart index c0406f8..2031aab 100644 --- a/lib/game.dart +++ b/lib/game.dart @@ -5,7 +5,9 @@ import 'package:flutter/material.dart'; import 'package:servermanager/autorestart.dart'; import 'package:servermanager/mod.dart'; import 'package:servermanager/pathtools.dart'; +import 'package:servermanager/serversettings.dart'; import 'package:servermanager/settings.dart'; +import 'package:servermanager/statemachine.dart'; Future doDownloadMods(String modsFolder) async { Settings settings = Settings(); @@ -100,7 +102,6 @@ class GameServerPageState extends State { Settings settings; GameServerPageState({required this.settings}); var downloading = false; - var running = false; late Stream> download_stream; TextEditingController ValueControl = TextEditingController(); @@ -223,34 +224,35 @@ class GameServerPageState extends State { }); }, ), + 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(running ? "Active" : "Not Running"), - leading: Icon(running ? Icons.play_arrow : Icons.error), + 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(() { - running = !running; + if (settings.subsys.currentState == States.Inactive) { + settings.subsys.changeState(States.Starting); + } else + settings.subsys.changeState(States.FullStop); }); - - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(running - ? "Server is now starting..." - : "Stopping the server"))); - - if (!running) { - // Send message indicating the server is stopping, then wait for 30 seconds and then stop the server - } else { - await settings.RunUpdate(); - await doDownloadMods(settings.getModPath()); - setState(() async { - settings.inst!.mods = - await doScanMods(settings.getModPath()); - }); - - // Generate the actual mod list now - await settings.writeOutModListFile(); - } }, ), ], diff --git a/lib/main.dart b/lib/main.dart index 1d44175..3a661a1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:servermanager/game.dart'; import 'package:servermanager/home.dart'; import 'package:servermanager/mod.dart'; import 'package:servermanager/proton.dart'; +import 'package:servermanager/serversettings.dart'; import 'package:servermanager/settings.dart'; import 'package:servermanager/settingsEntry.dart'; import 'package:servermanager/steamcmd.dart'; @@ -15,6 +16,8 @@ Future main() async { Hive.registerAdapter(CredentialsAdapter()); Hive.registerAdapter(ModAdapter()); Hive.registerAdapter(SettingsEntryAdapter()); + Hive.registerAdapter(AutomaticRestartInfoAdapter()); + Hive.registerAdapter(ServerSettingsAdapter()); runApp(MyApp()); } @@ -38,6 +41,7 @@ class MyApp extends StatelessWidget { ), "/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() diff --git a/lib/proton.dart b/lib/proton.dart index b8a8b86..5aba415 100644 --- a/lib/proton.dart +++ b/lib/proton.dart @@ -42,6 +42,33 @@ Future runProton(String command, List argx) async { } } +Future runDetachedProton( + String command, List argx, String workingDir) async { + Settings settings = Settings(); + Directory dir = + Directory("${settings.game_path}${Platform.pathSeparator}pfx"); + + if (dir.existsSync()) { + await dir.delete(recursive: true); + } + await dir.create(recursive: true); + + Map env = Map.from(Platform.environment); + env["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = "~/.steam"; + env["STEAM_COMPAT_DATA_PATH"] = dir.path; + + try { + List args = ["run", command]; + args.addAll(argx); + + Process.start("proton", args, // Run arbitrary command with arguments + environment: env, + workingDirectory: workingDir); + } catch (e) { + print('Error executing command: $e'); + } +} + class ProtonState extends State { Settings settings; ProtonState({required this.settings}); diff --git a/lib/serversettings.dart b/lib/serversettings.dart new file mode 100644 index 0000000..7d92f18 --- /dev/null +++ b/lib/serversettings.dart @@ -0,0 +1,165 @@ +import 'dart:ffi'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; + +part 'serversettings.g.dart'; + +@HiveType(typeId: 5) +class ServerSettings { + @HiveField(0, defaultValue: "Password01234") + final String RconPassword; + + @HiveField(1, defaultValue: 7779) + final int RconPort; + + @HiveField(2, defaultValue: 7780) + final int GamePort; + + @HiveField(3, defaultValue: 7782) + final int QueryPort; + + const ServerSettings( + {required this.RconPassword, + required this.RconPort, + required this.GamePort, + required this.QueryPort}); +} + +class ServerSettingsPage extends StatefulWidget { + ServerSettingsPage({super.key}); + + @override + ServerSettingsState createState() => ServerSettingsState(); +} + +class ServerSettingsState extends State { + bool firstRun = true; + String pass = ""; + TextEditingController passwordController = TextEditingController(); + int rconPort = 0; + int gPort = 0; + int qPort = 0; + + @override + Widget build(BuildContext context) { + if (firstRun) { + var args = ModalRoute.of(context)!.settings.arguments as ServerSettings; + + passwordController.text = args.RconPassword; + rconPort = args.RconPort; + gPort = args.GamePort; + qPort = args.QueryPort; + + firstRun = false; + } + return Scaffold( + appBar: AppBar( + title: Text("Server Settings"), + backgroundColor: Color.fromARGB(255, 100, 0, 0), + ), + body: WillPopScope( + onWillPop: () async { + Navigator.pop( + context, + ServerSettings( + RconPassword: passwordController.text, + RconPort: rconPort, + GamePort: gPort, + QueryPort: qPort)); + return true; + }, + child: 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: 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: 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, + )) + ], + ) + ], + )), + ), + ), + ); + } +} diff --git a/lib/settings.dart b/lib/settings.dart index e375f92..533acd2 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -4,6 +4,7 @@ import 'package:hive/hive.dart'; import 'package:servermanager/mod.dart'; import 'package:servermanager/pathtools.dart'; import 'package:servermanager/settingsEntry.dart'; +import 'package:servermanager/statemachine.dart'; class Settings { Settings._(); @@ -12,6 +13,8 @@ class Settings { String steamcmd_path = ""; String game_path = ""; + StateMachine subsys = StateMachine(); + factory Settings() { return Instance; } diff --git a/lib/settingsEntry.dart b/lib/settingsEntry.dart index c4b7515..0d7fb65 100644 --- a/lib/settingsEntry.dart +++ b/lib/settingsEntry.dart @@ -2,6 +2,7 @@ import 'package:hive/hive.dart'; import 'package:servermanager/autorestart.dart'; import 'package:servermanager/credentials.dart'; import 'package:servermanager/mod.dart'; +import 'package:servermanager/serversettings.dart'; part 'settingsEntry.g.dart'; @@ -15,4 +16,16 @@ class SettingsEntry { @HiveField(4, defaultValue: AutomaticRestartInfo()) AutomaticRestartInfo timer = AutomaticRestartInfo(); + + @HiveField(5, + defaultValue: ServerSettings( + RconPassword: "Password01234", + RconPort: 7779, + GamePort: 7780, + QueryPort: 7782)) + ServerSettings serverSettings = ServerSettings( + RconPassword: "Password01234", + RconPort: 7779, + GamePort: 7780, + QueryPort: 7782); } diff --git a/lib/statemachine.dart b/lib/statemachine.dart new file mode 100644 index 0000000..3e6ecd3 --- /dev/null +++ b/lib/statemachine.dart @@ -0,0 +1,75 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:servermanager/game.dart'; +import 'package:servermanager/pathtools.dart'; +import 'package:servermanager/proton.dart'; +import 'package:servermanager/settings.dart'; + +enum States { + Idle, // For when the state machine is waiting for a state change + 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 + WarnPlayersIntrusive, // Sends a message intrusively about the upcoming restart + FullStop, // Intends to set the inactive state, and immediately stops the server + + Inactive // The inactive state will be used for when the state machine is not supposed to be doing anything. +} + +class StateMachine { + var _currentState = States.Inactive; + StreamController _stateController = StreamController.broadcast(); + Stream get stateChanges => _stateController.stream; + + States get currentState => _currentState; + + void changeState(States n) { + _currentState = n; + _stateController.add(n); + } + + Future runTask() async { + if (currentState == States.Idle) { + return; // Nothing to do here + } else if (currentState == States.Starting) { + // Server startup in progress + Settings settings = Settings(); + await settings.RunUpdate(); + await doDownloadMods(settings.getModPath()); + settings.inst!.mods = await doScanMods(settings.getModPath()); + + await settings.writeOutModListFile(); + + var conanArgs = [ + "-RconEnabled=1", + "-RconPassword=${settings.inst!.serverSettings.RconPassword}", + "-RconPort=${settings.inst!.serverSettings.RconPort}", + "-Port=${settings.inst!.serverSettings.GamePort}", + "-QueryPort=${settings.inst!.serverSettings.QueryPort}" + ]; + // Start the server now + if (Platform.isWindows) { + Process.start( + PathHelper.combine( + settings.getServerPath(), "ConanSandboxServer.exe"), + conanArgs, + workingDirectory: settings.getServerPath()); + } else { + runDetachedProton( + PathHelper.combine( + settings.getServerPath(), "ConanSandboxServer.exe"), + conanArgs, + settings.getServerPath()); + } + + changeState(States.Idle); + } + } + + StateMachine() { + stateChanges.listen((event) async { + await runTask(); + }); + } +}