Darktide Mod Manager #39
6 changed files with 91 additions and 75 deletions
|
@ -14,9 +14,9 @@ use sdk::filetype::lua;
|
||||||
use sdk::filetype::package::Package;
|
use sdk::filetype::package::Package;
|
||||||
use sdk::murmur::Murmur64;
|
use sdk::murmur::Murmur64;
|
||||||
use sdk::{
|
use sdk::{
|
||||||
Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ToBinary,
|
Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ModConfig,
|
||||||
|
ToBinary,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::{fs, try_join};
|
use tokio::{fs, try_join};
|
||||||
use tracing::Instrument;
|
use tracing::Instrument;
|
||||||
|
@ -176,7 +176,10 @@ async fn build_bundles(state: Arc<State>) -> Result<()> {
|
||||||
|
|
||||||
bundle.add_file(file);
|
bundle.add_file(file);
|
||||||
|
|
||||||
let src = mod_dir.join(pkg_info.get_name());
|
let bundle_name = Murmur64::hash(pkg_info.get_name())
|
||||||
|
.to_string()
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
let src = mod_dir.join(&bundle_name);
|
||||||
let dest = bundle_dir.clone();
|
let dest = bundle_dir.clone();
|
||||||
let pkg_name = pkg_info.get_name().clone();
|
let pkg_name = pkg_info.get_name().clone();
|
||||||
let mod_name = mod_info.get_name().clone();
|
let mod_name = mod_info.get_name().clone();
|
||||||
|
@ -200,7 +203,7 @@ async fn build_bundles(state: Arc<State>) -> Result<()> {
|
||||||
dest.display()
|
dest.display()
|
||||||
);
|
);
|
||||||
fs::hard_link(&src, dest.as_ref()).await.wrap_err_with(|| {
|
fs::hard_link(&src, dest.as_ref()).await.wrap_err_with(|| {
|
||||||
format!("failed to hard link bundle {pkg_name} for mod {mod_name}")
|
format!("failed to hard link bundle {pkg_name} for mod {mod_name}. src: {}, dest: {}", src.display(), dest.display())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.instrument(span);
|
.instrument(span);
|
||||||
|
@ -376,13 +379,6 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
|
||||||
struct ModConfig {
|
|
||||||
name: String,
|
|
||||||
#[serde(default)]
|
|
||||||
description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(state))]
|
#[tracing::instrument(skip(state))]
|
||||||
pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo> {
|
pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo> {
|
||||||
let data = fs::read(&info.path)
|
let data = fs::read(&info.path)
|
||||||
|
@ -462,7 +458,7 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result<ModInfo>
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(name, files)| PackageInfo::new(name, files.into_iter().collect()))
|
.map(|(name, files)| PackageInfo::new(name, files.into_iter().collect()))
|
||||||
.collect();
|
.collect();
|
||||||
let info = ModInfo::new(mod_cfg.name, mod_cfg.description, packages);
|
let info = ModInfo::new(mod_cfg, packages);
|
||||||
|
|
||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ use druid::{
|
||||||
AppDelegate, Command, Data, DelegateCtx, Env, FileInfo, Handled, Lens, Selector, SingleUse,
|
AppDelegate, Command, Data, DelegateCtx, Env, FileInfo, Handled, Lens, Selector, SingleUse,
|
||||||
Target,
|
Target,
|
||||||
};
|
};
|
||||||
|
use sdk::ModConfig;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
@ -58,6 +59,27 @@ impl PackageInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Data, Debug)]
|
||||||
|
pub(crate) struct ModResourceInfo {
|
||||||
|
init: String,
|
||||||
|
data: String,
|
||||||
|
localization: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModResourceInfo {
|
||||||
|
pub(crate) fn get_init(&self) -> &String {
|
||||||
|
&self.init
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_data(&self) -> &String {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_localization(&self) -> &String {
|
||||||
|
&self.localization
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Data, Debug, Lens)]
|
#[derive(Clone, Data, Debug, Lens)]
|
||||||
pub(crate) struct ModInfo {
|
pub(crate) struct ModInfo {
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -65,15 +87,22 @@ pub(crate) struct ModInfo {
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
#[lens(ignore)]
|
#[lens(ignore)]
|
||||||
packages: Vector<PackageInfo>,
|
packages: Vector<PackageInfo>,
|
||||||
|
#[lens(ignore)]
|
||||||
|
resources: ModResourceInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModInfo {
|
impl ModInfo {
|
||||||
pub fn new(name: String, description: String, packages: Vector<PackageInfo>) -> Self {
|
pub fn new(cfg: ModConfig, packages: Vector<PackageInfo>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name,
|
name: cfg.name,
|
||||||
description: Arc::new(description),
|
description: Arc::new(cfg.description),
|
||||||
packages,
|
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
packages,
|
||||||
|
resources: ModResourceInfo {
|
||||||
|
init: cfg.resources.init,
|
||||||
|
data: cfg.resources.data,
|
||||||
|
localization: cfg.resources.localization,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,6 +113,14 @@ impl ModInfo {
|
||||||
pub(crate) fn get_name(&self) -> &String {
|
pub(crate) fn get_name(&self) -> &String {
|
||||||
&self.name
|
&self.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_enabled(&self) -> bool {
|
||||||
|
self.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_resources(&self) -> &ModResourceInfo {
|
||||||
|
&self.resources
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for ModInfo {
|
impl PartialEq for ModInfo {
|
||||||
|
|
|
@ -7,8 +7,7 @@ use color_eyre::{Help, Report};
|
||||||
use futures::future::try_join_all;
|
use futures::future::try_join_all;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use sdk::filetype::package::Package;
|
use sdk::filetype::package::Package;
|
||||||
use sdk::{Bundle, BundleFile};
|
use sdk::{Bundle, BundleFile, ModConfig};
|
||||||
use serde::Deserialize;
|
|
||||||
use tokio::fs::{self, File};
|
use tokio::fs::{self, File};
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
@ -36,16 +35,8 @@ pub(crate) fn command_definition() -> Command {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
|
||||||
struct ProjectConfig {
|
|
||||||
#[serde(skip)]
|
|
||||||
dir: PathBuf,
|
|
||||||
name: String,
|
|
||||||
packages: Vec<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
async fn find_project_config(dir: Option<PathBuf>) -> Result<ProjectConfig> {
|
async fn find_project_config(dir: Option<PathBuf>) -> Result<ModConfig> {
|
||||||
let (path, mut file) = if let Some(path) = dir {
|
let (path, mut file) = if let Some(path) = dir {
|
||||||
let file = File::open(&path.join(PROJECT_CONFIG_NAME))
|
let file = File::open(&path.join(PROJECT_CONFIG_NAME))
|
||||||
.await
|
.await
|
||||||
|
@ -83,7 +74,7 @@ async fn find_project_config(dir: Option<PathBuf>) -> Result<ProjectConfig> {
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
file.read_to_string(&mut buf).await?;
|
file.read_to_string(&mut buf).await?;
|
||||||
|
|
||||||
let mut cfg: ProjectConfig = serde_sjson::from_str(&buf)?;
|
let mut cfg: ModConfig = serde_sjson::from_str(&buf)?;
|
||||||
cfg.dir = path;
|
cfg.dir = path;
|
||||||
Ok(cfg)
|
Ok(cfg)
|
||||||
}
|
}
|
||||||
|
@ -210,23 +201,23 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let bundles = try_join_all(tasks).await?;
|
let bundles = try_join_all(tasks)
|
||||||
|
.await
|
||||||
|
.wrap_err("failed to build mod bundles")?;
|
||||||
|
|
||||||
let mod_file = {
|
let config_file = {
|
||||||
let mut path = cfg.dir.join(&cfg.name);
|
let path = cfg.dir.join("dtmt.cfg");
|
||||||
path.set_extension("mod");
|
fs::read(&path)
|
||||||
fs::read(path).await?
|
.await
|
||||||
|
.wrap_err_with(|| format!("failed to read mod config at {}", path.display()))?
|
||||||
};
|
};
|
||||||
|
|
||||||
let config_file = fs::read(cfg.dir.join("dtmt.cfg")).await?;
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let dest = dest.clone();
|
let dest = dest.clone();
|
||||||
let name = cfg.name.clone();
|
let name = cfg.name.clone();
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let mut archive = Archive::new(name);
|
let mut archive = Archive::new(name);
|
||||||
|
|
||||||
archive.add_mod_file(mod_file);
|
|
||||||
archive.add_config(config_file);
|
archive.add_config(config_file);
|
||||||
|
|
||||||
for bundle in bundles {
|
for bundle in bundles {
|
||||||
|
|
|
@ -8,13 +8,19 @@ use futures::{StreamExt, TryStreamExt};
|
||||||
use string_template::Template;
|
use string_template::Template;
|
||||||
use tokio::fs::{self, DirBuilder};
|
use tokio::fs::{self, DirBuilder};
|
||||||
|
|
||||||
const TEMPLATES: [(&str, &str); 6] = [
|
const TEMPLATES: [(&str, &str); 5] = [
|
||||||
(
|
(
|
||||||
"dtmt.cfg",
|
"dtmt.cfg",
|
||||||
r#"name = "{{name}}"
|
r#"name = "{{name}}"
|
||||||
description = "An elaborate description of my cool game mod!"
|
description = "An elaborate description of my cool game mod!"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
|
resources = {
|
||||||
|
script = "scripts/mods/{{name}}/init"
|
||||||
|
data = "scripts/mods/{{name}}/data"
|
||||||
|
localization = "scripts/mods/{{name}}/locationzation"
|
||||||
|
}
|
||||||
|
|
||||||
packages = [
|
packages = [
|
||||||
"packages/{{name}}"
|
"packages/{{name}}"
|
||||||
]
|
]
|
||||||
|
@ -23,21 +29,6 @@ depends = [
|
||||||
"dmf"
|
"dmf"
|
||||||
]
|
]
|
||||||
"#,
|
"#,
|
||||||
),
|
|
||||||
(
|
|
||||||
"{{name}}.mod",
|
|
||||||
r#"return {
|
|
||||||
run = function()
|
|
||||||
fassert(rawget(_G, "new_mod"), "`{{title}}` encountered an error loading the Darktide Mod Framework.")
|
|
||||||
|
|
||||||
new_mod("{{name}}", {
|
|
||||||
mod_script = "scripts/mods/{{name}}/{{name}}",
|
|
||||||
mod_data = "scripts/mods/{{name}}/{{name}}_data",
|
|
||||||
mod_localization = "scripts/mods/{{name}}/{{name}}_localization",
|
|
||||||
})
|
|
||||||
end,
|
|
||||||
packages = {},
|
|
||||||
}"#,
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"packages/{{name}}.package",
|
"packages/{{name}}.package",
|
||||||
|
@ -47,7 +38,7 @@ depends = [
|
||||||
"#,
|
"#,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"scripts/mods/{{name}}/{{name}}.lua",
|
"scripts/mods/{{name}}/init.lua",
|
||||||
r#"local mod = get_mod("{{name}}")
|
r#"local mod = get_mod("{{name}}")
|
||||||
|
|
||||||
-- Your mod code goes here.
|
-- Your mod code goes here.
|
||||||
|
@ -55,7 +46,7 @@ depends = [
|
||||||
"#,
|
"#,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"scripts/mods/{{name}}/{{name}}_data.lua",
|
"scripts/mods/{{name}}/data.lua",
|
||||||
r#"local mod = get_mod("{{name}}")
|
r#"local mod = get_mod("{{name}}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -65,7 +56,7 @@ return {
|
||||||
}"#,
|
}"#,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"scripts/mods/{{name}}/{{name}}_localization.lua",
|
"scripts/mods/{{name}}/localization.lua",
|
||||||
r#"return {
|
r#"return {
|
||||||
mod_description = {
|
mod_description = {
|
||||||
en = "An elaborate description of my cool game mod!",
|
en = "An elaborate description of my cool game mod!",
|
||||||
|
@ -127,8 +118,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()>
|
||||||
promptly::prompt_default("The mod identifier name", default)?
|
promptly::prompt_default("The mod identifier name", default)?
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::debug!(root = %root.display());
|
tracing::debug!(root = %root.display(), title, name);
|
||||||
tracing::debug!(title, name);
|
|
||||||
|
|
||||||
let mut data = HashMap::new();
|
let mut data = HashMap::new();
|
||||||
data.insert("name", name.as_str());
|
data.insert("name", name.as_str());
|
||||||
|
|
|
@ -12,7 +12,6 @@ use zip::ZipWriter;
|
||||||
pub struct Archive {
|
pub struct Archive {
|
||||||
name: String,
|
name: String,
|
||||||
bundles: Vec<Bundle>,
|
bundles: Vec<Bundle>,
|
||||||
mod_file: Option<Vec<u8>>,
|
|
||||||
config_file: Option<Vec<u8>>,
|
config_file: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +20,6 @@ impl Archive {
|
||||||
Self {
|
Self {
|
||||||
name,
|
name,
|
||||||
bundles: Vec::new(),
|
bundles: Vec::new(),
|
||||||
mod_file: None,
|
|
||||||
config_file: None,
|
config_file: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,10 +28,6 @@ impl Archive {
|
||||||
self.bundles.push(bundle)
|
self.bundles.push(bundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_mod_file(&mut self, content: Vec<u8>) {
|
|
||||||
self.mod_file = Some(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_config(&mut self, content: Vec<u8>) {
|
pub fn add_config(&mut self, content: Vec<u8>) {
|
||||||
self.config_file = Some(content);
|
self.config_file = Some(content);
|
||||||
}
|
}
|
||||||
|
@ -42,11 +36,6 @@ impl Archive {
|
||||||
where
|
where
|
||||||
P: AsRef<Path>,
|
P: AsRef<Path>,
|
||||||
{
|
{
|
||||||
let mod_file = self
|
|
||||||
.mod_file
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| eyre::eyre!("Mod file is missing in mod archive"))?;
|
|
||||||
|
|
||||||
let config_file = self
|
let config_file = self
|
||||||
.config_file
|
.config_file
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -64,13 +53,6 @@ impl Archive {
|
||||||
|
|
||||||
let base_path = PathBuf::from(&self.name);
|
let base_path = PathBuf::from(&self.name);
|
||||||
|
|
||||||
{
|
|
||||||
let mut name = base_path.join(&self.name);
|
|
||||||
name.set_extension("mod");
|
|
||||||
zip.start_file(name.to_string_lossy(), Default::default())?;
|
|
||||||
zip.write_all(mod_file)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let name = base_path.join("dtmt.cfg");
|
let name = base_path.join("dtmt.cfg");
|
||||||
zip.start_file(name.to_string_lossy(), Default::default())?;
|
zip.start_file(name.to_string_lossy(), Default::default())?;
|
||||||
|
|
|
@ -9,3 +9,23 @@ pub use bundle::database::BundleDatabase;
|
||||||
pub use bundle::decompress;
|
pub use bundle::decompress;
|
||||||
pub use bundle::{Bundle, BundleFile, BundleFileType, BundleFileVariant};
|
pub use bundle::{Bundle, BundleFile, BundleFileType, BundleFileVariant};
|
||||||
pub use context::Context;
|
pub use context::Context;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, serde::Deserialize)]
|
||||||
|
pub struct ModConfigResources {
|
||||||
|
pub init: String,
|
||||||
|
pub data: String,
|
||||||
|
pub localization: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, serde::Deserialize)]
|
||||||
|
pub struct ModConfig {
|
||||||
|
#[serde(skip)]
|
||||||
|
pub dir: std::path::PathBuf,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub version: String,
|
||||||
|
pub packages: Vec<std::path::PathBuf>,
|
||||||
|
pub resources: ModConfigResources,
|
||||||
|
#[serde(default)]
|
||||||
|
pub depends: Vec<String>,
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue