├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── about.hbs ├── about.toml ├── astro_mod_integrator ├── Cargo.toml ├── assets │ ├── ActorTemplate.uasset │ ├── ActorTemplate.uexp │ └── alert_mod │ │ ├── NotificationActor.uasset │ │ └── NotificationActor.uexp ├── baked │ └── 800-CoreMod-0.1.0_P │ │ ├── Astro │ │ └── Content │ │ │ └── Integrator │ │ │ ├── NotificationActor.uasset │ │ │ └── NotificationActor.uexp │ │ └── metadata.json ├── build.rs └── src │ ├── assets.rs │ ├── baked.rs │ ├── handlers │ ├── biome_placement_modifiers.rs │ ├── item_list_entries.rs │ ├── linked_actor_components.rs │ ├── mission_trailheads.rs │ └── mod.rs │ └── lib.rs └── astro_modloader ├── Cargo.toml ├── README.md ├── assets └── icon.ico ├── build.rs └── src ├── logging.rs └── main.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the workflow will run 4 | on: 5 | pull_request: 6 | branches: ["main"] 7 | push: 8 | branches: ["main"] 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - uses: actions/checkout@v3 24 | - uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: stable 27 | components: clippy 28 | override: true 29 | - uses: Swatinem/rust-cache@v2 30 | 31 | - name: Install cargo-about 32 | run: cargo install --locked cargo-about 33 | 34 | # - uses: actions-rs/cargo@v1 35 | # name: Unit test 36 | # env: 37 | # USE_PREBUILT_ASSETS: 1 38 | # USE_PRECOMPILED_CPP_LOADER: 1 39 | # with: 40 | # token: ${{ secrets.GITHUB_TOKEN }} 41 | # command: test 42 | 43 | - uses: actions-rs/clippy-check@v1 44 | name: Clippy check 45 | env: 46 | USE_PREBUILT_ASSETS: 1 47 | USE_PRECOMPILED_CPP_LOADER: 1 48 | with: 49 | token: ${{ secrets.GITHUB_TOKEN }} 50 | args: -- -D warnings 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /**/target 2 | Cargo.lock 3 | modloader_log.txt 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["astro_mod_integrator", "astro_modloader"] 3 | 4 | [workspace.package] 5 | version = "0.1.14" 6 | authors = ["AstroTechies, localcc, konsti219"] 7 | description = "Astroneer Modloader" 8 | 9 | [workspace.dependencies] 10 | astro_mod_integrator = { path = "./astro_mod_integrator" } 11 | 12 | unreal_mod_manager = { rev = "c636ad4e30c0f048f931d03a803d233c06dbf5d8", git = "https://github.com/AstroTechies/unrealmodding", features = [ 13 | "ue4_23", 14 | "cpp_loader", 15 | ] } 16 | 17 | lazy_static = "1.4.0" 18 | log = "0.4.17" 19 | regex = "1.7.3" 20 | serde_json = "1.0.94" 21 | serde = { version = "1.0.158", features = ["derive"] } 22 | 23 | [profile.release] 24 | lto = true 25 | codegen-units = 1 26 | strip = true 27 | 28 | [patch.crates-io] 29 | steamy-vdf = { git = "https://github.com/icewind1991/steamy" } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Astroneer Modloader 2 | 3 | A modloader for Astroneer, rewritten in Rust. 4 | 5 | ## Installation 6 | 7 | ### Windows 8 | 9 | Download the modloader (`astro_modloader.exe`) from the [releases 10 | page](https://github.com/AstroTechies/astro_modloader/releases/latest), below the changelog. 11 | 12 | ### Linux 13 | 14 | Pre-built binaries are not yet dsitributed for Linux. To build the modloader yourself on Linux: 15 | 16 | - Use your distribution's package manager to install `rustup` and `git`, 17 | - Use `rustup` to install the Rust programming language, 18 | - If `rustup` didn't install it for you, install `build-essential` or your distro's equivalent, 19 | - Use `cargo` to install `cargo-about`, 20 | - Use `git` to clone the modloader's repository, 21 | - Then run the following commands in the root of the repository: 22 | 23 | ``` 24 | export USE_PRECOMPILED_CPP_LOADER=1 25 | export USE_PREBUILT_ASSETS=1 26 | cargo build --release 27 | ``` 28 | 29 | The last command may take a while to run. Once it's done, the executable (`astro_modloader`) will be 30 | in `target/release`. 31 | -------------------------------------------------------------------------------- /about.hbs: -------------------------------------------------------------------------------- 1 | {{#each licenses}} 2 | # {{name}} 3 | 4 | ## Used by: 5 | 6 | {{#each used_by}} 7 | * [{{crate.name}} {{crate.version}}]({{#if crate.repository}} {{crate.repository}} {{else}} {{crate.name}} https://crates.io/crates/{{crate.name}} {{/if}}) 8 | {{/each}} 9 | 10 | ## License Text: 11 | 12 | {{text}} 13 | 14 | {{/each}} 15 | -------------------------------------------------------------------------------- /about.toml: -------------------------------------------------------------------------------- 1 | accepted = [ 2 | "Apache-2.0", 3 | "MIT", 4 | "Unicode-DFS-2016", 5 | "WTFPL", 6 | "OFL-1.1", 7 | "LicenseRef-UFL-1.0", 8 | "Zlib", 9 | "BSD-2-Clause", 10 | "BSL-1.0", 11 | "CC0-1.0", 12 | "MPL-2.0", 13 | "BSD-3-Clause", 14 | "ISC", 15 | "Unicode-3.0" 16 | ] 17 | 18 | [unreal_asset.clarify] 19 | license = "MIT" 20 | 21 | [[unreal_asset.clarify.files]] 22 | path = 'LICENSE' 23 | license = 'MIT' 24 | checksum = 'C4F0D7437AB4D3EC6F8C1F71FD279A86EE05CC1486CEE306B1B7890ED76C03C4' 25 | 26 | [[unreal_asset.clarify.files]] 27 | path = 'licenses/LICENSE.UAssetAPI' 28 | license = 'MIT' 29 | checksum = 'C0FC201CF231E76249104762E4C5B681068C1E7F1917B2A0DC0222E4E1B76067' 30 | -------------------------------------------------------------------------------- /astro_mod_integrator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "astro_mod_integrator" 3 | version.workspace = true 4 | authors.workspace = true 5 | description = "Integration for Astroneer Modloader" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | unreal_mod_manager.workspace = true 10 | 11 | lazy_static.workspace = true 12 | log.workspace = true 13 | regex.workspace = true 14 | serde.workspace = true 15 | serde_json.workspace = true 16 | uuid = { version = "1.3.0", features = ["v4", "fast-rng"] } 17 | 18 | [build-dependencies] 19 | unreal_mod_manager.workspace = true 20 | walkdir = "2.3.3" 21 | -------------------------------------------------------------------------------- /astro_mod_integrator/assets/ActorTemplate.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroTechies/astro_modloader/289812a5f519ec00e950184c63f0106eaa1fb012/astro_mod_integrator/assets/ActorTemplate.uasset -------------------------------------------------------------------------------- /astro_mod_integrator/assets/ActorTemplate.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroTechies/astro_modloader/289812a5f519ec00e950184c63f0106eaa1fb012/astro_mod_integrator/assets/ActorTemplate.uexp -------------------------------------------------------------------------------- /astro_mod_integrator/assets/alert_mod/NotificationActor.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroTechies/astro_modloader/289812a5f519ec00e950184c63f0106eaa1fb012/astro_mod_integrator/assets/alert_mod/NotificationActor.uasset -------------------------------------------------------------------------------- /astro_mod_integrator/assets/alert_mod/NotificationActor.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroTechies/astro_modloader/289812a5f519ec00e950184c63f0106eaa1fb012/astro_mod_integrator/assets/alert_mod/NotificationActor.uexp -------------------------------------------------------------------------------- /astro_mod_integrator/baked/800-CoreMod-0.1.0_P/Astro/Content/Integrator/NotificationActor.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroTechies/astro_modloader/289812a5f519ec00e950184c63f0106eaa1fb012/astro_mod_integrator/baked/800-CoreMod-0.1.0_P/Astro/Content/Integrator/NotificationActor.uasset -------------------------------------------------------------------------------- /astro_mod_integrator/baked/800-CoreMod-0.1.0_P/Astro/Content/Integrator/NotificationActor.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroTechies/astro_modloader/289812a5f519ec00e950184c63f0106eaa1fb012/astro_mod_integrator/baked/800-CoreMod-0.1.0_P/Astro/Content/Integrator/NotificationActor.uexp -------------------------------------------------------------------------------- /astro_mod_integrator/baked/800-CoreMod-0.1.0_P/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": 2, 3 | "name": "CoreMod", 4 | "mod_id": "CoreMod", 5 | "version": "0.1.0", 6 | "author": "AstroTechies", 7 | "integrator": { 8 | "persistent_actors": ["/Game/Integrator/NotificationActor"], 9 | "persistent_actor_maps": [ 10 | "Astro/Content/Maps/Staging_T2.umap", 11 | "Astro/Content/Maps/Staging_T2_PackedPlanets_Switch.umap", 12 | "Astro/Content/U32_Expansion/U32_Expansion.umap" 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /astro_mod_integrator/build.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | error::Error, 4 | fs::{self, OpenOptions}, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | use walkdir::WalkDir; 9 | 10 | use unreal_mod_manager::unreal_pak::{pakversion::PakVersion, PakWriter}; 11 | 12 | fn add_extension(path: &mut PathBuf, extension: &str) { 13 | match path.extension() { 14 | Some(existing_extension) => { 15 | let mut os_str = existing_extension.to_os_string(); 16 | os_str.push("."); 17 | os_str.push(extension); 18 | path.set_extension(os_str); 19 | } 20 | None => { 21 | path.set_extension(extension); 22 | } 23 | } 24 | } 25 | 26 | fn main() -> Result<(), Box> { 27 | println!("cargo:rerun-if-changed=baked"); 28 | 29 | let baked_dir = fs::read_dir("baked")?; 30 | let out_dir = env::var("OUT_DIR")?; 31 | let out_dir = Path::new(&out_dir).join("baked"); 32 | 33 | fs::create_dir_all(&out_dir)?; 34 | 35 | for path in baked_dir 36 | .filter_map(|e| e.ok()) 37 | .filter(|e| e.file_type().unwrap().is_dir()) 38 | { 39 | let path = path.path(); 40 | let mut pak_path = out_dir.join(path.file_name().unwrap()); 41 | add_extension(&mut pak_path, "pak"); 42 | 43 | OpenOptions::new() 44 | .create(true) 45 | .write(true) 46 | .truncate(true) 47 | .open(&pak_path)?; 48 | 49 | let file = OpenOptions::new().append(true).open(pak_path)?; 50 | 51 | let mut pak = PakWriter::new(&file, PakVersion::FnameBasedCompressionMethod); 52 | 53 | for entry in WalkDir::new(&path).into_iter().map(|e| e.unwrap()) { 54 | if entry.file_type().is_file() { 55 | let rel_path = entry.path().strip_prefix(&path).unwrap(); 56 | let record_name = rel_path.to_str().unwrap().replace('\\', "/"); 57 | 58 | pak.write_entry(&record_name, &fs::read(entry.path()).unwrap(), true)?; 59 | } 60 | } 61 | 62 | pak.finish_write()?; 63 | } 64 | 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /astro_mod_integrator/src/assets.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const ACTOR_TEMPLATE_ASSET: &[u8] = include_bytes!("../assets/ActorTemplate.uasset"); 2 | 3 | pub(crate) const ACTOR_TEMPLATE_EXPORT: &[u8] = include_bytes!("../assets/ActorTemplate.uexp"); 4 | 5 | pub(crate) const ALERT_MOD_NOTIFICATION_ACTOR_ASSET: &[u8] = 6 | include_bytes!("../assets/alert_mod/NotificationActor.uasset"); 7 | 8 | pub(crate) const ALERT_MOD_NOTIFICATION_ACTOR_EXPORT: &[u8] = 9 | include_bytes!("../assets/alert_mod/NotificationActor.uexp"); 10 | -------------------------------------------------------------------------------- /astro_mod_integrator/src/baked.rs: -------------------------------------------------------------------------------- 1 | macro_rules! baked_path { 2 | () => { 3 | concat!(env!("OUT_DIR"), "/baked/") 4 | }; 5 | } 6 | 7 | pub(crate) const CORE_MOD: &[u8] = 8 | include_bytes!(concat!(baked_path!(), "800-CoreMod-0.1.0_P.pak")); 9 | -------------------------------------------------------------------------------- /astro_mod_integrator/src/handlers/biome_placement_modifiers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs::File; 3 | use std::io::{self, BufReader, ErrorKind}; 4 | use std::path::Path; 5 | 6 | use log::warn; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use unreal_mod_manager::unreal_asset::types::PackageIndexTrait; 10 | use unreal_mod_manager::unreal_asset::unversioned::ancestry::Ancestry; 11 | use unreal_mod_manager::unreal_asset::{ 12 | cast, 13 | engine_version::EngineVersion, 14 | exports::ExportNormalTrait, 15 | properties::{object_property::ObjectProperty, Property, PropertyDataTrait}, 16 | types::PackageIndex, 17 | Import, 18 | }; 19 | use unreal_mod_manager::unreal_helpers::Guid; 20 | use unreal_mod_manager::unreal_mod_integrator::{ 21 | helpers::{get_asset, write_asset}, 22 | Error, 23 | }; 24 | use unreal_mod_manager::unreal_pak::{PakMemory, PakReader}; 25 | 26 | use super::MAP_PATHS; 27 | 28 | #[derive(Deserialize, Serialize, Debug)] 29 | enum BiomeType { 30 | Surface, 31 | Crust, 32 | } 33 | 34 | #[derive(Deserialize, Serialize, Debug)] 35 | struct PlacementModifier { 36 | pub planet_type: String, 37 | pub biome_type: BiomeType, 38 | pub biome_name: String, 39 | pub layer_name: String, 40 | pub placements: Vec, 41 | } 42 | 43 | #[allow(clippy::ptr_arg)] 44 | pub(crate) fn handle_biome_placement_modifiers( 45 | _data: &(), 46 | integrated_pak: &mut PakMemory, 47 | game_paks: &mut Vec>>, 48 | mod_paks: &mut Vec>>, 49 | placement_modifiers: &Vec, 50 | ) -> Result<(), Error> { 51 | let mut biome_placement_modifiers = Vec::new(); 52 | 53 | for modifiers in placement_modifiers { 54 | let modifiers: Vec = serde_json::from_value(modifiers.clone()) 55 | .map_err(|e| io::Error::new(ErrorKind::Other, e))?; 56 | 57 | biome_placement_modifiers.extend(modifiers); 58 | } 59 | 60 | for map_path in MAP_PATHS { 61 | if map_path == "Astro/Content/Maps/test/BasicSphereT2.umap" { 62 | continue; 63 | } 64 | let mut asset = get_asset( 65 | integrated_pak, 66 | game_paks, 67 | mod_paks, 68 | &map_path.to_string(), 69 | EngineVersion::VER_UE4_23, 70 | )?; 71 | 72 | let mut voxel_exports = HashMap::new(); 73 | 74 | for i in 0..asset.asset_data.exports.len() { 75 | let export = &asset.asset_data.exports[i]; 76 | if let Some(normal_export) = export.get_normal_export() { 77 | let class_index = normal_export.base_export.class_index; 78 | if class_index.is_import() { 79 | let import = asset.get_import(class_index).ok_or_else(|| { 80 | io::Error::new(ErrorKind::Other, "Corrupted game installation") 81 | })?; 82 | 83 | if import.object_name.get_owned_content() == "VoxelVolumeComponent" 84 | && normal_export.base_export.object_name.get_owned_content() 85 | != "Default Voxel Volume" 86 | { 87 | voxel_exports 88 | .insert(normal_export.base_export.object_name.get_owned_content(), i); 89 | } 90 | } 91 | } 92 | } 93 | 94 | for modifier in &biome_placement_modifiers { 95 | let mut modifier_imports = Vec::new(); 96 | for placement_path in &modifier.placements { 97 | let placement_name = Path::new(placement_path) 98 | .file_stem() 99 | .and_then(|e| e.to_str()) 100 | .ok_or_else(|| { 101 | io::Error::new( 102 | ErrorKind::Other, 103 | format!("Invalid placement {placement_path}"), 104 | ) 105 | })?; 106 | 107 | let package_import = Import { 108 | class_package: asset.add_fname("/Script/CoreUObject"), 109 | class_name: asset.add_fname("Package"), 110 | outer_index: PackageIndex::new(0), 111 | object_name: asset.add_fname(placement_path), 112 | optional: false, 113 | }; 114 | let package_import = asset.add_import(package_import); 115 | 116 | let modifier_import = Import { 117 | class_package: asset.add_fname("/Script/Terrain2"), 118 | class_name: asset.add_fname("ProceduralModifier"), 119 | outer_index: package_import, 120 | object_name: asset.add_fname(placement_name), 121 | optional: false, 122 | }; 123 | let modifier_import = asset.add_import(modifier_import); 124 | modifier_imports.push(modifier_import); 125 | } 126 | 127 | let voxels_name = modifier.planet_type.clone() + "Voxels"; 128 | let export_index = voxel_exports.get(&voxels_name); 129 | if export_index.is_none() { 130 | warn!( 131 | "Failed to find voxel export {} for {}", 132 | voxels_name, map_path 133 | ); 134 | continue; 135 | } 136 | 137 | let export_index = export_index.unwrap(); 138 | let mut name_map = asset.get_name_map(); 139 | let export = (asset.asset_data.exports[*export_index]) 140 | .get_normal_export_mut() 141 | .unwrap(); 142 | 143 | let biome_property_name = match modifier.biome_type { 144 | BiomeType::Surface => "SurfaceBiomes", 145 | BiomeType::Crust => "CrustBiome", 146 | }; 147 | 148 | let mut biome_property_index = None; 149 | for i in 0..export.properties.len() { 150 | let property = &export.properties[i]; 151 | if property 152 | .get_name() 153 | .get_content(|e| e == biome_property_name) 154 | { 155 | biome_property_index = Some(i); 156 | break; 157 | } 158 | } 159 | 160 | let biome_property_index = biome_property_index.ok_or_else(|| { 161 | io::Error::new( 162 | ErrorKind::Other, 163 | format!("Failed to find biome type {biome_property_name}"), 164 | ) 165 | })?; 166 | let biome_property = cast!( 167 | Property, 168 | ArrayProperty, 169 | &mut export.properties[biome_property_index] 170 | ) 171 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Corrupted game installation"))?; 172 | 173 | let biome = biome_property 174 | .value 175 | .iter_mut() 176 | .filter_map(|e| cast!(Property, StructProperty, e)) 177 | .find(|e| { 178 | e.value 179 | .iter() 180 | .filter_map(|e| cast!(Property, NameProperty, e)) 181 | .any(|e| e.value.get_content(|e| e == modifier.biome_name)) 182 | }) 183 | .ok_or_else(|| { 184 | io::Error::new( 185 | ErrorKind::Other, 186 | format!("Failed to find biome {}", modifier.biome_name), 187 | ) 188 | })?; 189 | 190 | let layers = biome 191 | .value 192 | .iter_mut() 193 | .find(|e| e.get_name().get_content(|e| e == "Layers")) 194 | .and_then(|e| cast!(Property, ArrayProperty, e)) 195 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Corrupted game installation"))?; 196 | 197 | let layer = layers 198 | .value 199 | .iter_mut() 200 | .filter_map(|e| cast!(Property, StructProperty, e)) 201 | .find(|e| { 202 | e.value 203 | .iter() 204 | .filter_map(|e| cast!(Property, NameProperty, e)) 205 | .any(|e| e.value.get_content(|e| e == modifier.layer_name)) 206 | }) 207 | .ok_or_else(|| { 208 | io::Error::new( 209 | ErrorKind::Other, 210 | format!( 211 | "Failed to find layer {} for biome {}", 212 | modifier.layer_name, modifier.biome_name 213 | ), 214 | ) 215 | })?; 216 | 217 | let object_placement_modifiers = layer 218 | .value 219 | .iter_mut() 220 | .find(|e| { 221 | e.get_name() 222 | .get_content(|e| e == "ObjectPlacementModifiers") 223 | }) 224 | .and_then(|e| cast!(Property, ArrayProperty, e)) 225 | .ok_or_else(|| { 226 | io::Error::new(ErrorKind::Other, "Corrupted game installation".to_string()) 227 | })?; 228 | 229 | for import_index in &modifier_imports { 230 | let placement_modifier = ObjectProperty { 231 | name: name_map 232 | .get_mut() 233 | .add_fname(&object_placement_modifiers.value.len().to_string()), 234 | ancestry: Ancestry::default(), 235 | property_guid: Some(Guid::default()), 236 | duplication_index: 0, 237 | value: *import_index, 238 | }; 239 | object_placement_modifiers 240 | .value 241 | .push(placement_modifier.into()); 242 | } 243 | } 244 | 245 | write_asset(integrated_pak, &asset, &map_path.to_string()) 246 | .map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?; 247 | } 248 | 249 | Ok(()) 250 | } 251 | -------------------------------------------------------------------------------- /astro_mod_integrator/src/handlers/item_list_entries.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs::File; 3 | use std::io::{self, BufReader, ErrorKind}; 4 | use std::path::Path; 5 | 6 | use unreal_mod_manager::unreal_asset::properties::object_property::TopLevelAssetPath; 7 | use unreal_mod_manager::unreal_asset::types::PackageIndexTrait; 8 | use unreal_mod_manager::unreal_asset::unversioned::ancestry::Ancestry; 9 | use unreal_mod_manager::unreal_asset::{ 10 | cast, 11 | engine_version::EngineVersion, 12 | exports::{Export, ExportNormalTrait}, 13 | properties::{ 14 | object_property::{ObjectProperty, SoftObjectPath, SoftObjectProperty}, 15 | Property, 16 | }, 17 | types::PackageIndex, 18 | Import, 19 | }; 20 | use unreal_mod_manager::unreal_helpers::game_to_absolute; 21 | use unreal_mod_manager::unreal_mod_integrator::{ 22 | helpers::{get_asset, write_asset}, 23 | Error, IntegratorConfig, 24 | }; 25 | use unreal_mod_manager::unreal_pak::{PakMemory, PakReader}; 26 | 27 | use crate::AstroIntegratorConfig; 28 | 29 | #[allow(clippy::assigning_clones, clippy::ptr_arg)] 30 | pub(crate) fn handle_item_list_entries( 31 | _data: &(), 32 | integrated_pak: &mut PakMemory, 33 | game_paks: &mut Vec>>, 34 | mod_paks: &mut Vec>>, 35 | item_list_entires_maps: &Vec, 36 | ) -> Result<(), Error> { 37 | let mut new_items = HashMap::new(); 38 | 39 | for item_list_entries_map in item_list_entires_maps { 40 | let item_list_entries_map = item_list_entries_map 41 | .as_object() 42 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Invalid item_list_entries"))?; 43 | 44 | // we duplicate /Game/Items/ItemTypes/MasterItemList entries into /Game/Items/ItemTypes/BaseGameInitialKnownItemList, if the latter list is not specified 45 | // this provides backwards compatibility for older mods 46 | // this can just be suppressed by specifying an entry for /Game/Items/ItemTypes/BaseGameInitialKnownItemList in metadata 47 | let exists_bgikil = item_list_entries_map.contains_key("/Game/Items/ItemTypes/BaseGameInitialKnownItemList"); 48 | 49 | for (name, item_list_entries) in item_list_entries_map { 50 | let item_list_entries = item_list_entries 51 | .as_object() 52 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Invalid item_list_entries"))?; 53 | 54 | { 55 | let new_items_entry = new_items.entry(name.clone()).or_insert_with(HashMap::new); 56 | 57 | for (item_name, entries) in item_list_entries { 58 | let entries = entries 59 | .as_array() 60 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Invalid item_list_entries"))?; 61 | 62 | let new_items_entry_map = new_items_entry 63 | .entry(item_name.clone()) 64 | .or_insert_with(Vec::new); 65 | for entry in entries { 66 | let entry = entry.as_str().ok_or_else(|| { 67 | io::Error::new(ErrorKind::Other, "Invalid item_list_entries") 68 | })?; 69 | new_items_entry_map.push(String::from(entry)); 70 | } 71 | } 72 | } 73 | 74 | // duplicate to BaseGameInitialKnownItemList appropriately, if applicable 75 | if name == "/Game/Items/ItemTypes/MasterItemList" && !exists_bgikil { 76 | let orig_entry = new_items.entry(name.clone()).or_insert_with(HashMap::new).clone(); 77 | 78 | new_items 79 | .entry(String::from("/Game/Items/ItemTypes/BaseGameInitialKnownItemList")) 80 | .or_insert_with(HashMap::new) 81 | .extend(orig_entry); 82 | } 83 | } 84 | } 85 | 86 | for (asset_name, entries) in &new_items { 87 | let asset_name = game_to_absolute(AstroIntegratorConfig::GAME_NAME, asset_name) 88 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Invalid asset name"))?; 89 | let mut asset = get_asset( 90 | integrated_pak, 91 | game_paks, 92 | mod_paks, 93 | &asset_name, 94 | EngineVersion::VER_UE4_23, 95 | )?; 96 | 97 | let mut item_types_property: HashMap> = HashMap::new(); 98 | for i in 0..asset.asset_data.exports.len() { 99 | if let Some(normal_export) = asset.asset_data.exports[i].get_normal_export() { 100 | for j in 0..normal_export.properties.len() { 101 | let property = &normal_export.properties[j]; 102 | for entry_name in entries.keys() { 103 | let mut arr_name = entry_name.clone(); 104 | if arr_name.contains('.') { 105 | let split: Vec<&str> = arr_name.split('.').collect(); 106 | let export_name = split[0].to_owned(); 107 | arr_name = split[1].to_owned(); 108 | 109 | if normal_export.base_export.class_index.is_import() { 110 | if asset 111 | .get_import(normal_export.base_export.class_index) 112 | .map(|e| e.object_name.get_content(|e| e != export_name)) 113 | .unwrap_or(true) 114 | { 115 | continue; 116 | } 117 | } else { 118 | continue; 119 | } 120 | } 121 | if let Some(array_property) = cast!(Property, ArrayProperty, property) { 122 | if array_property.name.get_content(|e| e == arr_name) { 123 | item_types_property 124 | .entry(entry_name.clone()) 125 | .or_default() 126 | .push(( 127 | i, 128 | j, 129 | array_property 130 | .array_type 131 | .as_ref() 132 | .ok_or_else(|| { 133 | io::Error::new( 134 | ErrorKind::Other, 135 | "Invalid array_property", 136 | ) 137 | })? 138 | .get_owned_content(), 139 | )); 140 | } 141 | } 142 | } 143 | } 144 | } 145 | } 146 | for (name, item_paths) in entries { 147 | if !item_types_property.contains_key(name) { 148 | continue; 149 | } 150 | for item_path in item_paths { 151 | let (real_name, class_name, soft_class_name) = match item_path.contains('.') { 152 | true => { 153 | let split: Vec<&str> = item_path.split('.').collect(); 154 | ( 155 | split[0].to_string(), 156 | split[1].to_string(), 157 | split[1].to_string(), 158 | ) 159 | } 160 | false => ( 161 | item_path.clone(), 162 | Path::new(item_path) 163 | .file_stem() 164 | .and_then(|e| e.to_str()) 165 | .map(|e| String::from(e) + "_C") 166 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Invalid item_path"))?, 167 | Path::new(item_path) 168 | .file_stem() 169 | .and_then(|e| e.to_str()) 170 | .map(|e| e.to_string()) 171 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Invalid item_path"))?, 172 | ), 173 | }; 174 | 175 | let mut new_import = PackageIndex::new(0); 176 | 177 | for (export_index, property_index, array_type) in 178 | item_types_property.get(name).unwrap() 179 | { 180 | match array_type.as_str() { 181 | "ObjectProperty" => { 182 | if new_import.index == 0 { 183 | let inner_import = Import { 184 | class_package: asset.add_fname("/Script/CoreUObject"), 185 | class_name: asset.add_fname("Package"), 186 | outer_index: PackageIndex::new(0), 187 | object_name: asset.add_fname(&real_name), 188 | optional: false, 189 | }; 190 | let inner_import = asset.add_import(inner_import); 191 | 192 | let import = Import { 193 | class_package: asset.add_fname("/Script/Engine"), 194 | class_name: asset.add_fname("BlueprintGeneratedClass"), 195 | outer_index: inner_import, 196 | object_name: asset.add_fname(&class_name), 197 | optional: false, 198 | }; 199 | new_import = asset.add_import(import); 200 | } 201 | 202 | let export = cast!( 203 | Export, 204 | NormalExport, 205 | &mut asset.asset_data.exports[*export_index] 206 | ) 207 | .expect("Corrupted memory"); 208 | let property = cast!( 209 | Property, 210 | ArrayProperty, 211 | &mut export.properties[*property_index] 212 | ) 213 | .expect("Corrupted memory"); 214 | property.value.push( 215 | ObjectProperty { 216 | name: property.name.clone(), 217 | ancestry: Ancestry::default(), 218 | property_guid: None, 219 | duplication_index: 0, 220 | value: new_import, 221 | } 222 | .into(), 223 | ); 224 | } 225 | "SoftObjectProperty" => { 226 | asset.add_name_reference(real_name.clone(), false); 227 | 228 | let asset_path_name = asset.add_fname(&real_name); 229 | 230 | let export = cast!( 231 | Export, 232 | NormalExport, 233 | &mut asset.asset_data.exports[*export_index] 234 | ) 235 | .expect("Corrupted memory"); 236 | let property = cast!( 237 | Property, 238 | ArrayProperty, 239 | &mut export.properties[*property_index] 240 | ) 241 | .expect("Corrupted memory"); 242 | property.value.push( 243 | SoftObjectProperty { 244 | name: property.name.clone(), 245 | ancestry: Ancestry::default(), 246 | property_guid: None, 247 | duplication_index: 0, 248 | value: SoftObjectPath { 249 | asset_path: TopLevelAssetPath::new(None, asset_path_name), 250 | sub_path_string: Some(soft_class_name.clone()), 251 | }, 252 | } 253 | .into(), 254 | ); 255 | } 256 | _ => {} 257 | } 258 | } 259 | } 260 | } 261 | 262 | write_asset(integrated_pak, &asset, &asset_name) 263 | .map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?; 264 | } 265 | 266 | Ok(()) 267 | } 268 | -------------------------------------------------------------------------------- /astro_mod_integrator/src/handlers/linked_actor_components.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs::File; 3 | use std::io::{self, BufReader, Cursor, ErrorKind}; 4 | use std::path::Path; 5 | 6 | use unreal_mod_manager::unreal_asset::reader::archive_trait::ArchiveTrait; 7 | use unreal_mod_manager::unreal_asset::types::PackageIndexTrait; 8 | use unreal_mod_manager::unreal_asset::unversioned::ancestry::Ancestry; 9 | use uuid::Uuid; 10 | 11 | use unreal_mod_manager::unreal_asset::{ 12 | cast, 13 | engine_version::EngineVersion, 14 | exports::{Export, ExportBaseTrait, ExportNormalTrait}, 15 | flags::EObjectFlags, 16 | properties::{ 17 | guid_property::GuidProperty, int_property::BoolProperty, object_property::ObjectProperty, 18 | str_property::NameProperty, struct_property::StructProperty, Property, PropertyDataTrait, 19 | }, 20 | types::PackageIndex, 21 | uproperty::UProperty, 22 | Asset, Import, 23 | }; 24 | use unreal_mod_manager::unreal_helpers::{game_to_absolute, Guid}; 25 | use unreal_mod_manager::unreal_mod_integrator::{ 26 | helpers::{get_asset, write_asset}, 27 | Error, IntegratorConfig, 28 | }; 29 | use unreal_mod_manager::unreal_pak::{PakMemory, PakReader}; 30 | 31 | use crate::assets::{ACTOR_TEMPLATE_ASSET, ACTOR_TEMPLATE_EXPORT}; 32 | use crate::AstroIntegratorConfig; 33 | 34 | #[allow(clippy::ptr_arg)] 35 | pub(crate) fn handle_linked_actor_components( 36 | _data: &(), 37 | integrated_pak: &mut PakMemory, 38 | game_paks: &mut Vec>>, 39 | mod_paks: &mut Vec>>, 40 | linked_actors_maps: &Vec, 41 | ) -> Result<(), Error> { 42 | let actor_asset = Asset::new( 43 | Cursor::new(ACTOR_TEMPLATE_ASSET.to_vec()), 44 | Some(Cursor::new(ACTOR_TEMPLATE_EXPORT.to_vec())), 45 | EngineVersion::VER_UE4_23, 46 | None, 47 | ) 48 | .map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?; 49 | 50 | let gen_variable = cast!(Export, NormalExport, &actor_asset.asset_data.exports[0]) 51 | .expect("Corrupted ActorTemplate"); 52 | let component_export = cast!(Export, PropertyExport, &actor_asset.asset_data.exports[1]) 53 | .expect("Corrupted ActorTemplate"); 54 | let scs_export = cast!(Export, NormalExport, &actor_asset.asset_data.exports[2]) 55 | .expect("Corrupted ActorTemplate"); 56 | 57 | let mut new_components = HashMap::new(); 58 | 59 | for linked_actor_map in linked_actors_maps { 60 | let linked_actors_map = linked_actor_map 61 | .as_object() 62 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Invalid linked_actor_components"))?; 63 | for (name, components) in linked_actors_map.iter() { 64 | let components = components.as_array().ok_or_else(|| { 65 | io::Error::new(ErrorKind::Other, "Invalid linked_actor_components") 66 | })?; 67 | 68 | let entry = new_components.entry(name.clone()).or_insert_with(Vec::new); 69 | for component in components { 70 | let component_name = component.as_str().ok_or_else(|| { 71 | io::Error::new(ErrorKind::Other, "Invalid linked_actor_components") 72 | })?; 73 | entry.push(String::from(component_name)); 74 | } 75 | } 76 | } 77 | 78 | for (name, components) in &new_components { 79 | let name = game_to_absolute(AstroIntegratorConfig::GAME_NAME, name) 80 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Invalid asset name"))?; 81 | let mut asset = get_asset( 82 | integrated_pak, 83 | game_paks, 84 | mod_paks, 85 | &name, 86 | EngineVersion::VER_UE4_23, 87 | )?; 88 | 89 | for component_path_raw in components { 90 | let mut actor_index = None; 91 | let mut simple_construction_script = None; 92 | let mut cdo_location = None; 93 | for i in 0..asset.asset_data.exports.len() { 94 | let export = &asset.asset_data.exports[i]; 95 | if let Some(normal_export) = export.get_normal_export() { 96 | if normal_export.base_export.class_index.is_import() { 97 | let import = asset 98 | .get_import(normal_export.base_export.class_index) 99 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Import not found"))?; 100 | match import.object_name.get_owned_content().as_str() { 101 | "BlueprintGeneratedClass" => actor_index = Some(i), 102 | "SimpleConstructionScript" => simple_construction_script = Some(i), 103 | _ => {} 104 | } 105 | } 106 | if normal_export 107 | .base_export 108 | .object_flags 109 | .contains(EObjectFlags::RF_CLASS_DEFAULT_OBJECT) 110 | { 111 | cdo_location = Some(i); 112 | } 113 | } 114 | } 115 | 116 | let actor_index = 117 | actor_index.ok_or_else(|| io::Error::new(ErrorKind::Other, "Actor not found"))?; 118 | let actor = actor_index as i32 + 1; 119 | let simple_construction_script_index = simple_construction_script 120 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "SCS not found"))?; 121 | let simple_construction_script = simple_construction_script_index as i32 + 1; 122 | let cdo_location = 123 | cdo_location.ok_or_else(|| io::Error::new(ErrorKind::Other, "CDO not found"))?; 124 | 125 | let script_core_uobject = asset.add_fname("/Script/CoreUObject"); 126 | let name_class = asset.add_fname("Class"); 127 | let object_property = asset.add_fname("ObjectProperty"); 128 | let default_object_property = asset.add_fname("Default__ObjectProperty"); 129 | let name_scs_node = asset.add_fname("SCS_Node"); 130 | let script_engine = asset.add_fname("/Script/Engine"); 131 | let default_scs_node = asset.add_fname("Default__SCS_Node"); 132 | 133 | let class_object_property_import = asset 134 | .find_import_no_index(&script_core_uobject, &name_class, &object_property) 135 | .expect("No class object property import"); 136 | 137 | let default_object_property_import = asset 138 | .find_import_no_index( 139 | &script_core_uobject, 140 | &object_property, 141 | &default_object_property, 142 | ) 143 | .expect("No default objectproperty"); 144 | 145 | let scs_node_import = asset 146 | .find_import_no_index(&script_core_uobject, &name_class, &name_scs_node) 147 | .expect("No SCS_Node"); 148 | 149 | let default_scs_node_import = asset 150 | .find_import_no_index(&script_engine, &name_scs_node, &default_scs_node) 151 | .expect("No default scs"); 152 | 153 | let component = Path::new(component_path_raw) 154 | .file_stem() 155 | .and_then(|e| e.to_str()) 156 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Invalid persistent actors"))?; 157 | 158 | let (component_path_raw, component) = match component.contains('.') { 159 | true => { 160 | let split: Vec<&str> = component.split('.').collect(); 161 | (split[0].to_string(), &split[1][..split[1].len() - 2]) 162 | } 163 | false => (component_path_raw.to_string(), component), 164 | }; 165 | let component_c = String::from(component) + "_C"; 166 | let default_component = String::from("Default__") + component + "_C"; 167 | 168 | let package_import = Import { 169 | class_package: asset.add_fname("/Script/CoreUObject"), 170 | class_name: asset.add_fname("Package"), 171 | outer_index: PackageIndex::new(0), 172 | object_name: asset.add_fname(&component_path_raw), 173 | optional: false, 174 | }; 175 | let package_import = asset.add_import(package_import); 176 | 177 | let blueprint_generated_class_import = Import { 178 | class_package: asset.add_fname("/Script/Engine"), 179 | class_name: asset.add_fname("BlueprintGeneratedClass"), 180 | outer_index: package_import, 181 | object_name: asset.add_fname(&component_c), 182 | optional: false, 183 | }; 184 | let blueprint_generated_class_import = 185 | asset.add_import(blueprint_generated_class_import); 186 | 187 | let default_import = Import { 188 | class_package: asset.add_fname("/Game/AddMe"), 189 | class_name: asset.add_fname(&component_c), 190 | outer_index: package_import, 191 | object_name: asset.add_fname(&default_component), 192 | optional: false, 193 | }; 194 | let default_import = asset.add_import(default_import); 195 | 196 | let mut component_export = component_export.clone(); 197 | let component_object_property = 198 | cast!(UProperty, UObjectProperty, &mut component_export.property) 199 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Corrupted starter pak"))?; 200 | component_object_property.property_class = blueprint_generated_class_import; 201 | 202 | let component_base_export = component_export.get_base_export_mut(); 203 | component_base_export.object_name = asset.add_fname(component); 204 | component_base_export.create_before_serialization_dependencies = 205 | Vec::from([blueprint_generated_class_import]); 206 | component_base_export.create_before_create_dependencies = 207 | Vec::from([PackageIndex::new(actor)]); 208 | component_base_export.outer_index = PackageIndex::new(actor); 209 | component_base_export.class_index = PackageIndex::new(class_object_property_import); 210 | component_base_export.template_index = 211 | PackageIndex::new(default_object_property_import); 212 | 213 | asset.asset_data.exports.push(component_export.into()); 214 | 215 | let component_export_index = asset.asset_data.exports.len() as i32; 216 | let actor_export = cast!( 217 | Export, 218 | ClassExport, 219 | &mut asset.asset_data.exports[actor_index] 220 | ) 221 | .expect("Corrupted memory"); 222 | actor_export 223 | .struct_export 224 | .children 225 | .push(PackageIndex::new(component_export_index)); 226 | actor_export 227 | .struct_export 228 | .normal_export 229 | .base_export 230 | .serialization_before_serialization_dependencies 231 | .push(PackageIndex::new(component_export_index)); 232 | 233 | let mut component_gen_variable = gen_variable.clone(); 234 | let component_gen_variable_base_export = component_gen_variable.get_base_export_mut(); 235 | component_gen_variable_base_export.outer_index = PackageIndex::new(actor); 236 | component_gen_variable_base_export.class_index = blueprint_generated_class_import; 237 | component_gen_variable_base_export.template_index = default_import; 238 | component_gen_variable_base_export.serialization_before_serialization_dependencies = 239 | Vec::from([PackageIndex::new(actor)]); 240 | component_gen_variable_base_export.serialization_before_create_dependencies = 241 | Vec::from([blueprint_generated_class_import, default_import]); 242 | component_gen_variable_base_export.create_before_create_dependencies = 243 | Vec::from([PackageIndex::new(actor)]); 244 | component_gen_variable_base_export.object_name = 245 | asset.add_fname(&(String::from(component) + "_GEN_VARIABLE")); 246 | 247 | let component_gen_variable_normal_export = 248 | component_gen_variable.get_normal_export_mut().unwrap(); 249 | asset.add_fname("BoolProperty"); 250 | component_gen_variable_normal_export.properties = Vec::from([BoolProperty { 251 | name: asset.add_fname("bAutoActivate"), 252 | ancestry: Ancestry::default(), 253 | property_guid: Some(Guid::default()), 254 | duplication_index: 0, 255 | value: true, 256 | } 257 | .into()]); 258 | 259 | asset.asset_data.exports.push(component_gen_variable.into()); 260 | let component_gen_variable_index = asset.asset_data.exports.len() as i32; 261 | 262 | let mut scs_node = scs_export.clone(); 263 | let scs_node_normal_export = scs_node 264 | .get_normal_export_mut() 265 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Corrupted starter pak"))?; 266 | scs_node_normal_export.properties = Vec::from([ 267 | ObjectProperty { 268 | name: asset.add_fname("ComponentClass"), 269 | ancestry: Ancestry::default(), 270 | property_guid: Some(Guid::default()), 271 | duplication_index: 0, 272 | value: blueprint_generated_class_import, 273 | } 274 | .into(), 275 | ObjectProperty { 276 | name: asset.add_fname("ComponentTemplate"), 277 | ancestry: Ancestry::default(), 278 | property_guid: Some(Guid::default()), 279 | duplication_index: 0, 280 | value: PackageIndex::new(component_gen_variable_index), 281 | } 282 | .into(), 283 | StructProperty { 284 | name: asset.add_fname("VariableGuid"), 285 | ancestry: Ancestry::default(), 286 | struct_type: Some(asset.add_fname("Guid")), 287 | struct_guid: Some(Guid::default()), 288 | property_guid: None, 289 | duplication_index: 0, 290 | serialize_none: true, 291 | value: Vec::from([GuidProperty { 292 | name: asset.add_fname("VariableGuid"), 293 | ancestry: Ancestry::default(), 294 | property_guid: None, 295 | duplication_index: 0, 296 | value: Guid::from(Uuid::new_v4().into_bytes()), 297 | } 298 | .into()]), 299 | } 300 | .into(), 301 | NameProperty { 302 | name: asset.add_fname("InternalVariableName"), 303 | ancestry: Ancestry::default(), 304 | property_guid: None, 305 | duplication_index: 0, 306 | value: asset.add_fname(component), 307 | } 308 | .into(), 309 | ]); 310 | scs_node_normal_export.base_export.outer_index = 311 | PackageIndex::new(simple_construction_script); 312 | scs_node_normal_export.base_export.class_index = PackageIndex::new(scs_node_import); 313 | scs_node_normal_export.base_export.template_index = 314 | PackageIndex::new(default_scs_node_import); 315 | scs_node_normal_export 316 | .base_export 317 | .create_before_serialization_dependencies = Vec::from([ 318 | blueprint_generated_class_import, 319 | PackageIndex::new(component_gen_variable_index), 320 | ]); 321 | scs_node_normal_export 322 | .base_export 323 | .serialization_before_create_dependencies = Vec::from([ 324 | PackageIndex::new(scs_node_import), 325 | PackageIndex::new(default_scs_node_import), 326 | ]); 327 | scs_node_normal_export 328 | .base_export 329 | .create_before_create_dependencies = 330 | Vec::from([PackageIndex::new(simple_construction_script)]); 331 | 332 | let mut last_scs_node_index = 0; 333 | for export in &asset.asset_data.exports { 334 | let object_name = &export.get_base_export().object_name; 335 | if object_name.get_content(|e| e == "SCS_Node") 336 | && last_scs_node_index < object_name.get_number() 337 | { 338 | last_scs_node_index = object_name.get_number(); 339 | } 340 | } 341 | scs_node_normal_export.base_export.object_name = 342 | asset.add_fname_with_number("SCS_Node", last_scs_node_index + 1); 343 | 344 | asset.asset_data.exports.push(scs_node.into()); 345 | let scs_node_index = asset.asset_data.exports.len() as i32; 346 | 347 | let cdo_base_export = asset.asset_data.exports[cdo_location].get_base_export_mut(); 348 | cdo_base_export 349 | .serialization_before_serialization_dependencies 350 | .push(PackageIndex::new(scs_node_index)); 351 | cdo_base_export 352 | .serialization_before_serialization_dependencies 353 | .push(PackageIndex::new(component_gen_variable_index)); 354 | 355 | let mut name_map = asset.get_name_map(); 356 | 357 | let simple_construction_script_export = asset.asset_data.exports 358 | [simple_construction_script_index] 359 | .get_normal_export_mut() 360 | .expect("Corrupted memory"); 361 | simple_construction_script_export 362 | .base_export 363 | .create_before_serialization_dependencies 364 | .push(PackageIndex::new(scs_node_index)); 365 | 366 | for property in &mut simple_construction_script_export.properties { 367 | if let Some(array_property) = cast!(Property, ArrayProperty, property) { 368 | let name = array_property.name.get_owned_content(); 369 | let name = name.as_str(); 370 | if name == "AllNodes" || name == "RootNodes" { 371 | let mut last_index = 0; 372 | for property in &array_property.value { 373 | let index = property.get_name().get_number(); 374 | if last_index < index { 375 | last_index = index; 376 | } 377 | } 378 | 379 | let name = name_map 380 | .get_mut() 381 | .add_fname_with_number(&(last_index + 1).to_string(), -2147483648); 382 | array_property.value.push( 383 | ObjectProperty { 384 | name, 385 | ancestry: Ancestry::default(), 386 | property_guid: None, 387 | duplication_index: 0, 388 | value: PackageIndex::new(scs_node_index), 389 | } 390 | .into(), 391 | ); 392 | } 393 | } 394 | } 395 | } 396 | 397 | write_asset(integrated_pak, &asset, &name) 398 | .map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?; 399 | } 400 | Ok(()) 401 | } 402 | -------------------------------------------------------------------------------- /astro_mod_integrator/src/handlers/mission_trailheads.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{self, BufReader, ErrorKind}; 3 | use std::path::Path; 4 | 5 | use unreal_mod_manager::unreal_asset::types::PackageIndexTrait; 6 | use unreal_mod_manager::unreal_asset::unversioned::ancestry::Ancestry; 7 | use unreal_mod_manager::unreal_asset::{ 8 | cast, 9 | engine_version::EngineVersion, 10 | exports::{Export, ExportNormalTrait}, 11 | properties::{object_property::ObjectProperty, Property}, 12 | types::PackageIndex, 13 | Import, 14 | }; 15 | use unreal_mod_manager::unreal_helpers::Guid; 16 | use unreal_mod_manager::unreal_mod_integrator::{ 17 | helpers::{get_asset, write_asset}, 18 | Error, 19 | }; 20 | use unreal_mod_manager::unreal_pak::{PakMemory, PakReader}; 21 | 22 | use super::MAP_PATHS; 23 | 24 | #[allow(clippy::ptr_arg)] 25 | pub(crate) fn handle_mission_trailheads( 26 | _data: &(), 27 | integrated_pak: &mut PakMemory, 28 | game_paks: &mut Vec>>, 29 | mod_paks: &mut Vec>>, 30 | trailhead_arrays: &Vec, 31 | ) -> Result<(), Error> { 32 | for map_path in MAP_PATHS { 33 | let mut asset = get_asset( 34 | integrated_pak, 35 | game_paks, 36 | mod_paks, 37 | &String::from(map_path), 38 | EngineVersion::VER_UE4_23, 39 | )?; 40 | 41 | let mut trailheads = Vec::new(); 42 | for trailheads_array in trailhead_arrays { 43 | let trailheads_array = trailheads_array 44 | .as_array() 45 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Invalid trailheads"))?; 46 | for trailhead in trailheads_array { 47 | let trailhead = trailhead 48 | .as_str() 49 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Invalid trailheads"))?; 50 | trailheads.push(trailhead); 51 | } 52 | } 53 | 54 | let mut mission_data_export_index = None; 55 | let mut mission_data_property_index = None; 56 | 57 | for i in 0..asset.asset_data.exports.len() { 58 | let export = &asset.asset_data.exports[i]; 59 | if let Some(normal_export) = export.get_normal_export() { 60 | if normal_export.base_export.class_index.is_import() { 61 | let import = asset 62 | .get_import(normal_export.base_export.class_index) 63 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Invalid import"))?; 64 | if import.object_name.get_content(|e| e == "AstroSettings") { 65 | for j in 0..normal_export.properties.len() { 66 | let property = &normal_export.properties[j]; 67 | if let Some(array_property) = cast!(Property, ArrayProperty, property) { 68 | if array_property.name.get_content(|e| e == "MissionData") 69 | && array_property 70 | .array_type 71 | .as_ref() 72 | .map(|e| e.get_content(|e| e == "ObjectProperty")) 73 | .unwrap_or(false) 74 | { 75 | mission_data_export_index = Some(i); 76 | mission_data_property_index = Some(j); 77 | break; 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | if let (Some(mission_data_export_index), Some(mission_data_property_index)) = 87 | (mission_data_export_index, mission_data_property_index) 88 | { 89 | for trailhead in trailheads { 90 | let soft_class_name = Path::new(trailhead) 91 | .file_stem() 92 | .and_then(|e| e.to_str()) 93 | .ok_or_else(|| io::Error::new(ErrorKind::Other, "Invalid trailhead"))?; 94 | 95 | let package_link = Import { 96 | class_package: asset.add_fname("/Script/CoreUObject"), 97 | class_name: asset.add_fname("Package"), 98 | outer_index: PackageIndex::new(0), 99 | object_name: asset.add_fname(trailhead), 100 | optional: false, 101 | }; 102 | let package_link = asset.add_import(package_link); 103 | 104 | let mission_data_asset_link = Import { 105 | class_package: asset.add_fname("/Script/Astro"), 106 | class_name: asset.add_fname("AstroMissionDataAsset"), 107 | outer_index: package_link, 108 | object_name: asset.add_fname(soft_class_name), 109 | optional: false, 110 | }; 111 | let mission_data_asset_link = asset.add_import(mission_data_asset_link); 112 | 113 | let mission_data_export = cast!( 114 | Export, 115 | NormalExport, 116 | &mut asset.asset_data.exports[mission_data_export_index] 117 | ) 118 | .expect("Corrupted memory"); 119 | let mission_data_property = cast!( 120 | Property, 121 | ArrayProperty, 122 | &mut mission_data_export.properties[mission_data_property_index] 123 | ) 124 | .expect("Corrupted memory"); 125 | 126 | let property = ObjectProperty { 127 | name: mission_data_property.name.clone(), 128 | ancestry: Ancestry::default(), 129 | property_guid: Some(Guid::default()), 130 | duplication_index: 0, 131 | value: mission_data_asset_link, 132 | }; 133 | mission_data_property.value.push(property.into()); 134 | } 135 | } 136 | 137 | write_asset(integrated_pak, &asset, &String::from(map_path)) 138 | .map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?; 139 | } 140 | 141 | Ok(()) 142 | } 143 | -------------------------------------------------------------------------------- /astro_mod_integrator/src/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use regex::Regex; 3 | 4 | pub(crate) mod biome_placement_modifiers; 5 | pub(crate) mod item_list_entries; 6 | pub(crate) mod linked_actor_components; 7 | pub(crate) mod mission_trailheads; 8 | 9 | lazy_static! { 10 | static ref GAME_REGEX: Regex = Regex::new(r"^/Game/").unwrap(); 11 | } 12 | 13 | pub(crate) static MAP_PATHS: [&str; 3] = [ 14 | "Astro/Content/Maps/Staging_T2.umap", 15 | "Astro/Content/Maps/Staging_T2_PackedPlanets_Switch.umap", 16 | "Astro/Content/U32_Expansion/U32_Expansion.umap" 17 | ]; 18 | -------------------------------------------------------------------------------- /astro_mod_integrator/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use lazy_static::lazy_static; 4 | 5 | use unreal_mod_manager::unreal_asset::engine_version::EngineVersion; 6 | use unreal_mod_manager::unreal_helpers::game_to_absolute; 7 | use unreal_mod_manager::unreal_mod_integrator::{ 8 | BakedMod, Error, HandlerFn, IntegratorConfig, IntegratorMod, 9 | }; 10 | 11 | pub mod assets; 12 | pub(crate) mod baked; 13 | pub(crate) mod handlers; 14 | 15 | use crate::handlers::{ 16 | biome_placement_modifiers, item_list_entries, linked_actor_components, mission_trailheads, 17 | }; 18 | 19 | pub struct AstroIntegratorConfig; 20 | 21 | lazy_static! { 22 | static ref FILE_REFS: HashMap = HashMap::from([ 23 | ( 24 | game_to_absolute( 25 | AstroIntegratorConfig::GAME_NAME, 26 | "/Game/Integrator/NotificationActor.uasset" 27 | ) 28 | .unwrap(), 29 | assets::ALERT_MOD_NOTIFICATION_ACTOR_ASSET 30 | ), 31 | ( 32 | game_to_absolute( 33 | AstroIntegratorConfig::GAME_NAME, 34 | "/Game/Integrator/NotificationActor.uexp" 35 | ) 36 | .unwrap(), 37 | assets::ALERT_MOD_NOTIFICATION_ACTOR_EXPORT 38 | ), 39 | ]); 40 | } 41 | 42 | impl<'data> IntegratorConfig<'data, (), Error> for AstroIntegratorConfig { 43 | fn get_data(&self) -> &'data () { 44 | &() 45 | } 46 | 47 | fn get_handlers(&self) -> std::collections::HashMap>> { 48 | let mut handlers: std::collections::HashMap>> = 49 | HashMap::new(); 50 | 51 | handlers.insert( 52 | String::from("mission_trailheads"), 53 | Box::new(mission_trailheads::handle_mission_trailheads), 54 | ); 55 | 56 | handlers.insert( 57 | String::from("linked_actor_components"), 58 | Box::new(linked_actor_components::handle_linked_actor_components), 59 | ); 60 | 61 | handlers.insert( 62 | String::from("item_list_entries"), 63 | Box::new(item_list_entries::handle_item_list_entries), 64 | ); 65 | 66 | handlers.insert( 67 | String::from("biome_placement_modifiers"), 68 | Box::new(biome_placement_modifiers::handle_biome_placement_modifiers), 69 | ); 70 | 71 | handlers 72 | } 73 | 74 | fn get_baked_mods(&self) -> Vec> { 75 | Vec::from([BakedMod { 76 | data: baked::CORE_MOD, 77 | mod_id: "CoreMod".to_string(), 78 | filename: "800-CoreMod-0.1.0_P.pak", 79 | is_core: true, 80 | priority: 800, 81 | } 82 | .into()]) 83 | } 84 | 85 | const GAME_NAME: &'static str = "Astro"; 86 | const INTEGRATOR_VERSION: &'static str = env!("CARGO_PKG_VERSION"); 87 | const ENGINE_VERSION: EngineVersion = EngineVersion::VER_UE4_23; 88 | } 89 | -------------------------------------------------------------------------------- /astro_modloader/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "astro_modloader" 3 | version.workspace = true 4 | authors.workspace = true 5 | description.workspace = true 6 | edition = "2021" 7 | 8 | [dependencies] 9 | astro_mod_integrator.workspace = true 10 | 11 | unreal_mod_manager.workspace = true 12 | 13 | autoupdater = "0.2.1" 14 | colored = "2.0.0" 15 | image = "0.24.6" 16 | lazy_static.workspace = true 17 | log.workspace = true 18 | serde.workspace = true 19 | serde_json.workspace = true 20 | regex.workspace = true 21 | 22 | [target.'cfg(windows)'.build-dependencies] 23 | winres = "0.1.12" 24 | -------------------------------------------------------------------------------- /astro_modloader/README.md: -------------------------------------------------------------------------------- 1 | # astro_modloader 2 | Binary crate relying on unreal_mod_manager to build a Modloader specific to Astroneer. 3 | -------------------------------------------------------------------------------- /astro_modloader/assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroTechies/astro_modloader/289812a5f519ec00e950184c63f0106eaa1fb012/astro_modloader/assets/icon.ico -------------------------------------------------------------------------------- /astro_modloader/build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf, process::Command}; 2 | 3 | fn main() { 4 | println!("cargo-rerun-if-changed=build.rs"); 5 | println!("cargo-rerun-if-changed=Cargo.lock"); 6 | #[cfg(windows)] 7 | { 8 | winres::WindowsResource::new() 9 | .set_icon("assets/icon.ico") 10 | .compile() 11 | .unwrap(); 12 | } 13 | 14 | let template_file = PathBuf::from( 15 | env::var_os("CARGO_MANIFEST_DIR").expect("Failed to read CARGO_MANIFEST_DIR"), 16 | ) 17 | .join("..") // gross hack because no way to get manifest dir 18 | .join("about.hbs"); 19 | 20 | let licenses_dir = 21 | PathBuf::from(env::var_os("OUT_DIR").expect("Failed to read OUT_DIR")).join("licenses.md"); 22 | 23 | Command::new("cargo") 24 | .arg("about") 25 | .arg("generate") 26 | .arg("--all-features") 27 | .arg("--workspace") 28 | .arg("-o") 29 | .arg(licenses_dir) 30 | .arg(template_file) 31 | .spawn() 32 | .ok() 33 | .and_then(|mut e| e.wait().ok()) 34 | .expect("Failed to generate license summary"); 35 | } 36 | -------------------------------------------------------------------------------- /astro_modloader/src/logging.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io::{prelude::*, BufWriter}; 3 | use std::sync::{Mutex, OnceLock}; 4 | 5 | use colored::*; 6 | use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError}; 7 | 8 | #[derive(Debug)] 9 | struct SimpleLogger { 10 | // Synchronize log entries 11 | file: Mutex>, 12 | } 13 | 14 | impl SimpleLogger { 15 | fn new(file: fs::File) -> Self { 16 | SimpleLogger { 17 | file: Mutex::new(BufWriter::new(file)), 18 | } 19 | } 20 | 21 | fn lock(&self, f: impl FnOnce(&mut BufWriter) -> T) -> T { 22 | // Ignore log mutex poison 23 | let mut guard = match self.file.lock() { 24 | Ok(guard) => guard, 25 | Err(err) => err.into_inner(), 26 | }; 27 | 28 | f(&mut guard) 29 | } 30 | } 31 | 32 | impl Log for SimpleLogger { 33 | fn enabled(&self, metadata: &Metadata) -> bool { 34 | metadata.level() <= Level::Trace 35 | } 36 | 37 | fn log(&self, record: &Record) { 38 | if self.enabled(record.metadata()) { 39 | let file_path = match record.file() { 40 | Some(path) => match path.split_once(".cargo") { 41 | // cut down to only lib name 42 | Some((_, path)) => &path[42..], 43 | None => path, 44 | }, 45 | None => "", 46 | }; 47 | 48 | // just log debug and above, as otherwise logs are far, far too verbose 49 | if record.level() <= Level::Debug { 50 | let level_colored = match record.level() { 51 | Level::Error => "ERROR".red(), 52 | Level::Warn => "WARN".yellow(), 53 | Level::Info => "INFO".green(), 54 | Level::Debug => "DEBUG".cyan(), 55 | Level::Trace => "TRACE".blue(), 56 | }; 57 | let level = match record.level() { 58 | Level::Error => "ERROR", 59 | Level::Warn => "WARN", 60 | Level::Info => "INFO", 61 | Level::Debug => "DEBUG", 62 | Level::Trace => "TRACE", 63 | }; 64 | 65 | println!( 66 | "{}{level_colored:<5} {file_path}:{}{} {}", 67 | "[".truecolor(100, 100, 100), 68 | record.line().unwrap_or(0), 69 | "]".truecolor(100, 100, 100), 70 | record.args() 71 | ); 72 | 73 | self.lock(|file| { 74 | writeln!( 75 | file, 76 | "[{level:<5} {file_path}:{}] {}", 77 | record.line().unwrap_or(0), 78 | record.args() 79 | ) 80 | }) 81 | .unwrap(); 82 | } 83 | } 84 | } 85 | 86 | fn flush(&self) { 87 | self.lock(|file| file.flush()).unwrap() 88 | } 89 | } 90 | 91 | fn get_logger() -> &'static SimpleLogger { 92 | static LOGGER: OnceLock = OnceLock::new(); 93 | LOGGER.get_or_init(|| { 94 | SimpleLogger::new( 95 | // Open file 96 | fs::OpenOptions::new() 97 | .write(true) 98 | .create(true) 99 | .truncate(true) 100 | .open("modloader_log.txt") 101 | .unwrap(), 102 | ) 103 | }) 104 | } 105 | 106 | pub fn init() -> Result<(), SetLoggerError> { 107 | log::set_logger(get_logger()).map(|()| log::set_max_level(LevelFilter::Trace)) 108 | } 109 | 110 | pub fn flush() { 111 | get_logger().flush() 112 | } 113 | -------------------------------------------------------------------------------- /astro_modloader/src/main.rs: -------------------------------------------------------------------------------- 1 | // Don't show console window in release builds 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | use std::cell::RefCell; 5 | use std::collections::BTreeMap; 6 | use std::path::Path; 7 | 8 | use autoupdater::{ 9 | apis::{ 10 | github::{GithubApi, GithubRelease}, 11 | DownloadApiTrait, 12 | }, 13 | cargo_crate_version, 14 | }; 15 | use lazy_static::lazy_static; 16 | use log::info; 17 | 18 | use unreal_mod_manager::{ 19 | config::{GameConfig, IconData, InstallManager}, 20 | error::ModLoaderError, 21 | game_platform_managers::GetGameBuildTrait, 22 | unreal_cpp_bootstrapper::config::{FunctionInfo, FunctionInfoPatterns}, 23 | update_info::UpdateInfo, 24 | version::GameBuild, 25 | }; 26 | use unreal_mod_manager::{ 27 | unreal_cpp_bootstrapper::config::GameSettings, unreal_mod_integrator::IntegratorConfig, 28 | }; 29 | 30 | use astro_mod_integrator::AstroIntegratorConfig; 31 | 32 | mod logging; 33 | 34 | #[cfg(windows)] 35 | #[derive(Debug, Default)] 36 | struct SteamGetGameBuild { 37 | game_build: RefCell>, 38 | } 39 | 40 | #[cfg(windows)] 41 | use unreal_mod_manager::game_platform_managers::{MsStoreInstallManager, SteamInstallManager}; 42 | #[cfg(windows)] 43 | impl GetGameBuildTrait for SteamGetGameBuild { 44 | fn get_game_build(&self, manager: &SteamInstallManager) -> Option { 45 | if self.game_build.borrow().is_none() && manager.get_game_install_path().is_some() { 46 | let version_file_path = manager 47 | .game_path 48 | .borrow() 49 | .as_ref() 50 | .unwrap() 51 | .join("build.version"); 52 | 53 | if !version_file_path.is_file() { 54 | info!("{:?} not found", version_file_path); 55 | return None; 56 | } 57 | 58 | let version_file = std::fs::read_to_string(&version_file_path).unwrap(); 59 | let game_build_string = version_file.split(' ').next().unwrap().to_owned(); 60 | 61 | *self.game_build.borrow_mut() = GameBuild::try_from(&game_build_string).ok(); 62 | } 63 | *self.game_build.borrow() 64 | } 65 | } 66 | 67 | #[cfg(target_os = "linux")] 68 | #[derive(Debug, Default)] 69 | struct ProtonGetGameBuild { 70 | game_build: RefCell>, 71 | } 72 | 73 | #[cfg(target_os = "linux")] 74 | use unreal_mod_manager::game_platform_managers::ProtonInstallManager; 75 | #[cfg(target_os = "linux")] 76 | impl GetGameBuildTrait for ProtonGetGameBuild { 77 | fn get_game_build(&self, manager: &ProtonInstallManager) -> Option { 78 | if self.game_build.borrow().is_none() && manager.get_game_install_path().is_some() { 79 | let version_file_path = manager 80 | .game_path 81 | .borrow() 82 | .as_ref() 83 | .unwrap() 84 | .join("build.version"); 85 | 86 | if !version_file_path.is_file() { 87 | info!("{:?} not found", version_file_path); 88 | return None; 89 | } 90 | 91 | let version_file = std::fs::read_to_string(&version_file_path).unwrap(); 92 | let game_build_string = version_file.split(' ').next().unwrap().to_owned(); 93 | 94 | *self.game_build.borrow_mut() = GameBuild::try_from(&game_build_string).ok(); 95 | } 96 | *self.game_build.borrow() 97 | } 98 | } 99 | 100 | struct AstroGameConfig; 101 | 102 | fn load_icon() -> IconData { 103 | let data = include_bytes!("../assets/icon.ico"); 104 | let image = image::load_from_memory(data).unwrap().to_rgba8(); 105 | 106 | IconData { 107 | data: image.to_vec(), 108 | width: image.width(), 109 | height: image.height(), 110 | } 111 | } 112 | 113 | lazy_static! { 114 | static ref RGB_DATA: IconData = load_icon(); 115 | } 116 | 117 | impl AstroGameConfig { 118 | fn get_api(&self) -> GithubApi { 119 | let mut api = GithubApi::new("AstroTechies", "astro_modloader"); 120 | api.current_version(cargo_crate_version!()); 121 | api.prerelease(true); 122 | api 123 | } 124 | 125 | fn get_newer_release(&self, api: &GithubApi) -> Result, ModLoaderError> { 126 | api.get_newer(&None) 127 | .map_err(|e| ModLoaderError::other(e.to_string())) 128 | } 129 | } 130 | 131 | impl GameConfig<'static, AstroIntegratorConfig, D, E> 132 | for AstroGameConfig 133 | where 134 | AstroIntegratorConfig: IntegratorConfig<'static, D, E>, 135 | { 136 | fn get_integrator_config(&self) -> &AstroIntegratorConfig { 137 | &AstroIntegratorConfig 138 | } 139 | 140 | fn get_game_build(&self, install_path: &Path) -> Option { 141 | let version_file_path = install_path.join("build.version"); 142 | if !version_file_path.is_file() { 143 | info!("{:?} not found", version_file_path); 144 | return None; 145 | } 146 | 147 | let version_file = std::fs::read_to_string(&version_file_path).unwrap(); 148 | let game_build_string = version_file.split(' ').next().unwrap().to_owned(); 149 | 150 | GameBuild::try_from(&game_build_string).ok() 151 | } 152 | 153 | const WINDOW_TITLE: &'static str = "Astroneer Modloader"; 154 | const CONFIG_DIR: &'static str = "AstroModLoader"; 155 | const CRATE_VERSION: &'static str = cargo_crate_version!(); 156 | const ABOUT_TEXT: &'static str = concat!( 157 | "# License information: \n", 158 | include_str!(concat!(env!("OUT_DIR"), "/licenses.md")) 159 | ); 160 | 161 | fn get_install_managers( 162 | &self, 163 | ) -> std::collections::BTreeMap<&'static str, Box> { 164 | let mut managers: std::collections::BTreeMap<&'static str, Box> = 165 | BTreeMap::new(); 166 | 167 | #[cfg(windows)] 168 | managers.insert( 169 | "Steam", 170 | Box::new(SteamInstallManager::new( 171 | 361420, 172 | AstroIntegratorConfig::GAME_NAME, 173 | Box::::default(), 174 | )), 175 | ); 176 | #[cfg(windows)] 177 | managers.insert( 178 | "Microsoft Store", 179 | Box::new(MsStoreInstallManager::new( 180 | "SystemEraSoftworks", 181 | "ASTRONEER", 182 | "Astro", 183 | )), 184 | ); 185 | 186 | #[cfg(target_os = "linux")] 187 | managers.insert( 188 | "Steam (Proton)", 189 | Box::new(ProtonInstallManager::new( 190 | 361420, 191 | AstroIntegratorConfig::GAME_NAME, 192 | #[allow(clippy::box_default)] 193 | Box::new(ProtonGetGameBuild::default()), 194 | )), 195 | ); 196 | 197 | managers 198 | } 199 | 200 | fn get_newer_update(&self) -> Result, ModLoaderError> { 201 | let api = self.get_api(); 202 | let download = self.get_newer_release(&api)?; 203 | 204 | if let Some(download) = download { 205 | return Ok(Some(UpdateInfo::new(download.tag_name, download.body))); 206 | } 207 | 208 | Ok(None) 209 | } 210 | 211 | fn update_modloader(&self, callback: Box) -> Result<(), ModLoaderError> { 212 | let api = self.get_api(); 213 | let download = self.get_newer_release(&api)?; 214 | 215 | if let Some(download) = download { 216 | let asset = &download.assets[0]; 217 | api.download(asset, Some(callback)) 218 | .map_err(|e| ModLoaderError::other(e.to_string()))?; 219 | } 220 | Ok(()) 221 | } 222 | 223 | fn get_icon(&self) -> Option { 224 | Some(RGB_DATA.clone()) 225 | } 226 | 227 | fn get_cpp_loader_config() -> GameSettings { 228 | GameSettings { 229 | is_using_fchunked_fixed_uobject_array: true, 230 | uses_fname_pool: true, 231 | function_info_settings: Some(FunctionInfo::Patterns(FunctionInfoPatterns { 232 | call_function_by_name_with_arguments: Some("41 57 41 56 41 55 41 54 56 57 55 53 48 81 EC ? ? ? ? 44 0F 29 BC 24 ? ? ? ? 44 0F 29 B4 24 ? ? ? ? 44 0F 29 AC 24 ? ? ? ? 44 0F 29 A4 24 ? ? ? ? 44 0F 29 9C 24 ? ? ? ? 44 0F 29 94 24 ? ? ? ? 44 0F 29 8C 24 ? ? ? ? 44 0F 29 84 24 ? ? ? ? 0F 29 BC 24 ? ? ? ? 0F 29 B4 24 ? ? ? ? 48 8B 8C 24 ? ? ? ? 48 8B 94 24 ? ? ? ? 48 8B 84 24 ? ? ? ? 4C 8B 40 18 0F 28 3D".to_string()), 233 | create_default_object: Some("48 8B C4 55 41 54 41 55 41 56 41 57 48 8D A8 ? ? ? ? 48 81 EC ? ? ? ? 48 C7 45 ? ? ? ? ? 48 89 58 10 48 89 70 18 48 89 78 20 48 8B 05 ? ? ? ? 48 33 C4 48 89 85 ? ? ? ? 48 8B F9 48 83 B9 ? ? ? ? ? 0F 85 ? ? ? ? 48 8B 59 40 45 33 FF 48 85 DB 74 2E B2 01 48 8B CB".to_string()), 234 | ..Default::default() 235 | })), 236 | ..Default::default() 237 | } 238 | } 239 | } 240 | 241 | fn main() { 242 | logging::init().unwrap(); 243 | 244 | info!("Astroneer Modloader"); 245 | 246 | let config = AstroGameConfig; 247 | 248 | unreal_mod_manager::run(config); 249 | 250 | logging::flush(); 251 | } 252 | --------------------------------------------------------------------------------