import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'package:servermanager/autorestart.dart'; import 'package:servermanager/mod.dart'; import 'package:servermanager/pathtools.dart'; import 'package:servermanager/serversettings.dart'; import 'package:servermanager/settings.dart'; import 'package:servermanager/statemachine.dart'; Future doDownloadMods(String modsFolder) 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 manifest = [ "+@sSteamCmdForcePlatformType", "windows", "+force_install_dir", modsFolder, "+login", settings.inst!.steam_creds!.username, settings.inst!.steam_creds!.password, if (settings.inst!.steam_creds!.has_2fa) code.trim() ]; for (Mod M in settings.inst!.mods) { manifest.add("+workshop_download_item"); manifest.add("440900"); manifest.add("${M.mod_id}"); } await settings.createModFolderIfNotExists(); manifest.add("+quit"); //print( // "Running command: ${settings.getSteamCmd()} ${manifest.join(" ")}"); var result = await Process.run(settings.getSteamCmd(), manifest); print(result.stdout); } Future 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> doScanMods(String modsFolder) async { Settings settings = Settings(); List ret = []; for (Mod M in settings.inst!.mods.toList()) { var index = settings.inst!.mods.indexOf(M); // Assemble final path. String modsPath = PathHelper.builder(settings.game_path) .resolve("mods") .resolve("steamapps") .resolve("workshop") .resolve("content") .resolve("440900") .resolve("${M.mod_id}") .build(); Directory dir = Directory(modsPath); await for (var entity in dir.list()) { if (entity is File && entity.path.endsWith("pak")) { String name = entity.path.split(Platform.pathSeparator).last; var content = await entity.readAsBytes(); var data = md5.convert(content); var hash = data.toString(); M.mod_pak = name; M.mod_hash = hash; print("Discovered mod file: ${name}"); print("Hash: ${hash}"); // Update the mod instance, and retain the original modlist order ret.add(Mod( mod_hash: hash, mod_id: M.mod_id, mod_pak: name, mod_name: M.mod_name)); } } } return ret; } class GameServerPage extends StatefulWidget { Settings settings; GameServerPage({super.key, required this.settings}); @override GameServerPageState createState() => GameServerPageState(settings: settings); } class GameServerPageState extends State { Settings settings; GameServerPageState({required this.settings}); var downloading = false; late Stream> download_stream; TextEditingController ValueControl = TextEditingController(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Conan Exiles Server Manager - Game Server"), backgroundColor: Color.fromARGB(255, 100, 0, 0), ), body: WillPopScope( onWillPop: () async { if (downloading) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Wait until the download completes"))); return false; } else { return true; } }, child: SingleChildScrollView( padding: EdgeInsets.all(16), child: Column( children: [ ListTile( title: settings.serverInstalled() ? Text("Update / Validate Server Install") : Text("Initial Server Download"), subtitle: settings.serverInstalled() ? Text( "Validates game files or performs an update. This is done when starting the server as well.") : Text( "Download the game server. This is step 1, after having downloaded steamcmd."), leading: Icon(Icons.numbers), onTap: () async { if (downloading) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("Wait until the download completes"))); return; } if (!settings.isValid()) return; Directory(settings.getServerPath()).createSync(); setState(() { downloading = true; }); // Start server download into folder await settings.RunUpdate(); setState(() { downloading = false; }); }, ), if (downloading) ListTile( title: Text("Downloading..."), leading: Icon(Icons.downloading), ), 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"); }, ), if (settings.inst!.downloadMods) ListTile( title: Text("Download Mods"), subtitle: Text("Downloads the mods"), leading: Icon(Icons.download_sharp), onTap: () async { setState(() { downloading = true; }); await doDownloadMods(settings.getModPath()); setState(() { downloading = false; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Scanning mods..."))); var mods = await doScanMods(settings.getModPath()); setState(() { settings.inst!.mods = mods; settings.Write(); }); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Mod scanning complete"))); }, ), if (!settings.inst!.downloadMods) ListTile( title: Text("Conan Exiles Install Path"), subtitle: Text("Set the Steam Library location of Conan Exiles"), leading: Icon(Icons.folder), onTap: () async { // Open the folder select prompt var path = await getDirectoryPath(); setState(() { settings.inst!.conanExilesLibraryPath = path ?? settings.inst!.conanExilesLibraryPath; if (path == null) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( "You must select a valid SteamLibrary folder"))); } }); }, ), ListTile( title: Text("Configure AutoRestart"), leading: Icon(Icons.timer), onTap: () async { if (downloading) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("Wait until the download completes"))); return; } var reply = await Navigator.pushNamed( context, "/server/autorestart", arguments: settings.inst!.timer); 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); }); }, ), ], )), ), ); } } class ModManager extends StatefulWidget { Settings settings; ModManager({super.key, required this.settings}); @override ModManagerState createState() => ModManagerState(settings: settings); } class ModManagerState extends State { Settings settings; ModManagerState({required this.settings}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( 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; } 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)); if (reply != null) { setState(() { settings.inst!.mods[idx] = reply as Mod; }); } else { setState(() { settings.inst!.mods.removeAt(idx); }); } }, ), ); }, itemCount: settings.inst!.mods.length, ), onWillPop: () async { Navigator.pop(context); return true; }, ), floatingActionButton: ElevatedButton( child: Icon(Icons.add), onPressed: () async { // Open new mod info screen final reply = await Navigator.pushNamed(context, "/server/mods/edit", arguments: Mod(newMod: true)); if (reply != null) { Mod mod = reply as Mod; setState(() { settings.inst!.mods.add(mod); settings.Write(); }); } }, ), ); } } class ModPage extends StatelessWidget { bool initDone = false; TextEditingController id = TextEditingController(); TextEditingController name = TextEditingController(); String instance = ""; bool isNewMod = false; bool willDelete = false; String pak = "Not initialized"; String hash = ""; @override Widget build(BuildContext context) { final args = ModalRoute.of(context)!.settings.arguments as Mod?; if (!initDone) { initDone = true; if (args != null) { id.text = args.mod_id.toString(); name.text = args.mod_name; isNewMod = args.newMod; instance = args.mod_instance_id(); pak = args.mod_pak; hash = args.mod_hash; } } return Scaffold( appBar: AppBar( 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 { int idVal = 0; try { idVal = int.parse(id.text); } catch (E) {} if (willDelete) { Navigator.pop(context, null); } else { Navigator.pop(context, Mod(mod_id: idVal, mod_name: name.text, newMod: false)); } return true; }, ), ); } }