ConanServerManager/lib/structs/settings.dart

420 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";
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;
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("settings.dat");
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("settings.dat", 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 {
try {
Process.run("/app/rcon", [
"-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("echo", ["X", ">", "cxinit"]);
proc = await Process.start("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;
}
}