From 73c1ecb7f20f927c94a87ab57859071682dd5e01 Mon Sep 17 00:00:00 2001 From: spuds <71292624+bananaturtlesandwich@users.noreply.github.com> Date: Mon, 28 Aug 2023 14:44:14 +0100 Subject: [PATCH] switch to writing completely in memory --- Cargo.toml | 3 +- src/io.rs | 45 ++++++----- src/lib.rs | 7 +- src/map.rs | 11 ++- src/map/transplant.rs | 5 +- src/writing.rs | 168 +++++++++++++++++++-------------------- src/writing/cutscenes.rs | 14 ++-- src/writing/overworld.rs | 37 +++++---- src/writing/savegames.rs | 10 ++- src/writing/specific.rs | 70 ++++++++-------- 10 files changed, 190 insertions(+), 180 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 24c4d28..ae5cab8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,9 +8,8 @@ edition = "2021" [dependencies] eframe = { version = "0.22", default-features = false, features = ["glow", "persistence"] } -unreal_asset = { git = "https://github.com/astrotechies/unrealmodding", package = "unreal_asset", rev = "dfcc3e2"} +unreal_asset = { git = "https://github.com/astrotechies/unrealmodding", package = "unreal_asset", rev = "186842c"} repak = { git = "https://github.com/bananaturtlesandwich/repak", branch = "features-redo", default-features = false } -walkdir = "2.3" strum = { version = "0.25", features = ["derive"] } rand = "0.8" egui-modal = "0.2" diff --git a/src/io.rs b/src/io.rs index ca95227..e3f7207 100644 --- a/src/io.rs +++ b/src/io.rs @@ -1,33 +1,38 @@ -use std::{fs::File, io::Cursor, path::Path}; -use unreal_asset::{engine_version::EngineVersion::VER_UE4_25, error::Error, Asset}; +use unreal_asset::{engine_version::EngineVersion::VER_UE4_25, Asset}; -pub fn open(file: impl AsRef) -> Result, Error> { - Asset::new( - File::open(&file)?, - File::open(file.as_ref().with_extension("uexp")).ok(), +pub fn open(asset: Vec, bulk: Vec) -> Result>, crate::writing::Error> { + Ok(Asset::new( + std::io::Cursor::new(asset), + Some(std::io::Cursor::new(bulk)), VER_UE4_25, None, - ) + )?) } -pub fn open_from_bytes<'chain>( +pub fn open_slice<'chain>( asset: &'chain [u8], bulk: &'chain [u8], -) -> Result>, Error> { - Asset::new( - Cursor::new(asset), - Some(Cursor::new(bulk)), +) -> Result, crate::writing::Error> { + Ok(Asset::new( + std::io::Cursor::new(asset), + Some(std::io::Cursor::new(bulk)), VER_UE4_25, None, - ) + )?) } pub fn save( - asset: &mut Asset, - path: impl AsRef, -) -> Result<(), Error> { - asset.write_data( - &mut File::create(&path)?, - Some(&mut File::create(path.as_ref().with_extension("uexp"))?), - ) + map: &mut Asset, + mod_pak: &super::Mod, + path: &str, +) -> Result<(), crate::writing::Error> { + let mut asset = std::io::Cursor::new(vec![]); + let mut bulk = std::io::Cursor::new(vec![]); + map.write_data(&mut asset, Some(&mut bulk))?; + mod_pak.lock()?.write_file(path, &mut asset)?; + mod_pak.lock()?.write_file( + &path.replace(".uasset", ".uexp").replace(".umap", ".uexp"), + &mut bulk, + )?; + Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index d24bd26..1c25b81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,9 @@ mod logic; mod map; mod writing; +type Mod = std::sync::Arc>>; +type Asset = unreal_asset::Asset>; + pub struct Rando { notifs: egui_modal::Modal, pak: std::path::PathBuf, @@ -260,13 +263,11 @@ impl eframe::App for Rando { ) .clicked() { - std::fs::remove_dir_all(self.pak.join("rando_p")).unwrap_or_default(); notify!( self, logic::randomise(self), "seed has been generated, written and installed" - ); - std::fs::remove_dir_all(self.pak.join("rando_p")).unwrap_or_default(); + ) } }); self.notifs.show_dialog(); diff --git a/src/map.rs b/src/map.rs index 4588f38..8e6a1b8 100644 --- a/src/map.rs +++ b/src/map.rs @@ -53,7 +53,10 @@ fn get_actor_exports( } /// creates and assigns a unique name -fn give_unique_name(orig: &mut FName, asset: &mut Asset) { +fn give_unique_name( + orig: &mut FName, + asset: &mut Asset, +) -> Result<(), crate::writing::Error> { // for the cases where the number is unnecessary if orig @@ -61,12 +64,12 @@ fn give_unique_name(orig: &mut FName, asset: & .is_none() { *orig = orig.get_content(|name| asset.add_fname(name)); - return; + return Ok(()); } let mut name = orig.get_owned_content(); let mut counter: u16 = match name.rfind(|ch: char| ch.to_digit(10).is_none()) { Some(index) if index != name.len() - 1 => { - name.drain(index + 1..).collect::().parse().unwrap() + name.drain(index + 1..).collect::().parse()? } _ => 1, }; @@ -76,7 +79,7 @@ fn give_unique_name(orig: &mut FName, asset: & { counter += 1; } - *orig = asset.add_fname(&format!("{name}{counter}")) + Ok(*orig = asset.add_fname(&format!("{name}{counter}"))) } /// on all possible export references diff --git a/src/map/transplant.rs b/src/map/transplant.rs index 80d5235..fc1f1ac 100644 --- a/src/map/transplant.rs +++ b/src/map/transplant.rs @@ -10,14 +10,14 @@ pub fn transplant, donor: &Asset, -) { +) -> Result<(), crate::writing::Error> { let mut children = super::get_actor_exports(index, donor, recipient.asset_data.exports.len()); // make sure the actor has a unique object name super::give_unique_name( &mut children[0].get_base_export_mut().object_name, recipient, - ); + )?; let actor_ref = PackageIndex::new(recipient.asset_data.exports.len() as i32 + 1); // add the actor to persistent level @@ -132,4 +132,5 @@ pub fn transplant>), + #[error("some threads are still using writer")] + InnerArc, #[error("failed to strip prefix when writing file to pak")] Strip(#[from] std::path::StripPrefixError), + #[error("thread failed to complete")] + Thread, #[error("data was not as expected - you may have an older version of the game")] Assumption, } -pub const MOD: &str = "rando_p/Blue Fire/Content/BlueFire"; +macro_rules! stub { + ($type: ty, $variant: ident) => { + impl From<$type> for Error { + fn from(_: $type) -> Self { + Self::$variant + } + } + }; +} + +stub!( + std::sync::Arc>>, + InnerArc +); +stub!( + std::sync::PoisonError>>, + WriterPoison +); +stub!( + std::sync::PoisonError>>, + VecPoison +); +stub!(Box, Thread); + +pub const MOD: &str = "Blue Fire/Content/BlueFire/"; const SAVEGAME: &str = "Player/Logic/FrameWork/BlueFireSaveGame.uasset"; @@ -37,25 +74,20 @@ fn extract( app: &crate::Rando, pak: &repak::PakReader, path: &str, -) -> Result<(unreal_asset::Asset, std::path::PathBuf), Error> { - let loc = app.pak.join(MOD).join(path); - if path != "Maps/World/A02_ArcaneTunnels/A02_EastArcane.umap" { - std::fs::create_dir_all(loc.parent().expect("is a file"))?; - pak.read_file( +) -> Result>, Error> { + open( + pak.get( &format!("Blue Fire/Content/BlueFire/{path}"), &mut app.pak()?, - &mut std::fs::File::create(&loc)?, - )?; - pak.read_file( + )?, + pak.get( &format!( "Blue Fire/Content/BlueFire/{}", path.replace(".uasset", ".uexp").replace(".umap", ".uexp") ), &mut app.pak()?, - &mut std::fs::File::create(loc.with_extension("uexp"))?, - )?; - } - Ok((open(&loc)?, loc)) + )?, + ) } fn byte_property( @@ -110,83 +142,51 @@ fn set_byte( pub fn write(data: Data, app: &crate::Rando) -> Result<(), Error> { let mut sync = app.pak()?; let pak = repak::PakReader::new(&mut sync, repak::Version::V9)?; - // correct the shenanigans in spirit hunter - let loc = app - .pak - .join(MOD) - .join("Maps/World/A02_ArcaneTunnels/A02_EastArcane.umap"); - std::fs::create_dir_all(loc.parent().expect("is a file"))?; - pak.read_file( - "Blue Fire/Content/BlueFire/Maps/World/A02_ArcaneTunnels/A02_EastArcane.umap", - &mut sync, - &mut std::fs::File::create(&loc)?, - )?; - pak.read_file( - "Blue Fire/Content/BlueFire/Maps/World/A02_ArcaneTunnels/A02_EastArcane.uexp", - &mut sync, - &mut std::fs::File::create(loc.with_extension("uexp"))?, - )?; - let mut spirit_hunter = open(&loc)?; - spirit_hunter.asset_data.exports[440] - .get_base_export_mut() - .object_name = spirit_hunter.add_fname("Pickup_A02_SRF2"); - save(&mut spirit_hunter, &loc)?; + let mod_pak = std::sync::Arc::new(std::sync::Mutex::new(repak::PakWriter::new( + std::fs::File::create(app.pak.join("rando_p.pak"))?, + repak::Version::V9, + "../../../".to_string(), + None, + ))); std::thread::scope(|thread| -> Result<(), Error> { for thread in [ - thread.spawn(|| overworld::write(data.overworld, app, &pak)), - thread.spawn(|| cutscenes::write(data.cutscenes, app, &pak)), - thread.spawn(|| savegames::write(data.savegames, data.shop_emotes, app, &pak)), - thread.spawn(|| specific::write(data.cases, app, &pak)), + thread.spawn(|| overworld::write(data.overworld, app, &pak, &mod_pak)), + thread.spawn(|| cutscenes::write(data.cutscenes, app, &pak, &mod_pak)), + thread + .spawn(|| savegames::write(data.savegames, data.shop_emotes, app, &pak, &mod_pak)), + thread.spawn(|| specific::write(data.cases, app, &pak, &mod_pak)), ] { - thread.join().unwrap()? + thread.join()?? } Ok(()) })?; + let mut mod_pak = Mod::try_unwrap(mod_pak)?.into_inner()?; // change the logo so people know it worked - let logo = app.pak.join(MOD).join("HUD/Menu/Blue-Fire-Logo.uasset"); - std::fs::create_dir_all(logo.parent().expect("is a file"))?; - std::fs::write(&logo, include_bytes!("blueprints/logo.uasset"))?; - std::fs::write( - logo.with_extension("uexp"), - include_bytes!("blueprints/logo.uexp"), + let logo = MOD.to_string() + "HUD/Menu/Blue-Fire-Logo.uasset"; + mod_pak.write_file( + &logo, + &mut std::io::Cursor::new(include_bytes!("blueprints/logo.uasset")), )?; - let mut pak = repak::PakWriter::new( - std::fs::File::create(app.pak.join("rando_p.pak"))?, - repak::Version::V9, - "../../../".to_string(), - None, - ); - for file in walkdir::WalkDir::new(app.pak.join("rando_p")) - .into_iter() - .filter_map(|entry| entry.ok().filter(|file| file.path().is_file())) - { - pak.write_file( - &file - .path() - .strip_prefix(&app.pak.join("rando_p"))? - .to_str() - .unwrap_or_default() - .replace("\\", "/"), - &mut std::fs::File::open(file.path())?, - )? - } - pak.write_index()?; + mod_pak.write_file( + &logo.replace(".uasset", ".uexp"), + &mut std::io::Cursor::new(include_bytes!("blueprints/logo.uexp")), + )?; + mod_pak.write_index()?; Ok(()) } fn create_hook( app: &crate::Rando, pak: &repak::PakReader, - get_hook: impl Fn(&std::path::PathBuf) -> Result, Error>, + mod_pak: &Mod, + hook: &mut unreal_asset::Asset, drop: &Drop, cutscene: &str, index: usize, ) -> Result<(), Error> { - let mut loc = app.pak.join(MOD).join("Libraries"); - std::fs::create_dir_all(&loc)?; + let mut loc = MOD.to_string() + "Libraries"; let new_name = format!("{}_Hook", cutscene.split('/').last().unwrap_or_default()); - loc = loc.join(&new_name).with_extension("uasset"); - let mut hook = get_hook(&loc)?; + loc = format!("{loc}/{new_name}.uasset"); // edit the item given by the kismet bytecode in the hook let Export::FunctionExport(function_export::FunctionExport { struct_export: @@ -237,20 +237,18 @@ fn create_hook( let name = map.get_name_reference_mut(i as i32); *name = name.replace("hook", &new_name); } - save(&mut hook, loc)?; - let loc = app.pak.join(MOD).join(cutscene).with_extension("uasset"); - std::fs::create_dir_all(loc.parent().expect("is a file"))?; - pak.read_file( - &format!("Blue Fire/Content/BlueFire/{cutscene}.uasset"), - &mut app.pak()?, - &mut std::fs::File::create(&loc)?, - )?; - pak.read_file( - &format!("Blue Fire/Content/BlueFire/{cutscene}.uexp"), - &mut app.pak()?, - &mut std::fs::File::create(loc.with_extension("uexp"))?, + save(hook, mod_pak, &loc)?; + let loc = format!("{MOD}{cutscene}.uasset"); + let mut cutscene = open( + pak.get( + &format!("Blue Fire/Content/BlueFire/{cutscene}.uasset"), + &mut app.pak()?, + )?, + pak.get( + &format!("Blue Fire/Content/BlueFire/{cutscene}.uexp"), + &mut app.pak()?, + )?, )?; - let mut cutscene = open(&loc)?; let universal_refs: Vec = cutscene .get_name_map() .get_ref() @@ -265,7 +263,7 @@ fn create_hook( let name = map.get_name_reference_mut(i as i32); *name = name.replace("UniversalFunctions", &new_name); } - save(&mut cutscene, &loc)?; + save(&mut cutscene, mod_pak, &loc)?; Ok(()) } diff --git a/src/writing/cutscenes.rs b/src/writing/cutscenes.rs index daa9e59..f493a08 100644 --- a/src/writing/cutscenes.rs +++ b/src/writing/cutscenes.rs @@ -4,6 +4,7 @@ pub fn write( cutscenes: Vec, app: &crate::Rando, pak: &repak::PakReader, + mod_pak: &Mod, ) -> Result<(), Error> { std::thread::scope(|thread| -> Result<(), Error> { let mut threads = Vec::with_capacity(cutscenes.len()); @@ -15,12 +16,11 @@ pub fn write( create_hook( app, pak, - |_| { - Ok(open_from_bytes( - include_bytes!("../blueprints/hook.uasset"), - include_bytes!("../blueprints/hook.uexp"), - )?) - }, + mod_pak, + &mut open_slice( + include_bytes!("../blueprints/hook.uasset"), + include_bytes!("../blueprints/hook.uexp"), + )?, &drop, cutscene, 69, @@ -29,7 +29,7 @@ pub fn write( })); } for thread in threads { - thread.join().unwrap()?; + thread.join()??; } Ok(()) })?; diff --git a/src/writing/overworld.rs b/src/writing/overworld.rs index e7d5ba8..b308433 100644 --- a/src/writing/overworld.rs +++ b/src/writing/overworld.rs @@ -6,6 +6,7 @@ pub fn write( checks: std::collections::HashMap>, app: &crate::Rando, pak: &repak::PakReader, + mod_pak: &Mod, ) -> Result<(), Error> { // reference so it isn't moved let used = &std::sync::Arc::new(std::sync::Mutex::new(Vec::with_capacity(checks.len()))); @@ -13,7 +14,16 @@ pub fn write( for thread in checks.into_iter().map( |(location, checks)| -> Result>, Error> { Ok(thread.spawn(move || { - let (mut map, loc) = extract(app, pak, &format!("{PREFIX}{location}.umap"))?; + let mut path = format!("{PREFIX}{location}.umap"); + let mut map = extract(app, pak, &path)?; + if path.ends_with("Maps/World/A02_ArcaneTunnels/A02_EastArcane.umap"){ + // correct the shenanigans in spirit hunter + map.asset_data.exports[440] + .get_base_export_mut() + .object_name = map.add_fname("Pickup_A02_SRF2"); + + } + path = MOD.to_string() + &path; let mut name_map = map.get_name_map(); for Check { context, drop, .. } in checks { match context { @@ -26,13 +36,11 @@ pub fn write( _ => unimplemented!(), }, &mut map, - &open_from_bytes( - include_bytes!("../blueprints/collectibles.umap") - .as_slice(), - include_bytes!("../blueprints/collectibles.uexp") - .as_slice(), + &open_slice( + include_bytes!("../blueprints/collectibles.umap"), + include_bytes!("../blueprints/collectibles.uexp"), )?, - ); + )?; let mut pos = shop.location(); let (x, y) = (9.0 * index as f64).to_radians().sin_cos(); pos.x -= 1000.0 * x; @@ -88,13 +96,13 @@ pub fn write( }; let mut replace = |actor: usize| -> Result<(), Error> { // unfortunately i can't share this between threads - let donor = open_from_bytes( + let donor = open_slice( include_bytes!("../blueprints/collectibles.umap"), include_bytes!("../blueprints/collectibles.uexp"), )?; delete(i, &mut map); let insert = map.asset_data.exports.len(); - transplant(actor, &mut map, &donor); + transplant(actor, &mut map, &donor)?; let loc = get_location(i, &map); set_location( insert, @@ -128,15 +136,14 @@ pub fn write( Some(index) if index != name.len() - 1 => name .drain(index + 1..) .collect::() - .parse() - .unwrap(), + .parse()?, _ => 1, }; - while used.lock().unwrap().contains(&format!("{name}{counter}")) + while used.lock()?.contains(&format!("{name}{counter}")) { counter += 1; } - used.lock().unwrap().push(format!("{name}{counter}")); + used.lock()?.push(format!("{name}{counter}")); let norm = &mut map.asset_data.exports[insert] .get_normal_export_mut() .ok_or(Error::Assumption)?; @@ -319,12 +326,12 @@ pub fn write( } } map.rebuild_name_map(); - save(&mut map, &loc)?; + save(&mut map, mod_pak, &path)?; Ok(()) })) }, ) { - thread?.join().unwrap()? + thread?.join()?? } Ok(()) })?; diff --git a/src/writing/savegames.rs b/src/writing/savegames.rs index 43a7cd0..2e3a4d5 100644 --- a/src/writing/savegames.rs +++ b/src/writing/savegames.rs @@ -5,8 +5,12 @@ pub fn write( shop_emotes: Vec<(Shop, usize)>, app: &crate::Rando, pak: &repak::PakReader, + mod_pak: &Mod, ) -> Result<(), Error> { - let (mut savegame, savegame_loc) = extract(app, pak, SAVEGAME)?; + if checks.is_empty() && shop_emotes.is_empty() { + return Ok(()); + } + let mut savegame = extract(app, pak, SAVEGAME)?; let default = savegame.asset_data.exports[1] .get_normal_export_mut() .ok_or(Error::Assumption)?; @@ -49,7 +53,7 @@ pub fn write( } Context::Starting => { fn add_item( - savegame: &mut unreal_asset::Asset, + savegame: &mut unreal_asset::Asset>>, drop: Drop, name_map: &mut SharedResource, ) -> Result<(), Error> { @@ -212,6 +216,6 @@ pub fn write( .remove(i); } savegame.rebuild_name_map(); - save(&mut savegame, savegame_loc)?; + save(&mut savegame, mod_pak, &format!("{MOD}{SAVEGAME}"))?; Ok(()) } diff --git a/src/writing/specific.rs b/src/writing/specific.rs index 64b3928..2d2f0e6 100644 --- a/src/writing/specific.rs +++ b/src/writing/specific.rs @@ -1,6 +1,30 @@ use super::*; -pub fn write(cases: Vec, app: &crate::Rando, pak: &repak::PakReader) -> Result<(), Error> { +pub fn write( + cases: Vec, + app: &crate::Rando, + pak: &repak::PakReader, + mod_pak: &Mod, +) -> Result<(), Error> { + if cases.is_empty() { + return Ok(()); + } + let mut angels = open_slice( + include_bytes!("../blueprints/angel_hook.uasset"), + include_bytes!("../blueprints/angel_hook.uexp"), + )?; + let mut bremur = open_slice( + include_bytes!("../blueprints/bremur_hook.uasset"), + include_bytes!("../blueprints/bremur_hook.uexp"), + )?; + let mut paulale = open_slice( + include_bytes!("../blueprints/paulale_hook.uasset"), + include_bytes!("../blueprints/paulale_hook.uexp"), + )?; + let mut player = open_slice( + include_bytes!("../blueprints/player_hook.uasset"), + include_bytes!("../blueprints/player_hook.uexp"), + )?; for Check { context, drop, .. } in cases { let Context::Specific(case, index) = context else { return Err(Error::Assumption)?; @@ -8,44 +32,12 @@ pub fn write(cases: Vec, app: &crate::Rando, pak: &repak::PakReader) -> R create_hook( app, pak, - |loc| { - if !loc.exists() { - std::fs::write( - loc, - match case { - Case::Bremur => { - include_bytes!("../blueprints/bremur_hook.uasset").as_slice() - } - Case::Paulale => { - include_bytes!("../blueprints/paulale_hook.uasset").as_slice() - } - Case::Angels => { - include_bytes!("../blueprints/angel_hook.uasset").as_slice() - } - Case::AllVoids => { - include_bytes!("../blueprints/player_hook.uasset").as_slice() - } - }, - )?; - std::fs::write( - loc.with_extension("uexp"), - match case { - Case::Bremur => { - include_bytes!("../blueprints/bremur_hook.uexp").as_slice() - } - Case::Paulale => { - include_bytes!("../blueprints/paulale_hook.uexp").as_slice() - } - Case::Angels => { - include_bytes!("../blueprints/angel_hook.uexp").as_slice() - } - Case::AllVoids => { - include_bytes!("../blueprints/player_hook.uexp").as_slice() - } - }, - )?; - } - Ok(open(loc)?) + mod_pak, + match case { + Case::Bremur => &mut bremur, + Case::Paulale => &mut paulale, + Case::Angels => &mut angels, + Case::AllVoids => &mut player, }, &drop, case.as_ref(),