Add state machine

This commit is contained in:
zontreck 2023-11-05 00:03:15 -07:00
parent 1bd6da4f1b
commit fee32c7701
7 changed files with 312 additions and 23 deletions

View file

@ -5,7 +5,9 @@ import 'package:flutter/material.dart';
import 'package:servermanager/autorestart.dart'; import 'package:servermanager/autorestart.dart';
import 'package:servermanager/mod.dart'; import 'package:servermanager/mod.dart';
import 'package:servermanager/pathtools.dart'; import 'package:servermanager/pathtools.dart';
import 'package:servermanager/serversettings.dart';
import 'package:servermanager/settings.dart'; import 'package:servermanager/settings.dart';
import 'package:servermanager/statemachine.dart';
Future<void> doDownloadMods(String modsFolder) async { Future<void> doDownloadMods(String modsFolder) async {
Settings settings = Settings(); Settings settings = Settings();
@ -100,7 +102,6 @@ class GameServerPageState extends State<GameServerPage> {
Settings settings; Settings settings;
GameServerPageState({required this.settings}); GameServerPageState({required this.settings});
var downloading = false; var downloading = false;
var running = false;
late Stream<List<int>> download_stream; late Stream<List<int>> download_stream;
TextEditingController ValueControl = TextEditingController(); TextEditingController ValueControl = TextEditingController();
@ -223,34 +224,35 @@ class GameServerPageState extends State<GameServerPage> {
}); });
}, },
), ),
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( ListTile(
title: Text("Status:"), title: Text("Status:"),
subtitle: Text(running ? "Active" : "Not Running"), subtitle: Text(settings.subsys.currentState != States.Inactive
leading: Icon(running ? Icons.play_arrow : Icons.error), ? "Active"
: "Not Running"),
leading: Icon(settings.subsys.currentState != States.Inactive
? Icons.play_arrow
: Icons.error),
onTap: () async { onTap: () async {
// Toggle state // Toggle state
setState(() { 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();
}
}, },
), ),
], ],

View file

@ -6,6 +6,7 @@ import 'package:servermanager/game.dart';
import 'package:servermanager/home.dart'; import 'package:servermanager/home.dart';
import 'package:servermanager/mod.dart'; import 'package:servermanager/mod.dart';
import 'package:servermanager/proton.dart'; import 'package:servermanager/proton.dart';
import 'package:servermanager/serversettings.dart';
import 'package:servermanager/settings.dart'; import 'package:servermanager/settings.dart';
import 'package:servermanager/settingsEntry.dart'; import 'package:servermanager/settingsEntry.dart';
import 'package:servermanager/steamcmd.dart'; import 'package:servermanager/steamcmd.dart';
@ -15,6 +16,8 @@ Future<void> main() async {
Hive.registerAdapter(CredentialsAdapter()); Hive.registerAdapter(CredentialsAdapter());
Hive.registerAdapter(ModAdapter()); Hive.registerAdapter(ModAdapter());
Hive.registerAdapter(SettingsEntryAdapter()); Hive.registerAdapter(SettingsEntryAdapter());
Hive.registerAdapter(AutomaticRestartInfoAdapter());
Hive.registerAdapter(ServerSettingsAdapter());
runApp(MyApp()); runApp(MyApp());
} }
@ -38,6 +41,7 @@ class MyApp extends StatelessWidget {
), ),
"/server": (context) => GameServerPage(settings: appSettings), "/server": (context) => GameServerPage(settings: appSettings),
"/server/autorestart": (context) => AutoRestartPage(), "/server/autorestart": (context) => AutoRestartPage(),
"/server/ports": (context) => ServerSettingsPage(),
"/server/mods": (context) => ModManager(settings: appSettings), "/server/mods": (context) => ModManager(settings: appSettings),
"/server/mods/edit": (context) => ModPage(), "/server/mods/edit": (context) => ModPage(),
"/steamcmd/creds": (context) => CredentialsPrompt() "/steamcmd/creds": (context) => CredentialsPrompt()

View file

@ -42,6 +42,33 @@ Future<void> runProton(String command, List<String> argx) async {
} }
} }
Future<void> runDetachedProton(
String command, List<String> 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<String, String> env = Map.from(Platform.environment);
env["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = "~/.steam";
env["STEAM_COMPAT_DATA_PATH"] = dir.path;
try {
List<String> 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<Proton> { class ProtonState extends State<Proton> {
Settings settings; Settings settings;
ProtonState({required this.settings}); ProtonState({required this.settings});

165
lib/serversettings.dart Normal file
View file

@ -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<ServerSettingsPage> {
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,
))
],
)
],
)),
),
),
);
}
}

View file

@ -4,6 +4,7 @@ import 'package:hive/hive.dart';
import 'package:servermanager/mod.dart'; import 'package:servermanager/mod.dart';
import 'package:servermanager/pathtools.dart'; import 'package:servermanager/pathtools.dart';
import 'package:servermanager/settingsEntry.dart'; import 'package:servermanager/settingsEntry.dart';
import 'package:servermanager/statemachine.dart';
class Settings { class Settings {
Settings._(); Settings._();
@ -12,6 +13,8 @@ class Settings {
String steamcmd_path = ""; String steamcmd_path = "";
String game_path = ""; String game_path = "";
StateMachine subsys = StateMachine();
factory Settings() { factory Settings() {
return Instance; return Instance;
} }

View file

@ -2,6 +2,7 @@ import 'package:hive/hive.dart';
import 'package:servermanager/autorestart.dart'; import 'package:servermanager/autorestart.dart';
import 'package:servermanager/credentials.dart'; import 'package:servermanager/credentials.dart';
import 'package:servermanager/mod.dart'; import 'package:servermanager/mod.dart';
import 'package:servermanager/serversettings.dart';
part 'settingsEntry.g.dart'; part 'settingsEntry.g.dart';
@ -15,4 +16,16 @@ class SettingsEntry {
@HiveField(4, defaultValue: AutomaticRestartInfo()) @HiveField(4, defaultValue: AutomaticRestartInfo())
AutomaticRestartInfo timer = 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);
} }

75
lib/statemachine.dart Normal file
View file

@ -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<States> _stateController = StreamController.broadcast();
Stream<States> get stateChanges => _stateController.stream;
States get currentState => _currentState;
void changeState(States n) {
_currentState = n;
_stateController.add(n);
}
Future<void> 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();
});
}
}