NOTE: Moves the jail folder where the mods folder used to be. The Jail folder is then recreated after the next restart.
436 lines
12 KiB
Dart
436 lines
12 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:archive/archive.dart';
|
|
import 'package:dio/dio.dart';
|
|
import 'package:libac_dart/nbt/NbtIo.dart';
|
|
import 'package:libac_dart/nbt/NbtUtils.dart';
|
|
import 'package:libac_dart/nbt/impl/CompoundTag.dart';
|
|
import 'package:libac_dart/nbt/impl/StringTag.dart';
|
|
import 'package:libac_dart/packets/packets.dart';
|
|
import 'package:libac_dart/utils/IOTools.dart';
|
|
import 'package:libac_dart/utils/uuid/NbtUUID.dart';
|
|
import 'package:libac_dart/utils/uuid/UUID.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 'package:servermanager/wine.dart';
|
|
|
|
class Settings {
|
|
final String windows =
|
|
"https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip";
|
|
final String linux =
|
|
"https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz";
|
|
|
|
final String Base2FAPath =
|
|
"https://github.com/zontreck/steamcmd-2fa/releases/download/0.2.0/steamcmd-2fa";
|
|
|
|
static String ABSOLUTE_PATH = "";
|
|
|
|
static void makeAbsoluteSettingsPath() {
|
|
File fi = File("settings.dat");
|
|
ABSOLUTE_PATH = fi.absolute.path;
|
|
}
|
|
|
|
Settings._();
|
|
static final Settings Instance = Settings._();
|
|
|
|
bool server = true;
|
|
bool wineInitialized = false;
|
|
|
|
String steamcmd_path = "";
|
|
String game_path = "";
|
|
String base_path = "";
|
|
String gameServerDBFile = "";
|
|
bool FTS = true;
|
|
UUID remoteLoginToken = UUID.ZERO;
|
|
|
|
bool isDownloadingMods = false;
|
|
|
|
PacketClient? client;
|
|
User superuser = User.make("admin", "changeMe123", UserLevel.Super_User);
|
|
|
|
User? loggedInUser;
|
|
|
|
StateMachine subsys = StateMachine();
|
|
|
|
factory Settings() {
|
|
return Instance;
|
|
}
|
|
|
|
CompoundTag serialize() {
|
|
CompoundTag tag = CompoundTag();
|
|
tag.put("steamcmd", StringTag.valueOf(steamcmd_path));
|
|
tag.put("game", StringTag.valueOf(game_path));
|
|
tag.put("base", StringTag.valueOf(base_path));
|
|
tag.put("dbfile", StringTag.valueOf(getWorldGameDB()));
|
|
NbtUtils.writeBoolean(tag, "fts", FTS);
|
|
NbtUtils.writeBoolean(tag, "wine_init", wineInitialized);
|
|
|
|
tag.put("superuser", superuser.serialize());
|
|
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();
|
|
base_path = tag.get("base")!.asString();
|
|
if (tag.containsKey("dbfile")) {
|
|
gameServerDBFile = tag.get("dbfile")!.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
|
|
|
|
if (tag.containsKey("superuser")) {
|
|
superuser = User.deserialize(tag.get("superuser")!.asCompoundTag());
|
|
}
|
|
|
|
if (tag.containsKey("wine_init")) {
|
|
wineInitialized = NbtUtils.readBoolean(tag, "wine_init");
|
|
}
|
|
|
|
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(ABSOLUTE_PATH) as CompoundTag;
|
|
|
|
inst = SettingsEntry.deserialize(tag.get("entry") as CompoundTag);
|
|
|
|
if (tag.containsKey("wine_init")) {
|
|
wineInitialized = NbtUtils.readBoolean(tag, "wine_init");
|
|
}
|
|
|
|
if (tag.containsKey("superuser")) {
|
|
superuser = User.deserialize(tag.get("superuser")!.asCompoundTag());
|
|
}
|
|
|
|
FTS = NbtUtils.readBoolean(tag, "fts");
|
|
} catch (E) {
|
|
print("No existing settings file found, initializing default settings");
|
|
inst = SettingsEntry();
|
|
|
|
superuser = User.make("admin", "changeMe123", UserLevel.Super_User);
|
|
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());
|
|
tag.put("superuser", superuser.serialize());
|
|
NbtUtils.writeBoolean(tag, "fts", FTS);
|
|
NbtUtils.writeBoolean(tag, "wine_init", wineInitialized);
|
|
|
|
NbtIo.write(ABSOLUTE_PATH, tag);
|
|
}
|
|
|
|
Future<void> Open() async {
|
|
Close();
|
|
Instance.Read();
|
|
}
|
|
|
|
static void Close() async {
|
|
Instance.Write();
|
|
Instance.inst = null;
|
|
}
|
|
|
|
String getServerPath() {
|
|
return game_path;
|
|
}
|
|
|
|
bool checkInitDone() {
|
|
if (File("$steamcmd_path${Platform.pathSeparator}cxinit").existsSync()) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
String getSteamCmd() {
|
|
return "$steamcmd_path${Platform.pathSeparator}steamcmd${Platform.isWindows ? ".exe" : ".sh"}";
|
|
}
|
|
|
|
String getSteamCmdPath() {
|
|
return steamcmd_path;
|
|
}
|
|
|
|
String getModPath() {
|
|
return PathHelper(pth: base_path).resolve("mods").build();
|
|
}
|
|
|
|
String getModJailPath() {
|
|
return PathHelper(pth: base_path).resolve("mods.jail").build();
|
|
}
|
|
|
|
String getWorldSnapshotFolder() {
|
|
return PathHelper(pth: base_path).resolve("backups").build();
|
|
}
|
|
|
|
String getWorldGameDB() {
|
|
var path = PathHelper(pth: getServerPath())
|
|
.resolve("ConanSandbox")
|
|
.resolve("Saved");
|
|
var pth2 = path.resolve("game.db");
|
|
if (pth2.exists()) return pth2.build();
|
|
var pth1 = path.resolve("dlc_siptah.db");
|
|
if (pth1.exists()) return pth1.build();
|
|
|
|
return pth2.build(); // Fallback to game.db
|
|
}
|
|
|
|
Future<List<String>> getWorldSnapshotFiles() async {
|
|
Directory dir = Directory(getWorldSnapshotFolder());
|
|
var lst = await dir.list().toList();
|
|
List<String> backupNames = [];
|
|
for (var file in lst) {
|
|
backupNames.add(file.path);
|
|
}
|
|
|
|
return backupNames;
|
|
}
|
|
|
|
Future<void> createModFolderIfNotExists() async {
|
|
if (Directory(getModPath()).existsSync()) {
|
|
return;
|
|
} else {
|
|
await Directory(getModPath()).create(recursive: true);
|
|
}
|
|
}
|
|
|
|
Future<void> createModJailFolderIfNotExists() async {
|
|
if (Directory(getModJailPath()).existsSync()) {
|
|
return;
|
|
} else {
|
|
await Directory(getModJailPath()).create(recursive: true);
|
|
}
|
|
}
|
|
|
|
Future<void> createBackupsFolderIfNotExists() async {
|
|
if (Directory(getWorldSnapshotFolder()).existsSync()) {
|
|
return;
|
|
} else {
|
|
await Directory(getWorldSnapshotFolder()).create(recursive: true);
|
|
}
|
|
}
|
|
|
|
bool serverInstalled() {
|
|
return File(
|
|
"${getServerPath()}${Platform.pathSeparator}ConanSandboxServer.exe")
|
|
.existsSync();
|
|
}
|
|
|
|
String getWinePrefixPath() {
|
|
return PathHelper.builder(base_path).resolve("pfx").build();
|
|
}
|
|
|
|
Future<ProcessResult> RunUpdate({bool valid = true}) {
|
|
return Process.run(getSteamCmd(), [
|
|
"+@sSteamCmdForcePlatformType",
|
|
"windows",
|
|
"+force_install_dir",
|
|
getServerPath(),
|
|
"+login",
|
|
"anonymous",
|
|
"+app_update",
|
|
"443030",
|
|
"public",
|
|
if (valid) "validate",
|
|
"+quit"
|
|
]);
|
|
}
|
|
|
|
Future<void> createServerModFolderIfNotExists() async {
|
|
Directory dir = Directory(PathHelper(pth: getServerPath())
|
|
.resolve("ConanSandbox")
|
|
.resolve("Mods")
|
|
.build());
|
|
|
|
if (await dir.exists()) {
|
|
return;
|
|
} else {
|
|
await dir.create(recursive: true);
|
|
}
|
|
}
|
|
|
|
File getModListFile() {
|
|
return File(PathHelper(pth: getServerPath())
|
|
.resolve("ConanSandbox")
|
|
.resolve("Mods")
|
|
.resolve("modlist.txt")
|
|
.build());
|
|
}
|
|
|
|
Future<void> writeOutModListFile() async {
|
|
await createServerModFolderIfNotExists();
|
|
var file = getModListFile();
|
|
|
|
List<String> paths = [];
|
|
for (Mod mod in inst!.mods) {
|
|
if (!mod.enabled) continue;
|
|
var pth = PathHelper(pth: getModPath())
|
|
.resolve("steamapps")
|
|
.resolve("workshop")
|
|
.resolve("content")
|
|
.resolve("440900")
|
|
.resolve("${mod.mod_id}")
|
|
.resolve(mod.mod_pak)
|
|
.build();
|
|
if (Platform.isWindows) {
|
|
paths.add(pth);
|
|
} else {
|
|
var rpl = []; // proton's
|
|
rpl.addAll(pth.split('/'));
|
|
rpl[0] = "Z:";
|
|
paths.add(rpl.join("\\"));
|
|
}
|
|
}
|
|
|
|
await file.writeAsString(paths.join("\n"),
|
|
flush: true, mode: FileMode.writeOnly);
|
|
}
|
|
|
|
Future<bool> sendRconCommand(String command) async {
|
|
bool wine = await requiresWine();
|
|
|
|
try {
|
|
Process.run(wine ? "/app/rcon" : "C:\\rcon.exe", [
|
|
"-H",
|
|
"127.0.0.1",
|
|
"-p",
|
|
inst!.serverSettings.RconPassword,
|
|
"-P",
|
|
"${inst!.serverSettings.RconPort}",
|
|
command
|
|
]);
|
|
return true;
|
|
} catch (E) {
|
|
// Sending rcon failed
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<void> initializeWine() async {
|
|
await runWinetrick("win10");
|
|
await runWinetrick("cmd");
|
|
await runWinetrick("vcrun2013");
|
|
await runWinetrick("vcrun2015");
|
|
await runWinetrick("vcrun2017");
|
|
await runWinetrick("vcrun2019");
|
|
await runWinetrick("vcrun2022");
|
|
await runWinetrick("andale");
|
|
await runWinetrick("allfonts");
|
|
await runWinetrick("gdiplus");
|
|
await runWinetrick("dxsdk_jun2010");
|
|
}
|
|
|
|
Future<void> initializeSteamCmd() async {
|
|
if (File(PathHelper(pth: getSteamCmdPath()).resolve("cxinit").build())
|
|
.existsSync()) {
|
|
print(
|
|
"Skipping SteamCmd and Proton initialization, already marked as ready");
|
|
return;
|
|
}
|
|
// Yes, Proceed
|
|
var x = Directory(steamcmd_path);
|
|
try {
|
|
await x.delete(recursive: true);
|
|
} catch (e) {}
|
|
|
|
await x.create(recursive: true);
|
|
|
|
Directory.current = Directory(steamcmd_path);
|
|
final dio = Dio();
|
|
|
|
Process proc;
|
|
|
|
if (Platform.isWindows) {
|
|
// Download zip file
|
|
final path = "$steamcmd_path${Platform.pathSeparator}windows.zip";
|
|
final reply = await dio.download(windows, path);
|
|
|
|
final bytes = File(path).readAsBytesSync();
|
|
final arc = ZipDecoder().decodeBytes(bytes);
|
|
|
|
print("SteamCmd downloaded. Performing initial update");
|
|
|
|
for (final file in arc) {
|
|
final name = file.name;
|
|
if (file.isFile) {
|
|
final data = file.content as List<int>;
|
|
File(name)
|
|
..createSync(recursive: true)
|
|
..writeAsBytesSync(data);
|
|
} else {
|
|
Directory(name).create(recursive: true);
|
|
}
|
|
}
|
|
|
|
Process.runSync("cmd", ["/C", "echo", "X", ">", "cxinit"],
|
|
workingDirectory: Directory.current.path);
|
|
|
|
proc = await Process.start("cmd", ["/C", "steamcmd.exe", "+quit"]);
|
|
print("Completed.");
|
|
} else {
|
|
// Download tgz file
|
|
final path = "$steamcmd_path${Platform.pathSeparator}linux.tgz";
|
|
final reply = await dio.download(linux, path);
|
|
|
|
final bytes = File(path).readAsBytesSync();
|
|
final arc = GZipDecoder().decodeBytes(bytes);
|
|
final arc2 = TarDecoder().decodeBytes(arc);
|
|
|
|
print("SteamCmd downloaded. Performing initial update");
|
|
|
|
for (final file in arc2) {
|
|
final name = file.name;
|
|
if (file.isFile) {
|
|
final data = file.content as List<int>;
|
|
File(name)
|
|
..createSync(recursive: true)
|
|
..writeAsBytesSync(data);
|
|
} else {
|
|
Directory(name).create(recursive: true);
|
|
}
|
|
}
|
|
Process.runSync("chmod", ["+x", "steamcmd.sh"]);
|
|
Process.runSync("chmod", ["+x", "linux32/steamcmd"]);
|
|
Process.runSync("touch", ["cxinit"]);
|
|
|
|
Process.runSync("./steamcmd.sh", ["+quit"]);
|
|
}
|
|
|
|
Directory.current = Directory(game_path);
|
|
}
|
|
|
|
static void Clear() {
|
|
Instance.inst = SettingsEntry();
|
|
Instance.subsys = StateMachine();
|
|
Instance.superuser =
|
|
User.make("admin", "changeMe123", UserLevel.Super_User);
|
|
Instance.server = false;
|
|
}
|
|
}
|