Got more things functional
This commit is contained in:
parent
a1141cd2b8
commit
fc0c1c7e7a
23 changed files with 1080 additions and 876 deletions
|
@ -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");
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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")));
|
||||
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."),
|
||||
)
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,69 +22,64 @@ 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(
|
||||
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<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,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")
|
||||
],
|
||||
))
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -34,105 +34,109 @@ 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(
|
||||
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"))
|
||||
],
|
||||
)
|
||||
],
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,101 +32,100 @@ 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(
|
||||
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"))
|
||||
],
|
||||
)
|
||||
]),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
97
lib/pages/credentials_prompt.dart
Normal file
97
lib/pages/credentials_prompt.dart
Normal 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))),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
|
@ -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"))
|
||||
],
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
)
|
||||
],
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
18
lib/structs/SessionData.dart
Normal file
18
lib/structs/SessionData.dart
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
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<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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
13
test/rcon_command.dart
Normal 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");
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue