Got more things functional

This commit is contained in:
zontreck 2024-05-23 22:18:59 -07:00
parent a1141cd2b8
commit fc0c1c7e7a
23 changed files with 1080 additions and 876 deletions

View file

@ -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");

View file

@ -8,17 +8,6 @@ import 'package:servermanager/structs/settings.dart';
Future<void> 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<String> manifest = [
"+@sSteamCmdForcePlatformType",
@ -26,9 +15,7 @@ Future<void> 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<void> doDownloadMods(bool jail) async {
}
await settings.createModFolderIfNotExists();
await settings.createModJailFolderIfNotExists();
manifest.add("+quit");
@ -48,44 +36,6 @@ Future<void> doDownloadMods(bool jail) async {
print(result.stdout);
}
Future<void> 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<List<Mod>> doScanMods() async {
Settings settings = Settings();
@ -114,8 +64,8 @@ Future<List<Mod>> 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(

View file

@ -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<void> 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 {

View file

@ -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<PacketResponse> 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<String, dynamic> toJson() {
return {"username": username, "password": Hashing.sha256Hash(password)};
}
@override
Future<void> 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<String, dynamic> js) {
valid = js['valid'] as bool;
token = UUID.parse(js['token'] as String);
}
@override
String getChannelID() {
return "LoginReply";
}
@override
Future<void> handleClientPacket() async {
// Handle login finalization related stuff
Settings settings = Settings();
settings.remoteLoginToken = token;
}
@override
Future<PacketResponse> handleServerPacket() async {
return PacketResponse.nil; // We only operate on the client
}
@override
Map<String, dynamic> 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<String, dynamic> js) {}
@override
String getChannelID() {
return "C2SRequestSettings";
}
@override
Future<void> handleClientPacket() async {
Settings settings = Settings();
settings.deserialize(serverSettings);
settings.server = false;
}
@override
Future<PacketResponse> handleServerPacket() async {
S2CResponse response = S2CResponse();
Settings settings = Settings();
serverSettings = settings.serialize();
response.contents = encodeTag().asCompoundTag();
return PacketResponse(replyDataTag: response.encodeTag().asCompoundTag());
}
@override
Map<String, dynamic> 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<void> 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<String, dynamic> js) {}
@override
String getChannelID() {
return "C2SUploadSettings";
}
@override
Future<void> handleClientPacket() async {
// No client response or handling needed
}
@override
Future<PacketResponse> 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<String, dynamic> toJson() {
return {};
}
}

View file

@ -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,18 +26,7 @@ class GameServerPageState extends State<GameServerPage> {
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")));
return false;
} else {
return true;
}
},
child: SingleChildScrollView(
body: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
children: [
@ -57,27 +44,6 @@ class GameServerPageState extends State<GameServerPage> {
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;
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),
@ -92,22 +58,13 @@ class GameServerPageState extends State<GameServerPage> {
context, "/server/autorestart",
arguments: settings.inst!.timer);
if (reply == null) return; // No change was made
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),
@ -115,33 +72,27 @@ class GameServerPageState extends State<GameServerPage> {
var reply = await Navigator.pushNamed(
context, "/server/ports",
arguments: settings.inst!.serverSettings);
if (reply != null) return; // If null, no change.
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
SwitchListTile(
value: settings.inst!.pterodactylMode,
onChanged: (B) {
setState(() {
if (settings.subsys.currentState == States.Inactive) {
settings.subsys.changeState(States.Starting);
} else
settings.subsys.changeState(States.FullStop);
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."),
)
],
)),
),
);
}
}

View file

