ConanServerManager/lib/game.dart

455 lines
15 KiB
Dart

import 'dart:io';
import 'package:crypto/crypto.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<List<Mod>> doScanMods(String modsFolder) async {
Settings settings = Settings();
List<Mod> 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<GameServerPage> {
Settings settings;
GameServerPageState({required this.settings});
var downloading = false;
late Stream<List<int>> 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");
},
),
ListTile(
title: Text("Check Mods"),
subtitle: Text("Checks the local mod copies against Steam"),
leading: Icon(Icons.download_sharp),
onTap: () async {
setState(() {
downloading = true;
});
// TODO: Insert the copy function from the configured mod location
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")));
},
),
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;
});
},
),
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<ModManager> {
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;
},
),
);
}
}