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 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 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> getWorldSnapshotFiles() async { Directory dir = Directory(getWorldSnapshotFolder()); var lst = await dir.list().toList(); List backupNames = []; for (var file in lst) { backupNames.add(file.path); } return backupNames; } Future createModFolderIfNotExists() async { if (Directory(getModPath()).existsSync()) { return; } else { await Directory(getModPath()).create(recursive: true); } } Future createModJailFolderIfNotExists() async { if (Directory(getModJailPath()).existsSync()) { return; } else { await Directory(getModJailPath()).create(recursive: true); } } Future 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 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 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 writeOutModListFile() async { await createServerModFolderIfNotExists(); var file = getModListFile(); List 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 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 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 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; 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; 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; } }