@ -22,8 +22,7 @@ class ModManagerState extends State<ModManager> {
title: Text("Conan Exiles Server Manager - Mod Manager"),
backgroundColor: Color.fromARGB(255, 100, 0, 0),
),
body: WillPopScope(
child: ReorderableListView.builder(
body: ReorderableListView.builder(
onReorder: (oldIndex, newIndex) {
if (oldIndex < newIndex) {
// From top to Bottom
@ -55,7 +54,7 @@ class ModManagerState extends State<ModManager> {
padding: EdgeInsets.all(12),
child: ListTile(
title: Text(mod.mod_name),
subtitle: Text("ID: ${mod.mod_id}"),
subtitle: Text("ID: ${mod.mod_id}\nLoad Order: ${idx}"),
onTap: () async {
final reply = await Navigator.pushNamed(
context, "/server/mods/edit",
@ -67,25 +66,21 @@ class ModManagerState extends State<ModManager> {
newMod: false));
if (reply != null) {
setState(() {
settings.inst!.mods[idx] = reply as Mod;
});
} else {
if (reply is bool) {
setState(() {
settings.inst!.mods.removeAt(idx);
});
}
setState(() {
settings.inst!.mods[idx] = reply as Mod;
});
}
},
),
);
},
itemCount: settings.inst!.mods.length,
),
onWillPop: () async {
Navigator.pop(context);
return true;
},
),
floatingActionButton: ElevatedButton(
child: Icon(Icons.add),
onPressed: () async {
@ -93,11 +88,10 @@ class ModManagerState extends State<ModManager> {
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,8 +131,23 @@ class ModPage extends StatelessWidget {
title: Text("Mod Editor"),
backgroundColor: Color.fromARGB(255, 100, 0, 0),
),
body: WillPopScope(
child: SingleChildScrollView(
floatingActionButton: ElevatedButton(
child: Text("Save"),
onPressed: () {
int idVal = 0;
try {
idVal = int.parse(id.text);
} catch (E) {}
if (willDelete) {
Navigator.pop(context, true);
} else {
Navigator.pop(context,
Mod(mod_id: idVal, mod_name: name.text, newMod: false));
}
},
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(children: [
Row(
@ -166,8 +175,7 @@ class ModPage extends StatelessWidget {
SizedBox(
width: 150,
child: ListTile(
leading: Icon(Icons.perm_identity),
title: Text("Mod ID")),
leading: Icon(Icons.perm_identity), title: Text("Mod ID")),
),
Expanded(
child: TextField(
@ -188,12 +196,11 @@ class ModPage extends StatelessWidget {
),
ListTile(
title: Text("Mod Pak File: $pak"),
subtitle:
Text("Mod pak file name as detected during downloading"),
subtitle: Text("Mod pak file name as detected during downloading"),
),
ListTile(
title: Text("Mod Hash"),
subtitle: Text("$hash"),
subtitle: Text(hash),
),
if (!isNewMod)
ElevatedButton(
@ -212,21 +219,6 @@ class ModPage extends StatelessWidget {
))
]),
),
onWillPop: () async {
int idVal = 0;
try {
idVal = int.parse(id.text);
} catch (E) {}
if (willDelete) {
Navigator.pop(context, null);
} else {
Navigator.pop(context,
Mod(mod_id: idVal, mod_name: name.text, newMod: false));
}
return true;
},
),
);
}
}

View file

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

View file

@ -34,17 +34,7 @@ class ServerSettingsState extends State<ServerSettingsPage> {
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(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
@ -70,7 +60,7 @@ class ServerSettingsState extends State<ServerSettingsPage> {
width: 256,
child: ListTile(
title: Text("Rcon Port"),
subtitle: Text("${rconPort}"),
subtitle: Text("$rconPort"),
),
),
Expanded(
@ -92,7 +82,7 @@ class ServerSettingsState extends State<ServerSettingsPage> {
width: 256,
child: ListTile(
title: Text("Game Port"),
subtitle: Text("${gPort}"),
subtitle: Text("$gPort"),
),
),
Expanded(
@ -114,7 +104,7 @@ class ServerSettingsState extends State<ServerSettingsPage> {
width: 256,
child: ListTile(
title: Text("Query Port"),
subtitle: Text("${qPort}"),
subtitle: Text("$qPort"),
),
),
Expanded(
@ -129,11 +119,25 @@ class ServerSettingsState extends State<ServerSettingsPage> {
max: 8000,
))
],
),
Row(
children: [
ElevatedButton(
onPressed: () {
Navigator.pop(
context,
ServerSettings(
RconPassword: passwordController.text,
RconPort: rconPort,
GamePort: gPort,
QueryPort: qPort));
},
child: Text("Submit"))
],
)
],
)),
),
),
);
}
}

View file

@ -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<AutoRestartPage> {
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<AutoRestartPage> {
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,17 +32,7 @@ class AutoRestartState extends State<AutoRestartPage> {
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(
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(children: [
@ -64,17 +51,17 @@ class AutoRestartState extends State<AutoRestartPage> {
width: 256,
child: ListTile(
title: Text("Hours"),
subtitle: Text("${hours}"),
subtitle: Text("${time.hours}"),
),
),
Expanded(
child: Slider(
max: 24,
min: 0,
value: hours.toDouble(),
value: time.hours.toDouble(),
onChanged: (value) {
setState(() {
hours = value.toInt();
time.hours = value.toInt();
});
},
),
@ -87,17 +74,17 @@ class AutoRestartState extends State<AutoRestartPage> {
width: 256,
child: ListTile(
title: Text("Minutes"),
subtitle: Text("${minutes}"),
subtitle: Text("${time.minutes}"),
),
),
Expanded(
child: Slider(
max: 60,
min: 0,
value: minutes.toDouble(),
value: time.minutes.toDouble(),
onChanged: (value) {
setState(() {
minutes = value.toInt();
time.minutes = value.toInt();
});
},
),
@ -110,26 +97,35 @@ class AutoRestartState extends State<AutoRestartPage> {
width: 256,
child: ListTile(
title: Text("Seconds"),
subtitle: Text("${seconds}"),
subtitle: Text("${time.seconds}"),
),
),
Expanded(
child: Slider(
max: 60,
min: 0,
value: seconds.toDouble(),
value: time.seconds.toDouble(),
onChanged: (value) {
setState(() {
seconds = value.toInt();
time.seconds = value.toInt();
});
},
),
)
],
),
Row(
children: [
ElevatedButton(
onPressed: () {
Navigator.pop(context,
AutomaticRestartInfo(enabled: enabled, time: time));
},
child: Text("Submit"))
],
)
]),
)),
),
);
}
}

View file

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

View file

@ -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"))
],

View file

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

View file

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

View file

@ -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<void> runProton(String command, List<String> 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<void> runDetachedProton(
String command, List<String> 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<void> runDetachedProton(
List<String> 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) {

View file

@ -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<States> _stateController = StreamController.broadcast();
Stream<States> 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();
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<void> 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;
}
}
});
}
}

View file

@ -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;
}

View file

@ -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(
time: Time(
hours: tag.get(TAG_HOURS)?.asInt() ?? 12,
minutes: tag.get(TAG_MINUTES)?.asInt() ?? 0,
seconds: tag.get(TAG_SECONDS)?.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;

View file

@ -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,
Credentials({
required this.username,
required this.password,
required this.secret,
required this.has_2fa});
});
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";
}

View file

@ -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());
}
}

View file

@ -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<void> 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);
);
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,9 +228,10 @@ class Settings {
if (await dir.exists()) {
return;
} else
} else {
await dir.create(recursive: true);
}
}
File getModListFile() {
return File(PathHelper(pth: getServerPath())
@ -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<void> initializeProtonPrefix() async {
runProton("echo", ["hello"]);
}
Future<String> 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<void> 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();

View file

@ -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<Mod> 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;
}

View file

@ -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:

13
test/rcon_command.dart Normal file
View file

@ -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");
});
}