├── .gitignore ├── misc ├── tse.ico ├── tse.png ├── screenshots │ ├── ME1_Raw.png │ ├── ME2_Raw.png │ ├── ME3_Raw.png │ ├── ME1LE_Raw.png │ ├── ME2_Plot.png │ ├── ME3_Plot.png │ ├── ME1LE_Plot.png │ ├── ME1_General.png │ ├── ME2_General.png │ ├── ME2_Raw_Plot.png │ ├── ME2_Research.png │ ├── ME3_General.png │ ├── ME3_Raw_Plot.png │ ├── ME3_Weapons.png │ ├── ME1LE_General.png │ ├── ME1LE_Raw_Plot.png │ ├── ME2_Head_Morph.png │ ├── ME3_Head_Morph.png │ ├── ME1LE_Head_Morph.png │ └── ME1LE_Inventory.png └── 010 templates │ ├── me1_state.bt │ └── me1_player.bt ├── .rustfmt.toml ├── test ├── ME2Save.pcsav ├── ME3Save.pcsav ├── ME1LeSave.pcsav ├── ME2LeSave.pcsav ├── ME1LeExport.pcsav ├── ME1Save │ ├── player.sav │ ├── player.upk │ ├── state.sav │ └── WorldSavePackage.sav ├── ME2Save360.xbsav ├── ME3Save360.xbsav ├── GibbedME2.me2headmorph ├── GibbedME3.me3headmorph ├── ME1LeSave.uncompressed ├── ME1Save.MassEffectSave └── ME1Export.MassEffectSave ├── src ├── services │ ├── mod.rs │ ├── rpc.js │ ├── drop_handler.rs │ └── rpc.rs ├── gui │ ├── mass_effect_1 │ │ ├── mod.rs │ │ ├── raw_plot.rs │ │ ├── raw_data │ │ │ └── mod.rs │ │ └── plot.rs │ ├── components │ │ ├── raw_ui │ │ │ ├── mod.rs │ │ │ ├── raw_ui_enum.rs │ │ │ ├── raw_ui_guid.rs │ │ │ ├── raw_ui_struct.rs │ │ │ ├── raw_ui_option.rs │ │ │ └── raw_ui_vec.rs │ │ ├── mod.rs │ │ ├── check_box.rs │ │ ├── input_text.rs │ │ ├── helper.rs │ │ ├── input_number.rs │ │ ├── color_picker.rs │ │ ├── table.rs │ │ ├── auto_update.rs │ │ ├── select.rs │ │ └── tab_bar.rs │ ├── mass_effect_3 │ │ ├── mod.rs │ │ ├── raw_plot.rs │ │ ├── plot_variable.rs │ │ └── plot.rs │ ├── mass_effect_2 │ │ ├── mod.rs │ │ └── raw_plot.rs │ ├── mod.rs │ ├── shared │ │ ├── link.rs │ │ ├── mod.rs │ │ ├── head_morph.rs │ │ ├── plot_category.rs │ │ └── bonus_powers.rs │ ├── mass_effect_1_le │ │ ├── mod.rs │ │ └── bonus_talents.rs │ └── raw_ui.rs ├── save_data │ ├── mass_effect_2 │ │ ├── galaxy_map.rs │ │ ├── squad.rs │ │ ├── plot_db.rs │ │ └── player.rs │ ├── mass_effect_3 │ │ ├── squad.rs │ │ ├── galaxy_map.rs │ │ ├── plot.rs │ │ ├── plot_db.rs │ │ └── player.rs │ ├── shared │ │ ├── player.rs │ │ ├── plot.rs │ │ ├── mod.rs │ │ └── appearance.rs │ ├── mass_effect_1_le │ │ ├── item_db.rs │ │ ├── legacy │ │ │ ├── art_placeable.rs │ │ │ ├── inventory.rs │ │ │ └── pawn.rs │ │ ├── squad.rs │ │ ├── player_class_db.rs │ │ └── player.rs │ └── mass_effect_1 │ │ ├── plot_db.rs │ │ └── state.rs ├── main.rs └── unreal │ └── mod.rs ├── macros ├── Cargo.toml └── src │ ├── lib.rs │ └── raw_ui.rs ├── app ├── build.rs ├── Cargo.toml └── src │ ├── init.js │ ├── windows │ ├── mod.rs │ └── auto_update.rs │ ├── rpc │ ├── dialog.rs │ ├── command.rs │ └── mod.rs │ └── main.rs ├── package.json ├── index.html ├── tailwind.config.js ├── README.md ├── Cargo.toml ├── .github └── workflows │ └── CI.yml ├── Makefile.toml └── InnoSetup.iss /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /node_modules 3 | /target 4 | -------------------------------------------------------------------------------- /misc/tse.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/tse.ico -------------------------------------------------------------------------------- /misc/tse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/tse.png -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | newline_style = "Unix" 2 | fn_args_layout = "Compressed" 3 | use_small_heuristics = "Max" -------------------------------------------------------------------------------- /test/ME2Save.pcsav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/ME2Save.pcsav -------------------------------------------------------------------------------- /test/ME3Save.pcsav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/ME3Save.pcsav -------------------------------------------------------------------------------- /test/ME1LeSave.pcsav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/ME1LeSave.pcsav -------------------------------------------------------------------------------- /test/ME2LeSave.pcsav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/ME2LeSave.pcsav -------------------------------------------------------------------------------- /src/services/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod database; 2 | pub mod drop_handler; 3 | pub mod rpc; 4 | pub mod save_handler; 5 | -------------------------------------------------------------------------------- /test/ME1LeExport.pcsav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/ME1LeExport.pcsav -------------------------------------------------------------------------------- /test/ME1Save/player.sav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/ME1Save/player.sav -------------------------------------------------------------------------------- /test/ME1Save/player.upk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/ME1Save/player.upk -------------------------------------------------------------------------------- /test/ME1Save/state.sav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/ME1Save/state.sav -------------------------------------------------------------------------------- /test/ME2Save360.xbsav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/ME2Save360.xbsav -------------------------------------------------------------------------------- /test/ME3Save360.xbsav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/ME3Save360.xbsav -------------------------------------------------------------------------------- /misc/screenshots/ME1_Raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME1_Raw.png -------------------------------------------------------------------------------- /misc/screenshots/ME2_Raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME2_Raw.png -------------------------------------------------------------------------------- /misc/screenshots/ME3_Raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME3_Raw.png -------------------------------------------------------------------------------- /test/GibbedME2.me2headmorph: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/GibbedME2.me2headmorph -------------------------------------------------------------------------------- /test/GibbedME3.me3headmorph: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/GibbedME3.me3headmorph -------------------------------------------------------------------------------- /test/ME1LeSave.uncompressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/ME1LeSave.uncompressed -------------------------------------------------------------------------------- /test/ME1Save.MassEffectSave: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/ME1Save.MassEffectSave -------------------------------------------------------------------------------- /misc/screenshots/ME1LE_Raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME1LE_Raw.png -------------------------------------------------------------------------------- /misc/screenshots/ME2_Plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME2_Plot.png -------------------------------------------------------------------------------- /misc/screenshots/ME3_Plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME3_Plot.png -------------------------------------------------------------------------------- /test/ME1Export.MassEffectSave: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/ME1Export.MassEffectSave -------------------------------------------------------------------------------- /misc/screenshots/ME1LE_Plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME1LE_Plot.png -------------------------------------------------------------------------------- /misc/screenshots/ME1_General.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME1_General.png -------------------------------------------------------------------------------- /misc/screenshots/ME2_General.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME2_General.png -------------------------------------------------------------------------------- /misc/screenshots/ME2_Raw_Plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME2_Raw_Plot.png -------------------------------------------------------------------------------- /misc/screenshots/ME2_Research.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME2_Research.png -------------------------------------------------------------------------------- /misc/screenshots/ME3_General.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME3_General.png -------------------------------------------------------------------------------- /misc/screenshots/ME3_Raw_Plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME3_Raw_Plot.png -------------------------------------------------------------------------------- /misc/screenshots/ME3_Weapons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME3_Weapons.png -------------------------------------------------------------------------------- /test/ME1Save/WorldSavePackage.sav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/test/ME1Save/WorldSavePackage.sav -------------------------------------------------------------------------------- /misc/screenshots/ME1LE_General.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME1LE_General.png -------------------------------------------------------------------------------- /misc/screenshots/ME1LE_Raw_Plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME1LE_Raw_Plot.png -------------------------------------------------------------------------------- /misc/screenshots/ME2_Head_Morph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME2_Head_Morph.png -------------------------------------------------------------------------------- /misc/screenshots/ME3_Head_Morph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME3_Head_Morph.png -------------------------------------------------------------------------------- /misc/screenshots/ME1LE_Head_Morph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME1LE_Head_Morph.png -------------------------------------------------------------------------------- /misc/screenshots/ME1LE_Inventory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarlitosVII/trilogy-save-editor/HEAD/misc/screenshots/ME1LE_Inventory.png -------------------------------------------------------------------------------- /src/gui/mass_effect_1/mod.rs: -------------------------------------------------------------------------------- 1 | mod general; 2 | mod plot; 3 | mod raw_data; 4 | mod raw_plot; 5 | 6 | pub use self::{general::*, plot::*, raw_data::*, raw_plot::*}; 7 | -------------------------------------------------------------------------------- /src/services/rpc.js: -------------------------------------------------------------------------------- 1 | 2 | export async function call(method) { 3 | return window.rpc.call(method); 4 | } 5 | 6 | export async function call_with_params(method, params) { 7 | return window.rpc.call(method, params); 8 | } -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "macros" 3 | version = "2.0.0" 4 | edition = "2018" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | syn = "1.0" 11 | quote = "1.0" 12 | proc-macro2 = "1.0" 13 | heck = "0.4" -------------------------------------------------------------------------------- /src/gui/components/raw_ui/mod.rs: -------------------------------------------------------------------------------- 1 | mod raw_ui_enum; 2 | mod raw_ui_guid; 3 | mod raw_ui_index_map; 4 | mod raw_ui_option; 5 | mod raw_ui_struct; 6 | mod raw_ui_vec; 7 | 8 | pub use self::{ 9 | raw_ui_enum::*, raw_ui_guid::*, raw_ui_index_map::*, raw_ui_option::*, raw_ui_struct::*, 10 | raw_ui_vec::*, 11 | }; 12 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_2/galaxy_map.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::save_data::shared::Vector2d; 4 | 5 | #[rcize_fields] 6 | #[derive(Deserialize, Serialize, Clone, RawUi)] 7 | pub struct GalaxyMap { 8 | planets: Vec, 9 | } 10 | 11 | #[rcize_fields] 12 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 13 | #[display(fmt = "{}", id)] 14 | pub struct Planet { 15 | id: i32, 16 | visited: bool, 17 | probes: Vec, 18 | } 19 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_2/squad.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::player::Power; 4 | use crate::save_data::shared::player::WeaponLoadout; 5 | 6 | #[rcize_fields] 7 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 8 | #[display(fmt = "{}", tag)] 9 | pub struct Henchman { 10 | tag: String, 11 | powers: Vec, 12 | character_level: i32, 13 | talent_points: i32, 14 | weapon_loadout: WeaponLoadout, 15 | mapped_power: String, 16 | } 17 | -------------------------------------------------------------------------------- /src/gui/components/mod.rs: -------------------------------------------------------------------------------- 1 | mod auto_update; 2 | mod check_box; 3 | mod color_picker; 4 | mod helper; 5 | mod input_number; 6 | mod input_text; 7 | mod nav_bar; 8 | pub mod raw_ui; 9 | mod select; 10 | mod tab_bar; 11 | mod table; 12 | 13 | pub use self::{ 14 | auto_update::*, check_box::*, color_picker::*, helper::*, input_number::*, input_text::*, 15 | nav_bar::*, select::*, tab_bar::*, table::*, 16 | }; 17 | 18 | pub enum CallbackType { 19 | Byte(u8), 20 | Int(i32), 21 | Float(f32), 22 | String(String), 23 | } 24 | -------------------------------------------------------------------------------- /src/gui/mass_effect_3/mod.rs: -------------------------------------------------------------------------------- 1 | mod general; 2 | mod plot; 3 | mod plot_variable; 4 | mod raw_plot; 5 | 6 | pub use self::{general::*, plot::*, plot_variable::*, raw_plot::*}; 7 | 8 | use yew::prelude::*; 9 | 10 | use crate::{ 11 | gui::{raw_ui::RawUi, shared::Link}, 12 | save_data::{mass_effect_3::plot::PlotTable, RcRef}, 13 | }; 14 | 15 | impl RawUi for RcRef { 16 | fn view(&self, _: &str) -> yew::Html { 17 | html! { 18 | { "Raw Plot" } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "windows")] 2 | fn main() { 3 | if std::env::var("PROFILE").unwrap() == "release" { 4 | let mut res = winres::WindowsResource::new(); 5 | 6 | res.set("ProductName", "Trilogy Save Editor"); 7 | res.set("FileDescription", "Trilogy Save Editor"); 8 | res.set_icon("../misc/tse.ico"); 9 | 10 | if let Err(err) = res.compile() { 11 | eprint!("{}", err); 12 | std::process::exit(1); 13 | } 14 | } 15 | } 16 | 17 | #[cfg(not(target_os = "windows"))] 18 | fn main() {} 19 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_3/squad.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::player::{Power, Weapon, WeaponMod}; 4 | use crate::save_data::shared::player::WeaponLoadout; 5 | 6 | #[rcize_fields] 7 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 8 | #[display(fmt = "{}", tag)] 9 | pub struct Henchman { 10 | tag: String, 11 | powers: Vec, 12 | character_level: i32, 13 | talent_points: i32, 14 | weapon_loadout: WeaponLoadout, 15 | mapped_power: String, 16 | weapon_mods: Vec, 17 | grenades: i32, 18 | weapons: Vec, 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trilogy-save-editor", 3 | "author": "Karlitos", 4 | "license": "CECILL-2.1", 5 | "description": "A save editor for Mass Effect Trilogy (and Legendary)", 6 | "dependencies": {}, 7 | "scripts": { 8 | "build": "tailwindcss -i index.css -o target/index.css --jit", 9 | "release": "tailwindcss -i index.css -o target/index.css --jit --minify", 10 | "watch": "tailwindcss -i index.css -o target/index.css --jit --watch" 11 | }, 12 | "repository": { 13 | "type": "git" 14 | }, 15 | "devDependencies": { 16 | "autoprefixer": "^10.4.2", 17 | "postcss": "^8.4.5", 18 | "tailwindcss": "^3.0.13" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all)] 2 | 3 | #[macro_use] 4 | extern crate derive_more; 5 | 6 | #[macro_use] 7 | extern crate macros; 8 | 9 | mod gui; 10 | mod save_data; 11 | mod services; 12 | mod unreal; 13 | 14 | use gui::App; 15 | 16 | fn main() { 17 | let document = gloo::utils::document(); 18 | let body = document.body().unwrap(); 19 | let mount_point = body.last_element_child().unwrap(); 20 | 21 | document.get_element_by_id("title").unwrap().set_text_content(Some(&format!( 22 | "Trilogy Save Editor - v{} by Karlitos", 23 | env!("CARGO_PKG_VERSION") 24 | ))); 25 | 26 | yew::start_app_in_element::(mount_point); 27 | } 28 | -------------------------------------------------------------------------------- /src/save_data/shared/player.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Deserialize, Serialize, Copy, Clone, RawUi)] 4 | pub enum Origin { 5 | None, 6 | Spacer, 7 | Colonist, 8 | Earthborn, 9 | } 10 | 11 | #[derive(Deserialize, Serialize, Copy, Clone, RawUi)] 12 | pub enum Notoriety { 13 | None, 14 | Survivor, 15 | Warhero, 16 | Ruthless, 17 | } 18 | 19 | #[rcize_fields] 20 | #[derive(Deserialize, Serialize, Clone, Default, RawUi)] 21 | pub struct WeaponLoadout { 22 | assault_rifle: String, 23 | shotgun: String, 24 | sniper_rifle: String, 25 | submachine_gun: String, 26 | pistol: String, 27 | heavy_weapon: String, 28 | } 29 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_1_le/item_db.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::Deserialize; 3 | 4 | #[derive(Deserialize, Copy, Clone, PartialEq, Eq, Hash)] 5 | pub struct DbItem { 6 | pub item_id: i32, 7 | pub manufacturer_id: i32, 8 | } 9 | 10 | #[derive(Deserialize, Deref, From, PartialEq, Eq)] 11 | pub struct Me1ItemDb(IndexMap); 12 | 13 | #[cfg(test)] 14 | mod test { 15 | use std::fs; 16 | 17 | use anyhow::Result; 18 | 19 | use super::*; 20 | 21 | #[test] 22 | fn deserialize_item_db() -> Result<()> { 23 | let input = fs::read_to_string("databases/me1_item_db.ron")?; 24 | let _me1_item_db: Me1ItemDb = ron::from_str(&input)?; 25 | 26 | Ok(()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/gui/mass_effect_2/mod.rs: -------------------------------------------------------------------------------- 1 | mod general; 2 | mod plot; 3 | mod raw_plot; 4 | 5 | pub use self::{general::*, plot::*, raw_plot::*}; 6 | 7 | use crate::save_data::{ 8 | mass_effect_2::{Me2LeSaveGame, Me2SaveGame}, 9 | RcRef, 10 | }; 11 | 12 | #[derive(Clone)] 13 | pub enum Me2Type { 14 | Vanilla(RcRef), 15 | Legendary(RcRef), 16 | } 17 | 18 | impl PartialEq for Me2Type { 19 | fn eq(&self, other: &Me2Type) -> bool { 20 | match (self, other) { 21 | (Me2Type::Vanilla(vanilla), Me2Type::Vanilla(other)) => vanilla == other, 22 | (Me2Type::Legendary(legendary), Me2Type::Legendary(other)) => legendary == other, 23 | _ => false, 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_3/galaxy_map.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::save_data::shared::Vector2d; 4 | 5 | #[rcize_fields] 6 | #[derive(Deserialize, Serialize, Clone, RawUi)] 7 | pub struct GalaxyMap { 8 | planets: Vec, 9 | systems: Vec, 10 | } 11 | 12 | #[rcize_fields] 13 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 14 | #[display(fmt = "{}", id)] 15 | pub struct Planet { 16 | id: i32, 17 | visited: bool, 18 | probes: Vec, 19 | show_as_scanned: bool, 20 | } 21 | 22 | #[rcize_fields] 23 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 24 | #[display(fmt = "{}", id)] 25 | pub struct System { 26 | id: i32, 27 | reaper_alert_level: f32, 28 | reaper_detected: bool, 29 | } 30 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_1_le/legacy/art_placeable.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::BaseObject; 4 | use crate::save_data::Dummy; 5 | 6 | #[rcize_fields] 7 | #[derive(Deserialize, Serialize, Clone, RawUiChildren)] 8 | pub struct ArtPlaceableBehavior { 9 | is_dead: bool, 10 | generated_treasure: bool, 11 | challenge_scaled: bool, 12 | owner: Option, 13 | health: f32, 14 | current_health: f32, 15 | enabled: bool, 16 | current_fsm_state_name: String, 17 | is_destroyed: bool, 18 | state_0: String, 19 | state_1: String, 20 | use_case: u8, 21 | use_case_override: bool, 22 | player_only: bool, 23 | skill_difficulty: u8, 24 | inventory: Option, 25 | skill_game_failed: bool, 26 | skill_game_xp_awarded: bool, 27 | } 28 | 29 | #[rcize_fields] 30 | #[derive(Deserialize, Serialize, Clone, RawUiChildren)] 31 | pub struct ArtPlaceable { 32 | _unknown: Dummy<60>, 33 | } 34 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_1/plot_db.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::Deserialize; 3 | 4 | use crate::save_data::shared::plot::PlotCategory; 5 | 6 | #[derive(Deserialize)] 7 | pub struct Me1PlotDb { 8 | pub player_crew: IndexMap, 9 | pub missions: IndexMap, 10 | } 11 | 12 | #[cfg(test)] 13 | mod test { 14 | use anyhow::Result; 15 | use std::fs; 16 | 17 | use crate::save_data::shared::plot::RawPlotDb; 18 | 19 | use super::*; 20 | 21 | #[test] 22 | fn deserialize_plot_db() -> Result<()> { 23 | let input = fs::read_to_string("databases/me1_plot_db.ron")?; 24 | let _me1_plot_db: Me1PlotDb = ron::from_str(&input)?; 25 | 26 | Ok(()) 27 | } 28 | 29 | #[test] 30 | fn deserialize_raw_plot_db() -> Result<()> { 31 | let input = fs::read_to_string("databases/me1_raw_plot_db.ron")?; 32 | let _me1_raw_plot_db: RawPlotDb = ron::from_str(&input)?; 33 | 34 | Ok(()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_3/plot.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::save_data::shared::plot::{BitVec, PlotCodex}; 5 | 6 | #[rcize_fields] 7 | #[derive(Deserialize, Serialize, Clone)] 8 | pub struct PlotTable { 9 | pub booleans: BitVec, 10 | pub integers: IndexMap, 11 | pub floats: IndexMap, 12 | } 13 | 14 | #[rcize_fields] 15 | #[derive(Deserialize, Serialize, Clone, RawUi)] 16 | pub struct Journal { 17 | quest_progress_counter: i32, 18 | quest_progress: Vec, 19 | quest_ids: Vec, 20 | } 21 | 22 | #[rcize_fields] 23 | #[derive(Deserialize, Serialize, Clone, RawUi)] 24 | pub struct Codex { 25 | codex_entries: Vec, 26 | codex_ids: Vec, 27 | } 28 | 29 | #[rcize_fields] 30 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 31 | #[display(fmt = "{}", quest_counter)] 32 | pub struct PlotQuest { 33 | quest_counter: i32, 34 | quest_updated: bool, 35 | active_goal: i32, 36 | history: Vec, 37 | } 38 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_1_le/squad.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::player::{ComplexTalent, Item, SimpleTalent}; 4 | 5 | #[rcize_fields] 6 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 7 | #[display(fmt = "{}", tag)] 8 | pub struct Henchman { 9 | pub tag: String, 10 | simple_talents: Vec, 11 | pub complex_talents: Vec, 12 | pub equipment: Vec, 13 | pub quick_slots: Vec, 14 | pub talent_points: i32, 15 | talent_pool_points: i32, 16 | auto_levelup_template_id: i32, 17 | localized_last_name: i32, 18 | localized_class_name: i32, 19 | class_base: u8, 20 | health_per_level: f32, 21 | stability: f32, 22 | gender: u8, 23 | race: u8, 24 | toxic: f32, 25 | stamina: i32, 26 | focus: i32, 27 | precision: i32, 28 | coordination: i32, 29 | attribute_primary: u8, 30 | attribute_secondary: u8, 31 | health: f32, 32 | shield: f32, 33 | level: i32, 34 | helmet_shown: bool, 35 | current_quick_slot: u8, 36 | health_max: f32, 37 | } 38 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | Trilogy Save Editor - by Karlitos 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | Trilogy Save Editor - by Karlitos 19 | 20 |
21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app" 3 | version = "2.2.1" 4 | edition = "2021" 5 | rust-version = "1.56.0" 6 | 7 | [[bin]] 8 | name = "trilogy-save-editor" 9 | path = "src/main.rs" 10 | 11 | [target.'cfg(target_os="windows")'.build-dependencies] 12 | winres = "0.1" 13 | 14 | [dependencies] 15 | # Std-like 16 | anyhow = "1.0" 17 | # Async 18 | tokio = { version = "1.0", features = [ 19 | "rt-multi-thread", 20 | "sync", 21 | "parking_lot", 22 | "macros", 23 | "fs", 24 | "io-util", 25 | "process", 26 | ], default-features = false } 27 | parking_lot = "0.11" 28 | # Utils 29 | lazy_static = "1.0" 30 | clap = "3.0" 31 | mime_guess = "2.0" 32 | rust-embed = { version = "6.0", default-features = false } 33 | dirs = "4.0" 34 | rfd = "0.5" 35 | base64 = "0.13" 36 | opener = "0.5" 37 | image = { version = "0.23", features = ["png"], default-features = false } 38 | # Http 39 | reqwest = { version = "0.11", features = ["json"] } 40 | # (De)Serialize 41 | serde = { version = "1.0", features = ["derive"], default-features = false } 42 | serde_json = "1.0" 43 | # WebView 44 | wry = { version = "0.12", features = ["protocol"], default-features = false } 45 | -------------------------------------------------------------------------------- /src/unreal/mod.rs: -------------------------------------------------------------------------------- 1 | mod deserializer; 2 | mod serializer; 3 | 4 | pub use self::{deserializer::*, serializer::*}; 5 | 6 | use std::fmt::{self, Display}; 7 | 8 | use serde::{de, ser}; 9 | 10 | pub type Result = std::result::Result; 11 | 12 | #[derive(Clone, Debug)] 13 | pub enum Error { 14 | Message(String), 15 | Eof, 16 | } 17 | 18 | impl ser::Error for Error { 19 | fn custom(msg: T) -> Error { 20 | Error::Message(msg.to_string()) 21 | } 22 | } 23 | 24 | impl de::Error for Error { 25 | fn custom(msg: T) -> Error { 26 | Error::Message(msg.to_string()) 27 | } 28 | } 29 | 30 | impl Display for Error { 31 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 32 | match self { 33 | Error::Message(msg) => formatter.write_str(msg), 34 | Error::Eof => formatter.write_str( 35 | "Unexpected end of file, some data in your save are unexpected or your save is corrupted ?\n\ 36 | Save again and retry. If this error persists, please report a bug with your save attached"), 37 | } 38 | } 39 | } 40 | 41 | impl std::error::Error for Error {} 42 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_1_le/player_class_db.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::save_data::RcRef; 4 | 5 | use super::player::{ComplexTalent, Item, Me1LeClass, SimpleTalent}; 6 | 7 | #[derive(Deserialize)] 8 | pub struct Me1LeSpecializationBonus { 9 | pub id: i32, 10 | pub label: String, 11 | } 12 | 13 | #[derive(Deserialize)] 14 | pub struct Me1LePlayerClass { 15 | pub player_class: Me1LeClass, 16 | pub localized_class_name: i32, 17 | pub auto_levelup_template_id: i32, 18 | pub simple_talents: Vec>, 19 | pub complex_talents: Vec>, 20 | pub armor: Item, 21 | pub omni_tool: Item, 22 | pub bio_amp: Item, 23 | pub bonus_talents: RcRef>, 24 | } 25 | 26 | #[derive(Deserialize, Deref)] 27 | pub struct Me1LePlayerClassDb(Vec); 28 | 29 | #[cfg(test)] 30 | mod test { 31 | use std::fs; 32 | 33 | use super::*; 34 | use anyhow::Result; 35 | 36 | #[test] 37 | fn deserialize_player_class_db() -> Result<()> { 38 | let input = fs::read_to_string("databases/me1_le_player_class_db.ron")?; 39 | let _me1_le_player_class_db: Me1LePlayerClassDb = ron::from_str(&input)?; 40 | 41 | Ok(()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/gui/mod.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | pub mod components; 3 | mod mass_effect_1; 4 | mod mass_effect_1_le; 5 | mod mass_effect_2; 6 | mod mass_effect_3; 7 | pub mod raw_ui; 8 | pub mod shared; 9 | 10 | pub use self::app::*; 11 | 12 | use std::ops::Deref; 13 | 14 | use yew::{html, Html}; 15 | 16 | #[derive(Copy, Clone, PartialEq)] 17 | pub enum Theme { 18 | MassEffect1, 19 | MassEffect2, 20 | MassEffect3, 21 | } 22 | 23 | impl Deref for Theme { 24 | type Target = str; 25 | 26 | fn deref(&self) -> &Self::Target { 27 | match self { 28 | Theme::MassEffect1 => "mass-effect-1", 29 | Theme::MassEffect2 => "mass-effect-2", 30 | Theme::MassEffect3 => "mass-effect-3", 31 | } 32 | } 33 | } 34 | 35 | impl From for yew::Classes { 36 | fn from(theme: Theme) -> Self { 37 | theme.to_string().into() 38 | } 39 | } 40 | 41 | pub fn format_code(text: impl AsRef) -> Html { 42 | let text = text.as_ref().split('`').enumerate().map(|(i, text)| { 43 | if i % 2 != 0 { 44 | html! { { text }} 45 | } else { 46 | html! { text } 47 | } 48 | }); 49 | html! { for text } 50 | } 51 | -------------------------------------------------------------------------------- /src/gui/components/raw_ui/raw_ui_enum.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | use crate::{gui::components::Select, save_data::RcRef}; 4 | 5 | #[derive(Properties)] 6 | pub struct Props 7 | where 8 | T: From + Into + Clone + 'static, 9 | { 10 | pub label: String, 11 | pub items: &'static [&'static str], 12 | pub value: RcRef, 13 | } 14 | 15 | impl PartialEq for Props 16 | where 17 | T: From + Into + Clone + 'static, 18 | { 19 | fn eq(&self, other: &Self) -> bool { 20 | self.label == other.label && self.items == other.items && self.value == other.value 21 | } 22 | } 23 | 24 | #[function_component(RawUiEnum)] 25 | pub fn raw_ui_enum(props: &Props) -> Html 26 | where 27 | T: From + Into + Clone + 'static, 28 | { 29 | let options = props.items; 30 | let current_idx: usize = props.value.borrow().clone().into(); 31 | let onselect = { 32 | let value = RcRef::clone(&props.value); 33 | Callback::from(move |idx| *value.borrow_mut() = T::from(idx)) 34 | }; 35 | html! { 36 |
37 | 46 | { &ctx.props().label } 47 | 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require("tailwindcss/defaultTheme") 2 | 3 | module.exports = { 4 | content: [ 5 | "./index.html", 6 | "./src/**/*.rs", 7 | ], 8 | theme: { 9 | fontFamily: { 10 | default: [defaultTheme.fontFamily.mono], 11 | }, 12 | colors: { 13 | white: "#FFFFFF", 14 | default: { 15 | bg: "#0D0D0D", 16 | border: "#333333", 17 | }, 18 | "title-bar": { 19 | bg: "#191919", 20 | close: "#AA0000", 21 | }, 22 | "menu-bar": "#242424", 23 | "scroll-bar": { 24 | bg: "#090909", 25 | fg: "#4F4F4F", 26 | }, 27 | table: { 28 | odd: "#121212", 29 | even: "#1A1A1A", 30 | }, 31 | popup: "#121212", 32 | me1: { 33 | bg: "#1C526E", 34 | tab: "#296B94", 35 | active: "#478CAB", 36 | hover: "#D46E2B", 37 | }, 38 | me2: { 39 | bg: "#A3521F", 40 | tab: "#B35E29", 41 | active: "#D97D40", 42 | hover: "#38853B", 43 | }, 44 | me3: { 45 | bg: "#660000", 46 | tab: "#870000", 47 | active: "#B30000", 48 | hover: "#05476E", 49 | }, 50 | theme: { 51 | bg: "var(--bg)", 52 | tab: "var(--tab)", 53 | active: "var(--active)", 54 | hover: "var(--hover)", 55 | }, 56 | }, 57 | extend: {}, 58 | }, 59 | variants: { 60 | extend: {}, 61 | }, 62 | plugins: [], 63 | } 64 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_3/plot_db.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::Deserialize; 3 | 4 | use crate::save_data::shared::plot::PlotCategory; 5 | 6 | #[derive(Deserialize)] 7 | pub struct Me3PlotDb { 8 | pub general: PlotCategory, 9 | pub crew: IndexMap, 10 | pub romance: IndexMap, 11 | pub missions: IndexMap, 12 | pub citadel_dlc: IndexMap, 13 | pub normandy: IndexMap, 14 | pub appearances: IndexMap, 15 | pub weapons_powers: IndexMap, 16 | pub intel: PlotCategory, 17 | } 18 | 19 | #[derive(Deserialize, Clone, PartialEq, Eq)] 20 | pub struct PlotVariable { 21 | pub booleans: IndexMap, 22 | pub variables: IndexMap, 23 | } 24 | 25 | #[cfg(test)] 26 | mod test { 27 | use std::fs; 28 | 29 | use anyhow::Result; 30 | 31 | use super::*; 32 | use crate::save_data::shared::plot::RawPlotDb; 33 | 34 | #[test] 35 | fn deserialize_plot_db() -> Result<()> { 36 | let input = fs::read_to_string("databases/me3_plot_db.ron")?; 37 | let _me3_plot_db: Me3PlotDb = ron::from_str(&input)?; 38 | 39 | Ok(()) 40 | } 41 | 42 | #[test] 43 | fn deserialize_raw_plot_db() -> Result<()> { 44 | let input = fs::read_to_string("databases/me3_raw_plot_db.ron")?; 45 | let _me3_raw_plot_db: RawPlotDb = ron::from_str(&input)?; 46 | 47 | Ok(()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all)] 2 | 3 | mod raw_ui; 4 | mod rcize; 5 | 6 | use proc_macro::TokenStream; 7 | use syn::parse_macro_input; 8 | use syn::{self, DeriveInput}; 9 | 10 | use crate::raw_ui::Derive; 11 | 12 | #[proc_macro_attribute] 13 | pub fn rcize_fields(_: TokenStream, input: TokenStream) -> TokenStream { 14 | rcize::rcize_fields(input) 15 | } 16 | 17 | #[proc_macro_derive(RawUi)] 18 | pub fn raw_ui_derive(input: TokenStream) -> TokenStream { 19 | let ast = parse_macro_input!(input as DeriveInput); 20 | 21 | match ast.data { 22 | syn::Data::Struct(ref s) => raw_ui::impl_struct(&ast, &s.fields, Derive::RawUi), 23 | syn::Data::Enum(ref e) => raw_ui::impl_enum(&ast, &e.variants), 24 | _ => panic!("union not supported"), 25 | } 26 | .into() 27 | } 28 | 29 | #[proc_macro_derive(RawUiRoot)] 30 | pub fn raw_ui_derive_root(input: TokenStream) -> TokenStream { 31 | let ast = parse_macro_input!(input as DeriveInput); 32 | 33 | match ast.data { 34 | syn::Data::Struct(ref s) => raw_ui::impl_struct(&ast, &s.fields, Derive::RawUiRoot), 35 | _ => panic!("enum / union not supported"), 36 | } 37 | .into() 38 | } 39 | 40 | #[proc_macro_derive(RawUiChildren)] 41 | pub fn raw_ui_children_derive(input: TokenStream) -> TokenStream { 42 | let ast = parse_macro_input!(input as DeriveInput); 43 | 44 | match ast.data { 45 | syn::Data::Struct(ref s) => raw_ui::impl_struct(&ast, &s.fields, Derive::RawUiChildren), 46 | _ => panic!("enum / union not supported"), 47 | } 48 | .into() 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trilogy Save Editor 2 | A save editor for Mass Effect Trilogy (and Legendary) 3 | 4 | ![Screenshot](misc/screenshots/ME3_General.png) 5 | 6 | ## Features 7 | This software is similar to Gibbed's save editors (and forks) but adds a lot of stuff. 8 | 9 | - Support for the 2 Mass Effect Trilogies (Original and Legendary) 10 | - 100% of the saves can be edited (except ME1OT) 11 | - Thousands of plot flags 12 | - Bioware's plot database 13 | - Import / Export head morph 14 | - ME1LE inventory management 15 | - Multiple bonus powers for all games (except ME1OT) 16 | - Xbox 360 and PS4 support 17 | - Free (as freedom) and open-source software with [CeCILL license](https://en.wikipedia.org/wiki/CeCILL) 18 | 19 | ## Frequently Asked Questions 20 | You can read the [FAQ here](https://github.com/KarlitosVII/trilogy-save-editor/wiki/Frequently-Asked-Questions). 21 | 22 | ## Command line usage 23 | ``` 24 | USAGE: 25 | trilogy_save_editor(.exe) [FLAGS] [SAVE] 26 | 27 | FLAGS: 28 | -h, --help Prints help information 29 | -V, --version Prints version information 30 | 31 | ARGS: 32 | Mass Effect save file 33 | ``` 34 | 35 | ## Acknowledgments 36 | 37 | - The whole ME3Explorer team (https://github.com/ME3Explorer/ME3Explorer) 38 | - Gibbed (https://github.com/gibbed) 39 | - Bioware / EA (https://github.com/electronicarts/MELE_ModdingSupport) 40 | 41 | Without them I could not have done anything. 42 | 43 | 44 | ## Compile from source 45 | 46 | ```sh 47 | rustup target add wasm32-unknown-unknown 48 | npm install 49 | cargo install cargo-make 50 | cargo make release 51 | ``` 52 | -------------------------------------------------------------------------------- /src/gui/shared/link.rs: -------------------------------------------------------------------------------- 1 | use gloo::utils; 2 | use wasm_bindgen::JsValue; 3 | use web_sys::{PopStateEvent, PopStateEventInit}; 4 | use yew::prelude::*; 5 | 6 | pub enum Msg { 7 | Clicked, 8 | } 9 | 10 | #[derive(Properties, PartialEq)] 11 | pub struct Props { 12 | pub tab: String, 13 | pub children: Children, 14 | } 15 | 16 | pub struct Link; 17 | 18 | impl Component for Link { 19 | type Message = Msg; 20 | type Properties = Props; 21 | 22 | fn create(_ctx: &Context) -> Self { 23 | Link 24 | } 25 | 26 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 27 | match msg { 28 | Msg::Clicked => { 29 | let window = utils::window(); 30 | 31 | let main_tab = JsValue::from_str(&ctx.props().tab); 32 | // let history = window.history().expect("no history"); 33 | // history.push_state(&main_tab, "").expect("push history"); 34 | 35 | let mut state = PopStateEventInit::new(); 36 | state.state(&main_tab); 37 | if let Ok(event) = PopStateEvent::new_with_event_init_dict("popstate", &state) { 38 | let _ = window.dispatch_event(&event); 39 | } 40 | false 41 | } 42 | } 43 | } 44 | 45 | fn view(&self, ctx: &Context) -> Html { 46 | html! { 47 | 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/gui/mass_effect_3/raw_plot.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use yew::prelude::*; 4 | 5 | use crate::{ 6 | gui::{ 7 | components::{Tab, TabBar}, 8 | shared::{FloatPlotType, IntPlotType, PlotType, RawPlot}, 9 | }, 10 | save_data::{shared::plot::BitVec, RcRef}, 11 | services::database::Databases, 12 | }; 13 | 14 | #[derive(Properties, PartialEq)] 15 | pub struct Props { 16 | pub booleans: RcRef, 17 | pub integers: IntPlotType, 18 | pub floats: FloatPlotType, 19 | } 20 | 21 | #[function_component(Me3RawPlot)] 22 | pub fn me3_raw_plot(props: &Props) -> Html { 23 | let dbs = use_context::().expect("no database provider"); 24 | if let Some(ref plot_db) = dbs.get_me3_raw_plot() { 25 | let (booleans, integers, floats) = (&props.booleans, &props.integers, &props.floats); 26 | html! { 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | } 39 | } else { 40 | html! { 41 | <> 42 |

{ "Loading database..." }

43 |
44 | 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/gui/mass_effect_1/raw_plot.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use yew::prelude::*; 4 | 5 | use crate::{ 6 | gui::{ 7 | components::{Tab, TabBar}, 8 | shared::{FloatPlotType, IntPlotType, PlotType, RawPlot}, 9 | }, 10 | save_data::{shared::plot::BitVec, RcRef}, 11 | services::database::Databases, 12 | }; 13 | 14 | #[derive(Properties, PartialEq)] 15 | pub struct Props { 16 | pub booleans: RcRef, 17 | pub integers: IntPlotType, 18 | pub floats: FloatPlotType, 19 | } 20 | 21 | #[function_component(Me1RawPlot)] 22 | pub fn me1_raw_plot(props: &Props) -> Html { 23 | let dbs = use_context::().expect("no database provider"); 24 | if let Some(ref plot_db) = dbs.get_me1_raw_plot() { 25 | let (booleans, integers, floats) = (&props.booleans, &props.integers, &props.floats); 26 | 27 | html! { 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | } 40 | } else { 41 | html! { 42 | <> 43 |

{ "Loading database..." }

44 |
45 | 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/gui/mass_effect_2/raw_plot.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use yew::prelude::*; 4 | 5 | use crate::{ 6 | gui::{ 7 | components::{Tab, TabBar}, 8 | shared::{FloatPlotType, IntPlotType, PlotType, RawPlot}, 9 | }, 10 | save_data::{shared::plot::BitVec, RcRef}, 11 | services::database::Databases, 12 | }; 13 | 14 | #[derive(Properties, PartialEq)] 15 | pub struct Props { 16 | pub booleans: RcRef, 17 | pub integers: IntPlotType, 18 | pub floats: FloatPlotType, 19 | } 20 | 21 | #[function_component(Me2RawPlot)] 22 | pub fn me2_raw_plot(props: &Props) -> Html { 23 | let dbs = use_context::().expect("no database provider"); 24 | if let Some(ref plot_db) = dbs.get_me2_raw_plot() { 25 | let (booleans, integers, floats) = (&props.booleans, &props.integers, &props.floats); 26 | 27 | html! { 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | } 40 | } else { 41 | html! { 42 | <> 43 |

{ "Loading database..." }

44 |
45 | 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_1_le/legacy/inventory.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::{BaseObject, OptionObjectProxy}; 4 | use crate::save_data::mass_effect_1_le::player::ItemLevel; 5 | 6 | #[rcize_fields] 7 | #[derive(Deserialize, Serialize, Clone, RawUiChildren)] 8 | pub struct Shop { 9 | last_player_level: i32, 10 | is_initialized: bool, 11 | inventory: Vec, 12 | } 13 | 14 | #[rcize_fields] 15 | #[derive(Deserialize, Serialize, Clone, RawUiChildren)] 16 | pub struct Inventory { 17 | items: Vec, 18 | plot_items: Vec, 19 | credits: i32, 20 | grenades: i32, 21 | medigel: f32, 22 | omnigel: f32, 23 | } 24 | 25 | #[rcize_fields] 26 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 27 | #[display(fmt = "")] 28 | struct PlotItem { 29 | localized_name: i32, 30 | localized_desc: i32, 31 | export_id: i32, 32 | base_price: i32, 33 | shop_gui_image_id: i32, 34 | plot_conditional_id: i32, 35 | } 36 | 37 | #[rcize_fields] 38 | #[derive(Deserialize, Serialize, Clone, RawUiChildren)] 39 | pub struct Item { 40 | item_id: i32, 41 | item_level: ItemLevel, 42 | manufacturer_id: i32, 43 | plot_conditional_id: i32, 44 | slot_specs: Vec, 45 | } 46 | 47 | #[rcize_fields] 48 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 49 | #[display(fmt = "")] 50 | struct ModdableSlotSpec { 51 | type_id: i32, 52 | mods: Vec, 53 | } 54 | 55 | #[rcize_fields] 56 | #[derive(Deserialize, Serialize, Clone, RawUiChildren)] 57 | pub struct ItemMod { 58 | item_id: i32, 59 | item_level: ItemLevel, 60 | manufacturer_id: i32, 61 | plot_conditional_id: i32, 62 | type_id: i32, 63 | } 64 | -------------------------------------------------------------------------------- /src/gui/mass_effect_1/raw_data/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod property; 2 | 3 | use std::cell::Ref; 4 | 5 | use yew::prelude::*; 6 | 7 | use crate::{ 8 | gui::{components::Table, mass_effect_1::raw_data::property::Property}, 9 | save_data::{mass_effect_1::player::Player, RcRef}, 10 | }; 11 | 12 | #[derive(Properties, PartialEq)] 13 | pub struct Props { 14 | pub player: RcRef, 15 | } 16 | 17 | impl Props { 18 | fn player(&self) -> Ref<'_, Player> { 19 | self.player.borrow() 20 | } 21 | } 22 | 23 | pub struct Me1RawData; 24 | 25 | impl Component for Me1RawData { 26 | type Message = (); 27 | type Properties = Props; 28 | 29 | fn create(_ctx: &Context) -> Self { 30 | Me1RawData {} 31 | } 32 | 33 | fn view(&self, ctx: &Context) -> Html { 34 | let player = ctx.props().player(); 35 | let object_id = player 36 | .objects 37 | .iter() 38 | .enumerate() 39 | .find_map(|(idx, object)| { 40 | (player.get_name(object.object_name_id) == "CurrentGame").then(|| idx as i32 + 1) 41 | }) 42 | .unwrap_or_default(); 43 | 44 | let properties = &player.get_data(object_id).properties; 45 | let len = properties.len(); 46 | let take = if len > 0 { len - 1 } else { 0 }; 47 | let properties = properties.iter().take(take).map(|property| { 48 | html! { 49 | 53 | } 54 | }); 55 | 56 | html! { 57 | 58 | { for properties } 59 |
60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_1/state.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::save_data::{shared::plot::PlotTable, Dummy, List}; 4 | 5 | #[rcize_fields] 6 | #[derive(Deserialize, Serialize, Clone, RawUi)] 7 | pub struct State { 8 | _begin: Dummy<12>, 9 | base_level_name: String, 10 | _osef1: Dummy<24>, 11 | pub plot: PlotTable, 12 | _osef2: List, 13 | } 14 | 15 | #[cfg(test)] 16 | mod test { 17 | use std::fs; 18 | use std::io::{Cursor, Read}; 19 | 20 | use anyhow::Result; 21 | use zip::ZipArchive; 22 | 23 | use super::*; 24 | use crate::unreal; 25 | 26 | #[test] 27 | fn deserialize_serialize() -> Result<()> { 28 | let input = fs::read("test/ME1Save.MassEffectSave")?; 29 | 30 | let state_data = { 31 | let mut offset_bytes = [0; 4]; 32 | offset_bytes.copy_from_slice(&input[8..12]); 33 | let zip_offset = ::from_le_bytes(offset_bytes); 34 | let mut zip = ZipArchive::new(Cursor::new(&input[zip_offset as usize..]))?; 35 | 36 | let mut bytes = Vec::new(); 37 | zip.by_name("state.sav")?.read_to_end(&mut bytes)?; 38 | bytes 39 | }; 40 | 41 | // Deserialize 42 | let state: State = unreal::Deserializer::from_bytes(&state_data)?; 43 | 44 | // Serialize 45 | let output = unreal::Serializer::to_vec(&state)?; 46 | 47 | // // Check serialized = state_data 48 | // let cmp = state_data.chunks(4).zip(output.chunks(4)); 49 | // for (i, (a, b)) in cmp.enumerate() { 50 | // if a != b { 51 | // panic!("0x{:02x?} : {:02x?} != {:02x?}", i * 4, a, b); 52 | // } 53 | // } 54 | 55 | assert!(state_data == output); 56 | 57 | Ok(()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/gui/components/raw_ui/raw_ui_guid.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{Ref, RefMut}; 2 | 3 | use uuid::Uuid; 4 | use web_sys::HtmlInputElement; 5 | use yew::prelude::*; 6 | 7 | use crate::save_data::Guid; 8 | use crate::save_data::RcRef; 9 | 10 | pub enum Msg { 11 | Change(Event), 12 | } 13 | 14 | #[derive(Properties, PartialEq)] 15 | pub struct Props { 16 | pub label: String, 17 | pub guid: RcRef, 18 | } 19 | 20 | impl Props { 21 | fn guid(&self) -> Ref<'_, Guid> { 22 | self.guid.borrow() 23 | } 24 | 25 | fn guid_mut(&self) -> RefMut<'_, Guid> { 26 | self.guid.borrow_mut() 27 | } 28 | } 29 | 30 | pub struct RawUiGuid; 31 | 32 | impl Component for RawUiGuid { 33 | type Message = Msg; 34 | type Properties = Props; 35 | 36 | fn create(_ctx: &Context) -> Self { 37 | RawUiGuid 38 | } 39 | 40 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 41 | match msg { 42 | Msg::Change(event) => { 43 | if let Some(input) = event.target_dyn_into::() { 44 | if let Ok(guid) = Uuid::parse_str(&input.value()) { 45 | *ctx.props().guid_mut() = Guid::from(guid); 46 | } 47 | true 48 | } else { 49 | false 50 | } 51 | } 52 | } 53 | } 54 | 55 | fn view(&self, ctx: &Context) -> Html { 56 | let value = ctx.props().guid().hyphenated(); 57 | let onchange = ctx.link().callback(Msg::Change); 58 | html! { 59 | 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "trilogy-save-editor" 3 | version = "2.2.1" 4 | authors = ["Karlitos"] 5 | license = "CECILL-2.1" 6 | edition = "2021" 7 | rust-version = "1.56.0" 8 | 9 | [profile.release] 10 | lto = true 11 | 12 | [workspace] 13 | members = ["macros", "app"] 14 | 15 | [build-dependencies] 16 | regex = "1.0" 17 | 18 | [dependencies] 19 | # Karlitos 20 | macros = { path = "macros" } 21 | # Std-like 22 | anyhow = "1.0" 23 | derive_more = { version = "0.99", features = [ 24 | "deref", 25 | "deref_mut", 26 | "display", 27 | "from", 28 | ], default-features = false } 29 | bitvec = { version = "1.0", features = ["std"], default-features = false } 30 | indexmap = { version = "=1.7", features = [ 31 | "std", 32 | ], default-features = false } # FIXME: remove `=` when indexmap panic on release will be fixed 33 | encoding_rs = "0.8" 34 | # Sync 35 | # flume = { version = "0.10", features = ["async"], default-features = false } 36 | # Utils 37 | crc = "2.0" 38 | flate2 = { version = "1.0", features = [ 39 | "rust_backend", 40 | ], default-features = false } 41 | zip = { version = "0.5", features = ["deflate"], default-features = false } 42 | uuid = "0.8" 43 | ryu = "1.0" 44 | base64 = "0.13" 45 | # Wasm 46 | wasm-bindgen = "0.2" 47 | wasm-bindgen-futures = "0.4" 48 | # wasm-timer = "0.2" 49 | js-sys = "0.3" 50 | web-sys = { version = "0.3", features = [ 51 | "CssStyleDeclaration", 52 | "CustomEvent", 53 | "DataTransfer", 54 | "DomRect", 55 | "DomTokenList", 56 | "History", 57 | "PopStateEvent", 58 | "PopStateEventInit", 59 | ] } 60 | gloo = { version = "0.6", features = ["futures"], default-features = false } 61 | # (De)Serialization 62 | serde-wasm-bindgen = "0.4" 63 | serde = { version = "1.0", features = ["derive"], default-features = false } 64 | ron = { version = "0.7", features = ["indexmap"], default-features = false } 65 | # Yew 66 | yew = "0.19" 67 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | build: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [windows-latest, ubuntu-latest, macos-latest] 17 | target: 18 | - x86_64-pc-windows-msvc 19 | - x86_64-unknown-linux-gnu 20 | - x86_64-apple-darwin 21 | rust: [stable] 22 | exclude: 23 | - os: windows-latest 24 | target: x86_64-apple-darwin 25 | - os: windows-latest 26 | target: x86_64-unknown-linux-gnu 27 | - os: ubuntu-latest 28 | target: x86_64-pc-windows-msvc 29 | - os: ubuntu-latest 30 | target: x86_64-apple-darwin 31 | - os: macos-latest 32 | target: x86_64-pc-windows-msvc 33 | - os: macos-latest 34 | target: x86_64-unknown-linux-gnu 35 | steps: 36 | - uses: actions/checkout@v2 37 | - name: Install dependencies 38 | if: matrix.target == 'x86_64-unknown-linux-gnu' 39 | run: sudo apt update && sudo apt install libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev 40 | - name: Install latest stable rust 41 | uses: actions-rs/toolchain@v1 42 | with: 43 | toolchain: stable 44 | - name: Install wasm32-unknown-unknown 45 | run: rustup target add wasm32-unknown-unknown 46 | - name: Install Trunk 47 | uses: jetli/trunk-action@v0.1.0 48 | with: 49 | version: 'v0.14.0' 50 | - name: npm install 51 | run: npm install 52 | - name: Run tailwind 53 | run: npm run build 54 | - name: Trunk build 55 | run: trunk build --dist "target/dist" 56 | - name: App Build 57 | run: cargo build -p app 58 | - name: Run tests 59 | run: cargo test --all 60 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_2/player.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::save_data::shared::{ 4 | appearance::Appearance, 5 | player::{Notoriety, Origin, WeaponLoadout}, 6 | }; 7 | 8 | #[rcize_fields] 9 | #[derive(Deserialize, Serialize, Clone, RawUi)] 10 | pub struct Player { 11 | pub is_female: bool, 12 | pub class_name: String, 13 | pub level: i32, 14 | pub current_xp: f32, 15 | pub first_name: String, 16 | localized_last_name: i32, 17 | pub origin: Origin, 18 | pub notoriety: Notoriety, 19 | pub talent_points: i32, 20 | mapped_power_1: String, 21 | mapped_power_2: String, 22 | mapped_power_3: String, 23 | pub appearance: Appearance, 24 | pub powers: Vec, 25 | weapons: Vec, 26 | weapons_loadout: WeaponLoadout, 27 | hotkeys: Vec, 28 | pub credits: i32, 29 | pub medigel: i32, 30 | pub eezo: i32, 31 | pub iridium: i32, 32 | pub palladium: i32, 33 | pub platinum: i32, 34 | pub probes: i32, 35 | pub current_fuel: f32, 36 | pub face_code: String, 37 | localized_class_name: i32, 38 | } 39 | 40 | #[rcize_fields] 41 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 42 | #[display(fmt = "{}", name)] 43 | pub struct Power { 44 | pub name: String, 45 | rank: f32, 46 | pub power_class_name: String, 47 | wheel_display_index: i32, 48 | } 49 | 50 | #[rcize_fields] 51 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 52 | #[display(fmt = "{}", class_name)] 53 | struct Weapon { 54 | class_name: String, 55 | ammo_used_count: i32, 56 | ammo_total: i32, 57 | current_weapon: bool, 58 | last_weapon: bool, 59 | ammo_power_name: String, 60 | } 61 | 62 | #[rcize_fields] 63 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 64 | #[display(fmt = "")] 65 | struct Hotkey { 66 | pawn_name: String, 67 | power_id: i32, 68 | } 69 | -------------------------------------------------------------------------------- /app/src/init.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | // Prevent user to reload the page 3 | document.addEventListener("keydown", (e) => { 4 | if (e.key === "F5" || 5 | (e.ctrlKey && e.key === "r") || 6 | (e.ctrlKey && e.key === "R")) { 7 | e.preventDefault(); 8 | } 9 | }); 10 | 11 | // Disable WebView default context menu 12 | document.addEventListener("contextmenu", (e) => { 13 | e.preventDefault(); 14 | }); 15 | 16 | // Window events 17 | const MAIN_BUTTON = 1; 18 | const DOUBLE_CLICK = 2; 19 | window.addEventListener("load", () => { 20 | // Title bar events 21 | const drag_zone = document.getElementById("drag_zone"); 22 | drag_zone.addEventListener('mousedown', (e) => { 23 | if (e.buttons === MAIN_BUTTON) { 24 | if (e.detail === DOUBLE_CLICK) { 25 | window.rpc.notify('toggle_maximize'); 26 | } else { 27 | window.rpc.notify('drag_window'); 28 | } 29 | } 30 | }) 31 | 32 | const minimize = document.getElementById("minimize"); 33 | minimize.addEventListener("click", () => { 34 | window.rpc.notify("minimize"); 35 | }); 36 | 37 | const maximize = document.getElementById("maximize"); 38 | maximize.addEventListener("click", () => { 39 | window.rpc.notify("toggle_maximize"); 40 | }); 41 | 42 | document.addEventListener("tse_maximized_state_changed", (e) => { 43 | if (e.detail.is_maximized === true) { 44 | maximize.classList.add("maximized"); 45 | } else { 46 | maximize.classList.remove("maximized"); 47 | } 48 | }); 49 | 50 | const close = document.getElementById("close"); 51 | close.addEventListener("click", () => { 52 | window.rpc.notify("close"); 53 | }); 54 | 55 | // Show the window when initialized 56 | window.rpc.notify("init"); 57 | }); 58 | })(); -------------------------------------------------------------------------------- /misc/010 templates/me1_state.bt: -------------------------------------------------------------------------------- 1 | 2 | // Bool 3 | typedef struct { 4 | uint value: 1; 5 | uint : 31; 6 | } bool ; 7 | 8 | string read_bool(bool &b) { 9 | if(b.value == 1) 10 | return "true"; 11 | else 12 | return "false"; 13 | } 14 | 15 | // String 16 | typedef struct { 17 | int len ; 18 | SetBackColor(0x000044 + len); 19 | 20 | // Détection utf8 21 | if (len < 0) { 22 | wchar_t chars[Abs(len)]; 23 | } 24 | else { 25 | char chars[len]; 26 | } 27 | } String ; 28 | 29 | string read_string(String &s) { 30 | if(exists(s.chars)) 31 | return s.chars; 32 | else 33 | return ""; 34 | } 35 | 36 | // Plot Table 37 | typedef struct { 38 | uint b00: 1; 39 | uint b01: 1; 40 | uint b02: 1; 41 | uint b03: 1; 42 | uint b04: 1; 43 | uint b05: 1; 44 | uint b06: 1; 45 | uint b07: 1; 46 | uint b08: 1; 47 | uint b09: 1; 48 | uint b10: 1; 49 | uint b11: 1; 50 | uint b12: 1; 51 | uint b13: 1; 52 | uint b14: 1; 53 | uint b15: 1; 54 | uint b16: 1; 55 | uint b17: 1; 56 | uint b18: 1; 57 | uint b19: 1; 58 | uint b20: 1; 59 | uint b21: 1; 60 | uint b22: 1; 61 | uint b23: 1; 62 | uint b24: 1; 63 | uint b25: 1; 64 | uint b26: 1; 65 | uint b27: 1; 66 | uint b28: 1; 67 | uint b29: 1; 68 | uint b30: 1; 69 | uint b31: 1; 70 | } BitField; 71 | 72 | struct BitArray { 73 | uint len; 74 | BitField bit_field[len]; 75 | }; 76 | 77 | struct PlotTable { 78 | BitArray bool_variables ; 79 | struct { 80 | int len; 81 | int pair[len]; 82 | } int_variables ; 83 | struct { 84 | int len; 85 | float pair[len]; 86 | } float_variables ; 87 | }; 88 | 89 | // SaveGame 90 | struct { 91 | FSkip(12); 92 | String base_level_name; 93 | FSkip(24); 94 | PlotTable plot; 95 | } MassEffect2 ; 96 | -------------------------------------------------------------------------------- /app/src/windows/mod.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use anyhow::{bail, Result}; 4 | use tokio::{fs, process}; 5 | 6 | pub mod auto_update; 7 | 8 | pub async fn install_webview2() -> Result<()> { 9 | let should_install = rfd::AsyncMessageDialog::new() 10 | .set_title("Install WebView2 Runtime") 11 | .set_description("The WebView2 Runtime must be installed to use this program. Install now?") 12 | .set_level(rfd::MessageLevel::Warning) 13 | .set_buttons(rfd::MessageButtons::YesNo) 14 | .show() 15 | .await; 16 | 17 | if !should_install { 18 | bail!("WebView2 install cancelled by user"); 19 | } 20 | 21 | let setup = 22 | reqwest::get("https://go.microsoft.com/fwlink/p/?LinkId=2124703").await?.bytes().await?; 23 | 24 | let temp_dir = env::temp_dir().join("trilogy-save-editor"); 25 | let path = temp_dir.join("MicrosoftEdgeWebview2Setup.exe"); 26 | 27 | // If not exists 28 | if fs::metadata(&temp_dir).await.is_err() { 29 | fs::create_dir(temp_dir).await?; 30 | } 31 | fs::write(&path, setup).await?; 32 | 33 | let status = process::Command::new(path) 34 | .arg("/install") 35 | .status() 36 | .await 37 | .expect("Failed to launch WebView2 install"); 38 | 39 | if !status.success() { 40 | bail!("Failed to install WebView2"); 41 | } 42 | Ok(()) 43 | } 44 | 45 | pub fn clear_code_cache() { 46 | use std::fs; 47 | let execute = || -> Result<()> { 48 | let mut code_cache_dir = env::current_exe()?; 49 | code_cache_dir.set_extension("exe.WebView2"); 50 | code_cache_dir.push("EBWebView\\Default\\Code Cache\\wasm"); 51 | 52 | if code_cache_dir.is_dir() { 53 | const THRESHOLD: u64 = 1024 * 1024; // 1mo 54 | 55 | for entry in fs::read_dir(&code_cache_dir)? { 56 | let path = entry?.path(); 57 | if path.is_file() && path.metadata()?.len() > THRESHOLD { 58 | fs::remove_file(path)?; 59 | } 60 | } 61 | } 62 | Ok(()) 63 | }; 64 | let _ = execute(); 65 | } 66 | -------------------------------------------------------------------------------- /src/gui/components/input_text.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{Ref, RefMut}; 2 | use web_sys::HtmlInputElement; 3 | use yew::prelude::*; 4 | 5 | use crate::{gui::components::Helper, save_data::RcRef}; 6 | 7 | use super::CallbackType; 8 | 9 | pub enum Msg { 10 | Input(InputEvent), 11 | } 12 | 13 | #[derive(Properties, PartialEq)] 14 | pub struct Props { 15 | pub label: String, 16 | pub value: RcRef, 17 | pub helper: Option<&'static str>, 18 | pub oninput: Option>, 19 | } 20 | 21 | impl Props { 22 | fn value(&self) -> Ref<'_, String> { 23 | self.value.borrow() 24 | } 25 | 26 | fn value_mut(&self) -> RefMut<'_, String> { 27 | self.value.borrow_mut() 28 | } 29 | } 30 | 31 | pub struct InputText; 32 | 33 | impl Component for InputText { 34 | type Message = Msg; 35 | type Properties = Props; 36 | 37 | fn create(_ctx: &Context) -> Self { 38 | InputText 39 | } 40 | 41 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 42 | match msg { 43 | Msg::Input(event) => { 44 | if let Some(input) = event.target_dyn_into::() { 45 | if let Some(ref callback) = ctx.props().oninput { 46 | callback.emit(CallbackType::String(input.value())); 47 | } 48 | 49 | *ctx.props().value_mut() = input.value(); 50 | } 51 | false 52 | } 53 | } 54 | } 55 | 56 | fn view(&self, ctx: &Context) -> Html { 57 | let helper = ctx.props().helper.as_ref().map(|&helper| { 58 | html! { 59 | 60 | } 61 | }); 62 | let value = ctx.props().value().clone(); 63 | let oninput = ctx.link().callback(Msg::Input); 64 | html! { 65 | 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/gui/components/raw_ui/raw_ui_struct.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | use crate::gui::components::Table; 4 | 5 | pub enum Msg { 6 | Toggle, 7 | } 8 | 9 | #[derive(Properties, PartialEq)] 10 | pub struct Props { 11 | pub label: String, 12 | pub children: Children, 13 | #[prop_or(false)] 14 | pub opened: bool, 15 | } 16 | 17 | pub struct RawUiStruct { 18 | opened: bool, 19 | } 20 | 21 | impl Component for RawUiStruct { 22 | type Message = Msg; 23 | type Properties = Props; 24 | 25 | fn create(ctx: &Context) -> Self { 26 | RawUiStruct { opened: ctx.props().opened } 27 | } 28 | 29 | fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { 30 | match msg { 31 | Msg::Toggle => { 32 | self.opened = !self.opened; 33 | true 34 | } 35 | } 36 | } 37 | 38 | fn view(&self, ctx: &Context) -> Html { 39 | let Props { ref label, ref children, .. } = ctx.props(); 40 | let chevron = if self.opened { "table-chevron-down" } else { "table-chevron-right" }; 41 | 42 | let content = self.opened.then(|| { 43 | html! { 44 |
45 | 46 | { children.clone() } 47 |
48 |
49 | } 50 | }); 51 | 52 | html! { 53 |
54 |
55 | 70 |
71 | { for content } 72 |
73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/gui/shared/mod.rs: -------------------------------------------------------------------------------- 1 | mod bonus_powers; 2 | mod head_morph; 3 | mod link; 4 | mod plot_category; 5 | mod raw_plot; 6 | 7 | pub use self::{bonus_powers::*, head_morph::*, link::*, plot_category::*, raw_plot::*}; 8 | 9 | use indexmap::IndexMap; 10 | use yew::prelude::*; 11 | 12 | use crate::save_data::{ 13 | shared::plot::{BitVec, PlotTable}, 14 | RcCell, RcRef, 15 | }; 16 | 17 | use super::raw_ui::RawUi; 18 | 19 | #[derive(Clone)] 20 | pub enum IntPlotType { 21 | Vec(RcRef>>), 22 | IndexMap(RcRef>>), 23 | } 24 | 25 | impl PartialEq for IntPlotType { 26 | fn eq(&self, other: &IntPlotType) -> bool { 27 | match (self, other) { 28 | (IntPlotType::Vec(vec), IntPlotType::Vec(other)) => vec == other, 29 | (IntPlotType::IndexMap(index_map), IntPlotType::IndexMap(other)) => index_map == other, 30 | _ => false, 31 | } 32 | } 33 | } 34 | 35 | #[derive(Clone)] 36 | pub enum FloatPlotType { 37 | Vec(RcRef>>), 38 | IndexMap(RcRef>>), 39 | } 40 | 41 | impl PartialEq for FloatPlotType { 42 | fn eq(&self, other: &FloatPlotType) -> bool { 43 | match (self, other) { 44 | (FloatPlotType::Vec(vec), FloatPlotType::Vec(other)) => vec == other, 45 | (FloatPlotType::IndexMap(index_map), FloatPlotType::IndexMap(other)) => { 46 | index_map == other 47 | } 48 | _ => false, 49 | } 50 | } 51 | } 52 | 53 | #[derive(Clone)] 54 | pub enum PlotType { 55 | Boolean(RcRef), 56 | Int(IntPlotType), 57 | Float(FloatPlotType), 58 | } 59 | 60 | impl PartialEq for PlotType { 61 | fn eq(&self, other: &PlotType) -> bool { 62 | match (self, other) { 63 | (PlotType::Boolean(booleans), PlotType::Boolean(other)) => booleans == other, 64 | (PlotType::Int(integers), PlotType::Int(other)) => integers == other, 65 | (PlotType::Float(floats), PlotType::Float(other)) => floats == other, 66 | _ => false, 67 | } 68 | } 69 | } 70 | 71 | impl RawUi for RcRef { 72 | fn view(&self, _: &str) -> yew::Html { 73 | html! { 74 | { "Raw Plot" } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/gui/mass_effect_1/plot.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use yew::prelude::*; 3 | 4 | use crate::{ 5 | gui::{ 6 | components::{Tab, TabBar}, 7 | shared::{IntPlotType, PlotCategory}, 8 | }, 9 | save_data::{ 10 | mass_effect_1::plot_db::Me1PlotDb, 11 | shared::plot::{BitVec, PlotCategory as PlotCategoryDb}, 12 | RcRef, 13 | }, 14 | services::database::Databases, 15 | }; 16 | 17 | #[derive(Properties, PartialEq)] 18 | pub struct Props { 19 | pub booleans: RcRef, 20 | pub integers: IntPlotType, 21 | #[prop_or(false)] 22 | pub me3_imported_me1: bool, 23 | } 24 | 25 | #[function_component(Me1Plot)] 26 | pub fn me1_plot(props: &Props) -> Html { 27 | let dbs = use_context::().expect("no database provider"); 28 | if let Some(plot_db) = dbs.get_me1_plot() { 29 | let Props { booleans, integers, .. } = props; 30 | let Me1PlotDb { player_crew, missions } = &*plot_db; 31 | 32 | let view_categories = |categories: &IndexMap| { 33 | categories 34 | .iter() 35 | .map(|(title, category)| { 36 | html! { 37 | 44 | } 45 | }) 46 | .collect::>() 47 | }; 48 | 49 | let categories = [("Player / Crew", player_crew), ("Missions", missions)]; 50 | 51 | let categories = categories.iter().map(|(tab, categories)| { 52 | html_nested! { 53 | 54 |
55 | { for view_categories(categories) } 56 |
57 |
58 | } 59 | }); 60 | 61 | html! { 62 | 63 | { for categories } 64 | 65 | } 66 | } else { 67 | html! { 68 | <> 69 |

{ "Loading database..." }

70 |
71 | 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/save_data/shared/plot.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use bitvec::prelude::*; 3 | use indexmap::IndexMap; 4 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 5 | 6 | #[derive(Deref, DerefMut, Clone)] 7 | pub struct BitVec(bitvec::vec::BitVec); 8 | 9 | impl<'de> Deserialize<'de> for BitVec { 10 | fn deserialize(deserializer: D) -> Result 11 | where 12 | D: Deserializer<'de>, 13 | { 14 | let bitfields: Vec = Deserialize::deserialize(deserializer)?; 15 | let bitvec = bitvec::vec::BitVec::from_vec(bitfields); 16 | Ok(BitVec(bitvec)) 17 | } 18 | } 19 | 20 | impl serde::Serialize for BitVec { 21 | fn serialize(&self, serializer: S) -> Result 22 | where 23 | S: Serializer, 24 | { 25 | let bitfields = self.0.clone().into_vec(); 26 | serializer.collect_seq(bitfields) 27 | } 28 | } 29 | 30 | #[rcize_fields] 31 | #[derive(Deserialize, Serialize, Clone)] 32 | pub struct PlotTable { 33 | pub booleans: BitVec, 34 | pub integers: Vec, 35 | pub floats: Vec, 36 | } 37 | 38 | #[rcize_fields] 39 | #[derive(Deserialize, Serialize, Clone, RawUi)] 40 | pub struct Journal { 41 | quest_progress_counter: i32, 42 | quest_progress: Vec, 43 | quest_ids: Vec, 44 | } 45 | 46 | #[rcize_fields] 47 | #[derive(Deserialize, Serialize, Clone, RawUi)] 48 | pub struct Codex { 49 | codex_entries: Vec, 50 | codex_ids: Vec, 51 | } 52 | 53 | #[rcize_fields] 54 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 55 | #[display(fmt = "{}", quest_counter)] 56 | pub struct PlotQuest { 57 | quest_counter: i32, 58 | quest_updated: bool, 59 | history: Vec, 60 | } 61 | 62 | #[rcize_fields] 63 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 64 | #[display(fmt = "")] 65 | pub struct PlotCodex { 66 | pages: Vec, 67 | } 68 | 69 | #[rcize_fields] 70 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 71 | #[display(fmt = "{}", page)] 72 | pub struct PlotCodexPage { 73 | page: i32, 74 | is_new: bool, 75 | } 76 | 77 | #[derive(Deserialize, Clone, PartialEq, Eq)] 78 | pub struct PlotCategory { 79 | pub booleans: IndexMap, 80 | pub integers: IndexMap, 81 | } 82 | 83 | #[derive(Deserialize, Clone, PartialEq, Eq)] 84 | pub struct RawPlotDb { 85 | pub booleans: IndexMap, 86 | pub integers: IndexMap, 87 | pub floats: IndexMap, 88 | } 89 | -------------------------------------------------------------------------------- /src/gui/components/raw_ui/raw_ui_option.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::{Ref, RefMut}, 3 | marker::PhantomData, 4 | }; 5 | 6 | use yew::prelude::*; 7 | 8 | use crate::{gui::raw_ui::RawUi, save_data::RcRef}; 9 | 10 | pub enum Msg { 11 | Remove, 12 | } 13 | 14 | #[derive(Properties, PartialEq)] 15 | pub struct Props 16 | where 17 | T: RawUi, 18 | { 19 | pub label: String, 20 | pub option: RcRef>, 21 | } 22 | 23 | impl Props 24 | where 25 | T: RawUi, 26 | { 27 | fn option(&self) -> Ref<'_, Option> { 28 | self.option.borrow() 29 | } 30 | 31 | fn option_mut(&self) -> RefMut<'_, Option> { 32 | self.option.borrow_mut() 33 | } 34 | } 35 | 36 | pub struct RawUiOption 37 | where 38 | T: RawUi, 39 | { 40 | _marker: PhantomData, 41 | } 42 | 43 | impl Component for RawUiOption 44 | where 45 | T: RawUi, 46 | { 47 | type Message = Msg; 48 | type Properties = Props; 49 | 50 | fn create(_ctx: &Context) -> Self { 51 | RawUiOption { _marker: PhantomData } 52 | } 53 | 54 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 55 | match msg { 56 | Msg::Remove => { 57 | *ctx.props().option_mut() = None; 58 | true 59 | } 60 | } 61 | } 62 | 63 | fn view(&self, ctx: &Context) -> Html { 64 | match *ctx.props().option() { 65 | Some(ref content) => html! { 66 |
67 | 84 | { content.view(&ctx.props().label) } 85 |
86 | }, 87 | None => html! { 88 |
89 | { "None" } 90 | { &ctx.props().label } 91 |
92 | }, 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/services/drop_handler.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | use anyhow::{Context, Result}; 5 | use gloo::{ 6 | events::{EventListener, EventListenerOptions}, 7 | file::{self, callbacks::FileReader, FileList}, 8 | utils, 9 | }; 10 | use wasm_bindgen::JsCast; 11 | use yew::prelude::*; 12 | 13 | pub struct DropHandler { 14 | _drag_over_listener: EventListener, 15 | _drop_listener: EventListener, 16 | _file_reader: Rc>>, 17 | } 18 | 19 | impl DropHandler { 20 | pub fn new(ondrop: Callback)>>) -> Self { 21 | let document = utils::document(); 22 | let options = EventListenerOptions::enable_prevent_default(); 23 | 24 | let drag_over_listener = 25 | EventListener::new_with_options(&document, "dragover", options, |event| { 26 | // To allow drop 27 | event.prevent_default(); 28 | }); 29 | 30 | let file_reader = Rc::new(RefCell::new(None)); 31 | let drop_listener = { 32 | let file_reader = Rc::clone(&file_reader); 33 | EventListener::new_with_options(&document, "drop", options, move |event| { 34 | let ondrop = ondrop.clone(); 35 | let file_reader = Rc::clone(&file_reader); 36 | let handle_drop = move || { 37 | event.prevent_default(); 38 | 39 | let drag_event = event.dyn_ref::()?; 40 | let data = drag_event.data_transfer()?; 41 | let file_list: FileList = data.files()?.into(); 42 | 43 | if let Some(file) = file_list.first() { 44 | let file_name = file.name(); 45 | let reader = { 46 | let file_reader = Rc::clone(&file_reader); 47 | file::callbacks::read_as_bytes(file, move |read_file| { 48 | let result = read_file 49 | .map(|bytes| (file_name, bytes)) 50 | .context("Failed to open the save"); 51 | ondrop.emit(result); 52 | 53 | // Drop the reader when finished 54 | *(*file_reader).borrow_mut() = None; 55 | }) 56 | }; 57 | // Keep the reader alive 58 | *(*file_reader).borrow_mut() = Some(reader); 59 | } 60 | Some(()) 61 | }; 62 | let _ = handle_drop(); 63 | }) 64 | }; 65 | DropHandler { 66 | _drag_over_listener: drag_over_listener, 67 | _drop_listener: drop_listener, 68 | _file_reader: file_reader, 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/save_data/shared/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod appearance; 2 | pub mod player; 3 | pub mod plot; 4 | 5 | use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; 6 | 7 | use super::Guid; 8 | 9 | #[derive(Clone, RawUi)] 10 | #[repr(u32)] 11 | pub enum EndGameState { 12 | NotFinished, 13 | OutInABlazeOfGlory, 14 | LivedToFightAgain, 15 | } 16 | 17 | impl<'de> Deserialize<'de> for EndGameState { 18 | fn deserialize(deserializer: D) -> Result 19 | where 20 | D: Deserializer<'de>, 21 | { 22 | let idx: u32 = Deserialize::deserialize(deserializer)?; 23 | 24 | let end_game_state = match idx { 25 | 0 => EndGameState::NotFinished, 26 | 1 => EndGameState::OutInABlazeOfGlory, 27 | 2 => EndGameState::LivedToFightAgain, 28 | _ => return Err(de::Error::custom("invalid EndGameState variant")), 29 | }; 30 | Ok(end_game_state) 31 | } 32 | } 33 | 34 | impl serde::Serialize for EndGameState { 35 | fn serialize(&self, serializer: S) -> Result 36 | where 37 | S: Serializer, 38 | { 39 | serializer.serialize_u32(self.clone() as u32) 40 | } 41 | } 42 | 43 | #[rcize_fields] 44 | #[derive(Deserialize, Serialize, Clone, RawUi)] 45 | pub struct SaveTimeStamp { 46 | seconds_since_midnight: i32, 47 | day: i32, 48 | month: i32, 49 | year: i32, 50 | } 51 | 52 | #[rcize_fields] 53 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 54 | #[display(fmt = "")] 55 | pub struct Vector { 56 | x: f32, 57 | y: f32, 58 | z: f32, 59 | } 60 | 61 | #[rcize_fields] 62 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 63 | #[display(fmt = "")] 64 | pub struct Vector2d { 65 | x: f32, 66 | y: f32, 67 | } 68 | 69 | #[rcize_fields] 70 | #[derive(Deserialize, Serialize, Clone, RawUi)] 71 | pub struct Rotator { 72 | pitch: i32, 73 | yaw: i32, 74 | roll: i32, 75 | } 76 | 77 | #[rcize_fields] 78 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 79 | #[display(fmt = "{}", name)] 80 | pub struct Level { 81 | name: String, 82 | should_be_loaded: bool, 83 | should_be_visible: bool, 84 | } 85 | 86 | #[rcize_fields] 87 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 88 | #[display(fmt = "{}", name)] 89 | pub struct StreamingState { 90 | name: String, 91 | is_active: bool, 92 | } 93 | 94 | #[rcize_fields] 95 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 96 | #[display(fmt = "")] 97 | pub struct Kismet { 98 | guid: Guid, 99 | value: bool, 100 | } 101 | 102 | #[rcize_fields] 103 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 104 | #[display(fmt = "")] 105 | pub struct Door { 106 | guid: Guid, 107 | current_state: u8, 108 | old_state: u8, 109 | } 110 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_3/player.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::save_data::{ 5 | shared::{ 6 | appearance::Appearance, 7 | player::{Notoriety, Origin, WeaponLoadout}, 8 | }, 9 | Guid, 10 | }; 11 | 12 | #[rcize_fields] 13 | #[derive(Deserialize, Serialize, Clone, RawUi)] 14 | pub struct Player { 15 | pub is_female: bool, 16 | pub class_name: String, 17 | is_combat_pawn: bool, 18 | is_injured_pawn: bool, 19 | use_casual_appearance: bool, 20 | pub level: i32, 21 | pub current_xp: f32, 22 | pub first_name: String, 23 | localized_last_name: i32, 24 | pub origin: Origin, 25 | pub notoriety: Notoriety, 26 | pub talent_points: i32, 27 | mapped_power_1: String, 28 | mapped_power_2: String, 29 | mapped_power_3: String, 30 | pub appearance: Appearance, 31 | emissive_id: i32, 32 | pub powers: Vec, 33 | war_assets: IndexMap, 34 | weapons: Vec, 35 | weapons_mods: Vec, 36 | weapons_loadout: WeaponLoadout, 37 | primary_weapon: String, 38 | secondary_weapon: String, 39 | loadout_weapon_group: Vec, 40 | hotkeys: Vec, 41 | health: f32, 42 | pub credits: i32, 43 | pub medigel: i32, 44 | eezo: i32, 45 | iridium: i32, 46 | palladium: i32, 47 | platinum: i32, 48 | probes: i32, 49 | pub current_fuel: f32, 50 | pub grenades: i32, 51 | pub face_code: String, 52 | localized_class_name: i32, 53 | character_guid: Guid, 54 | } 55 | 56 | #[rcize_fields] 57 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 58 | #[display(fmt = "{}", name)] 59 | pub struct Power { 60 | pub name: String, 61 | rank: f32, 62 | evolved_choice_0: i32, 63 | evolved_choice_1: i32, 64 | evolved_choice_2: i32, 65 | evolved_choice_3: i32, 66 | evolved_choice_4: i32, 67 | evolved_choice_5: i32, 68 | pub power_class_name: String, 69 | wheel_display_index: i32, 70 | } 71 | 72 | #[rcize_fields] 73 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 74 | #[display(fmt = "{}", class_name)] 75 | pub struct Weapon { 76 | class_name: String, 77 | ammo_used_count: i32, 78 | ammo_total: i32, 79 | current_weapon: bool, 80 | was_last_weapon: bool, 81 | ammo_power_name: String, 82 | ammo_power_source_tag: String, 83 | } 84 | 85 | #[rcize_fields] 86 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 87 | #[display(fmt = "{}", weapon_class_name)] 88 | pub struct WeaponMod { 89 | weapon_class_name: String, 90 | weapon_mod_class_names: Vec, 91 | } 92 | 93 | #[rcize_fields] 94 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 95 | #[display(fmt = "")] 96 | struct Hotkey { 97 | pawn_name: String, 98 | power_name: String, 99 | } 100 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_1_le/legacy/pawn.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::{BaseObject, OptionObjectProxy}; 4 | use crate::save_data::{ 5 | shared::{Rotator, Vector}, 6 | Dummy, 7 | }; 8 | 9 | #[rcize_fields] 10 | #[derive(Deserialize, Serialize, Clone, RawUiChildren)] 11 | pub struct PawnBehavior { 12 | is_dead: bool, 13 | generated_treasure: bool, 14 | challenge_scaled: bool, 15 | owner: Option, 16 | health: f32, 17 | shield: f32, 18 | first_name: String, 19 | localized_last_name: i32, 20 | health_max: f32, 21 | health_regen_rate: f32, 22 | radar_range: f32, 23 | level: i32, 24 | health_per_level: f32, 25 | stability: f32, 26 | gender: u8, 27 | race: u8, 28 | toxic: f32, 29 | stamina: i32, 30 | focus: i32, 31 | precision: i32, 32 | coordination: i32, 33 | quick_slot: u8, 34 | squad: Option, 35 | inventory: Option, 36 | _unknown: Dummy<3>, 37 | experience: i32, 38 | talent_points: i32, 39 | talent_pool_points: i32, 40 | attribute_primary: u8, 41 | attribute_secondary: u8, 42 | class_base: u8, 43 | localized_class_name: i32, 44 | auto_level_up_template_id: i32, 45 | spectre_rank: u8, 46 | background_origin: u8, 47 | background_notoriety: u8, 48 | specialization_bonus_id: u8, 49 | skill_charm: f32, 50 | skill_intimidate: f32, 51 | skill_haggle: f32, 52 | audibility: f32, 53 | blindness: f32, 54 | damage_duration_mult: f32, 55 | deafness: f32, 56 | unlootable_grenade_count: i32, 57 | head_gear_visible_preference: bool, 58 | simple_talents: Vec, 59 | complex_talents: Vec, 60 | quick_slots: Vec, 61 | equipment: Vec, 62 | } 63 | 64 | #[rcize_fields] 65 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 66 | #[display(fmt = "{}", talent_id)] 67 | struct SimpleTalent { 68 | talent_id: i32, 69 | current_rank: i32, 70 | } 71 | 72 | #[rcize_fields] 73 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 74 | #[display(fmt = "{}", talent_id)] 75 | struct ComplexTalent { 76 | talent_id: i32, 77 | current_rank: i32, 78 | max_rank: i32, 79 | level_offset: i32, 80 | levels_per_rank: i32, 81 | visual_order: i32, 82 | prereq_talent_ids: Vec, 83 | prereq_talent_ranks: Vec, 84 | } 85 | 86 | #[rcize_fields] 87 | #[derive(Deserialize, Serialize, Clone, RawUiChildren)] 88 | pub struct Pawn { 89 | location: Vector, 90 | rotation: Rotator, 91 | velocity: Vector, 92 | acceleration: Vector, 93 | script_initialized: bool, 94 | hidden: bool, 95 | stasis: bool, 96 | grime_level: f32, 97 | grime_dirt_level: f32, 98 | talked_to_count: i32, 99 | head_gear_visible_preference: bool, 100 | } 101 | 102 | #[rcize_fields] 103 | #[derive(Deserialize, Serialize, Clone, RawUiChildren)] 104 | pub struct BaseSquad { 105 | inventory: Option, 106 | } 107 | -------------------------------------------------------------------------------- /src/gui/mass_effect_1_le/mod.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | use crate::{ 4 | gui::{ 5 | components::{raw_ui::RawUiStruct, Table}, 6 | raw_ui::{RawUi, RawUiChildren}, 7 | }, 8 | save_data::{ 9 | mass_effect_1_le::{ 10 | legacy::{BaseObject, Object, OptionObjectProxy}, 11 | Me1LeSaveData, NoExport, 12 | }, 13 | RcRef, 14 | }, 15 | }; 16 | 17 | mod bonus_talents; 18 | mod general; 19 | mod inventory; 20 | 21 | pub use self::{general::*, inventory::*}; 22 | 23 | impl RawUi for RcRef { 24 | fn view(&self, _: &str) -> yew::Html { 25 | let no_export = self 26 | .borrow() 27 | .no_export() 28 | .as_ref() 29 | .map(|no_export_data| no_export_data.children()) 30 | .unwrap_or_else(|| vec![html! { "Export Save" }]) 31 | .into_iter(); 32 | 33 | let children = self.children(); 34 | let len = children.len(); 35 | html! { 36 | 37 | { for children.into_iter().take(len - 1) } 38 | { for no_export } 39 |
40 | } 41 | } 42 | } 43 | 44 | impl RawUi for RcRef { 45 | fn view(&self, _: &str) -> yew::Html { 46 | Default::default() 47 | } 48 | } 49 | 50 | impl RawUi for RcRef { 51 | fn view(&self, label: &str) -> yew::Html { 52 | let BaseObject { _class_name, owner_name, owner_class, _object } = &*self.borrow(); 53 | 54 | let object_children = match _object { 55 | Object::PawnBehavior(pawn_behavior) => pawn_behavior.children(), 56 | Object::Pawn(pawn) => pawn.children(), 57 | Object::BaseSquad(squad) => squad.children(), 58 | Object::Shop(shop) => shop.children(), 59 | Object::Inventory(inventory) => inventory.children(), 60 | Object::Item(item) => item.children(), 61 | Object::ItemMod(item_mod) => item_mod.children(), 62 | Object::ArtPlaceableBehavior(art_placeable_behavior) => { 63 | art_placeable_behavior.children() 64 | } 65 | Object::ArtPlaceable(art_placeable) => art_placeable.children(), 66 | Object::VehicleBehavior(vehicle_behavior) => vehicle_behavior.children(), 67 | Object::Vehicle(vehicle) => vehicle.children(), 68 | Object::World(world) => world.children(), 69 | Object::Default => unreachable!(), 70 | }; 71 | 72 | html! { 73 | 74 |
75 | { &_class_name } 76 | { "Class Name" } 77 |
78 | { owner_name.view("Owner Name") } 79 | { owner_class.view("Owner Class") } 80 | { for object_children } 81 |
82 | } 83 | } 84 | } 85 | 86 | impl RawUi for RcRef { 87 | fn view(&self, label: &str) -> yew::Html { 88 | self.borrow().proxy.view(label) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/save_data/shared/appearance.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use indexmap::IndexMap; 3 | use serde::{ser::SerializeTupleStruct, Deserialize, Deserializer, Serialize, Serializer}; 4 | 5 | use super::Vector; 6 | 7 | #[rcize_fields] 8 | #[derive(Deserialize, Serialize, Clone, RawUi)] 9 | pub struct Appearance { 10 | combat_appearance: PlayerAppearanceType, 11 | casual_id: i32, 12 | full_body_id: i32, 13 | torso_id: i32, 14 | shoulder_id: i32, 15 | arm_id: i32, 16 | leg_id: i32, 17 | specular_id: i32, 18 | tint1_id: i32, 19 | tint2_id: i32, 20 | tint3_id: i32, 21 | pattern_id: i32, 22 | pattern_color_id: i32, 23 | helmet_id: i32, 24 | pub head_morph: Option, 25 | } 26 | 27 | #[derive(Deserialize, Serialize, Clone, RawUi)] 28 | enum PlayerAppearanceType { 29 | Parts, 30 | Full, 31 | } 32 | 33 | #[rcize_fields] 34 | #[derive(Deserialize, Serialize, Clone, RawUi, RawUiChildren)] 35 | pub struct HeadMorph { 36 | pub hair_mesh: String, 37 | pub accessory_mesh: Vec, 38 | pub morph_features: IndexMap, 39 | pub offset_bones: IndexMap, 40 | pub lod0_vertices: Vec, 41 | pub lod1_vertices: Vec, 42 | pub lod2_vertices: Vec, 43 | pub lod3_vertices: Vec, 44 | pub scalar_parameters: IndexMap, 45 | pub vector_parameters: IndexMap, 46 | pub texture_parameters: IndexMap, 47 | } 48 | 49 | #[derive(Default, Clone)] 50 | pub struct LinearColor { 51 | pub r: f32, 52 | pub g: f32, 53 | pub b: f32, 54 | pub a: f32, 55 | } 56 | 57 | impl<'de> Deserialize<'de> for LinearColor { 58 | fn deserialize(deserializer: D) -> Result 59 | where 60 | D: Deserializer<'de>, 61 | { 62 | #[derive(Deserialize)] 63 | struct LinearColor(f32, f32, f32, f32); 64 | 65 | let LinearColor(r, g, b, a) = Deserialize::deserialize(deserializer)?; 66 | Ok(Self { r, g, b, a }) 67 | } 68 | } 69 | 70 | impl serde::Serialize for LinearColor { 71 | fn serialize(&self, serializer: S) -> Result 72 | where 73 | S: Serializer, 74 | { 75 | let mut linear_color = serializer.serialize_tuple_struct("LinearColor", 4)?; 76 | linear_color.serialize_field(&self.r)?; 77 | linear_color.serialize_field(&self.g)?; 78 | linear_color.serialize_field(&self.b)?; 79 | linear_color.serialize_field(&self.a)?; 80 | linear_color.end() 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | mod test { 86 | use std::fs; 87 | 88 | use anyhow::Result; 89 | 90 | use super::*; 91 | use crate::unreal; 92 | 93 | #[test] 94 | fn gibbed_head_morph() -> Result<()> { 95 | let me2 = fs::read("test/GibbedME2.me2headmorph")?; 96 | let me3 = fs::read("test/GibbedME3.me3headmorph")?; 97 | 98 | // Deserialize 99 | let _: HeadMorph = unreal::Deserializer::from_bytes(&me2[31..])?; 100 | let _: HeadMorph = unreal::Deserializer::from_bytes(&me3[31..])?; 101 | 102 | Ok(()) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/gui/components/helper.rs: -------------------------------------------------------------------------------- 1 | use gloo::utils; 2 | use web_sys::HtmlElement; 3 | use yew::prelude::*; 4 | 5 | use crate::gui::format_code; 6 | 7 | pub enum Msg { 8 | Hover, 9 | Out, 10 | } 11 | 12 | #[derive(Properties, PartialEq)] 13 | pub struct Props { 14 | pub text: &'static str, 15 | } 16 | 17 | pub struct Helper { 18 | popup_ref: NodeRef, 19 | hovered: bool, 20 | } 21 | 22 | impl Component for Helper { 23 | type Message = Msg; 24 | type Properties = Props; 25 | 26 | fn create(_ctx: &Context) -> Self { 27 | Helper { popup_ref: Default::default(), hovered: false } 28 | } 29 | 30 | fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { 31 | match msg { 32 | Msg::Hover => { 33 | self.hovered = true; 34 | true 35 | } 36 | Msg::Out => { 37 | self.hovered = false; 38 | true 39 | } 40 | } 41 | } 42 | 43 | fn rendered(&mut self, _ctx: &Context, _first_render: bool) { 44 | // Keep the popup in the viewport 45 | if let Some(popup) = self.popup_ref.cast::() { 46 | let viewport_width = utils::document().document_element().unwrap().client_width(); 47 | let client_rect = popup.get_bounding_client_rect(); 48 | let width = client_rect.width() as i32; 49 | let left = client_rect.left() as i32; 50 | let right = left + width; 51 | 52 | if right > viewport_width - 30 { 53 | let _ = 54 | popup.style().set_property("left", &format!("{}px", viewport_width - right)); 55 | let _ = popup.style().set_property("top", "30px"); 56 | } else { 57 | let _ = popup.style().set_property("left", "30px"); 58 | let _ = popup.style().set_property("top", "10px"); 59 | } 60 | } 61 | } 62 | 63 | fn view(&self, ctx: &Context) -> Html { 64 | let text = ctx.props().text.split_terminator('\n').map(|text| { 65 | html! {

{ format_code(text) }

} 66 | }); 67 | html! { 68 |
69 |
73 | { "(?)" } 74 |
75 |
91 | { for text } 92 |
93 |
94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/rpc/dialog.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::path::PathBuf; 3 | 4 | use wry::application::window::Window; 5 | 6 | use super::command::DialogParams; 7 | 8 | pub fn open_save(window: &Window, last_dir: bool) -> Option { 9 | let mut dialog = rfd::FileDialog::new() 10 | .add_filter("Mass Effect Trilogy Save", &["pcsav", "xbsav", "ps4sav", "MassEffectSave"]) 11 | .add_filter("All Files", &["*"]); 12 | 13 | dialog = with_parent(dialog, window); 14 | 15 | if !last_dir { 16 | if let Some(bioware_dir) = bioware_dir() { 17 | dialog = dialog.set_directory(bioware_dir); 18 | } 19 | } 20 | 21 | dialog.pick_file() 22 | } 23 | 24 | pub fn save_save(window: &Window, params: DialogParams) -> Option { 25 | let DialogParams { path, filters } = params; 26 | 27 | let file_name = path.file_name().map(OsStr::to_string_lossy).unwrap_or_default(); 28 | 29 | let mut dialog = rfd::FileDialog::new().set_file_name(&file_name); 30 | dialog = with_parent(dialog, window); 31 | 32 | for (filter, extensions) in filters { 33 | let extensions: Vec<&str> = extensions.iter().map(String::as_str).collect(); 34 | dialog = dialog.add_filter(&filter, &extensions); 35 | } 36 | 37 | let directory = path 38 | .parent() 39 | .and_then(|parent| parent.is_dir().then(|| parent.to_owned())) 40 | .or_else(bioware_dir); 41 | 42 | if let Some(directory) = directory { 43 | dialog = dialog.set_directory(directory); 44 | } 45 | 46 | dialog.save_file() 47 | } 48 | 49 | pub fn import_head_morph(window: &Window) -> Option { 50 | let dialog = rfd::FileDialog::new() 51 | .add_filter("Head Morph", &["ron", "me2headmorph", "me3headmorph"]) 52 | .add_filter("All Files", &["*"]); 53 | 54 | with_parent(dialog, window).pick_file() 55 | } 56 | 57 | pub fn export_head_morph(window: &Window) -> Option { 58 | let dialog = rfd::FileDialog::new().add_filter("Head Morph", &["ron"]); 59 | with_parent(dialog, window).save_file() 60 | } 61 | 62 | #[cfg(target_os = "windows")] 63 | fn bioware_dir() -> Option { 64 | dirs::document_dir().and_then(|mut path| { 65 | path.push("BioWare\\"); 66 | path.is_dir().then(|| path) 67 | }) 68 | } 69 | 70 | // FIXME: Find some nicer way of finding where the game saves are. 71 | // Currently, this should be universal for everyone who has their 72 | // Mass Effect games installed in the default steam library, in 73 | // the user's home directory. 74 | #[cfg(target_os = "linux")] 75 | fn bioware_dir() -> Option { 76 | dirs::home_dir().and_then(|mut path| { 77 | path.push(".steam/root/steamapps/compatdata/1328670/pfx/drive_c/users/steamuser/My Documents/BioWare/"); 78 | path.is_dir().then(|| path) 79 | }) 80 | } 81 | 82 | #[cfg(all(not(target_os = "linux"), not(target_os = "windows")))] 83 | fn bioware_dir() -> Option { 84 | None 85 | } 86 | 87 | // FIXME: Remove this and set directly `set_parent` when `tao` will implement `raw_window_handle` for linux 88 | #[cfg(not(target_os = "linux"))] 89 | fn with_parent(dialog: rfd::FileDialog, window: &Window) -> rfd::FileDialog { 90 | dialog.set_parent(window) 91 | } 92 | 93 | #[cfg(target_os = "linux")] 94 | fn with_parent(dialog: rfd::FileDialog, _: &Window) -> rfd::FileDialog { 95 | dialog 96 | } 97 | -------------------------------------------------------------------------------- /Makefile.toml: -------------------------------------------------------------------------------- 1 | [config] 2 | default_to_workspace = false 3 | 4 | # Utils 5 | [tasks.npm-update] 6 | command = "npm" 7 | args = ["update"] 8 | 9 | [tasks.npm-update.windows] 10 | command = "powershell" 11 | args = ["-Command", "npm", "update"] 12 | 13 | [tasks.update] 14 | command = "cargo" 15 | args = ["update"] 16 | dependencies = ["npm-update"] 17 | 18 | [tasks.fmt] 19 | install_crate = "rustfmt" 20 | command = "cargo" 21 | args = ["fmt", "--all"] 22 | 23 | [tasks.clippy] 24 | install_crate = "clippy" 25 | command = "cargo" 26 | args = ["clippy", "--all"] 27 | dependencies = ["fmt"] 28 | 29 | [tasks.outdated] 30 | install_crate = "outdated" 31 | command = "cargo" 32 | args = ["outdated", "-wR"] 33 | dependencies = ["update"] 34 | 35 | [tasks.test] 36 | command = "cargo" 37 | args = ["test", "--all"] 38 | dependencies = ["update"] 39 | 40 | # Serve 41 | [tasks.tailwind-watch] 42 | command = "npm" 43 | args = ["run", "watch"] 44 | 45 | [tasks.tailwind-watch.windows] 46 | command = "powershell" 47 | args = ["-Command", "npm", "run", "watch"] 48 | 49 | [tasks.trunk-serve] 50 | install_crate = "trunk" 51 | command = "trunk" 52 | args = ["serve", "--dist", "target/dist"] 53 | 54 | [tasks.serve] 55 | run_task = { name = ["tailwind-watch", "trunk-serve"], parallel = true } 56 | 57 | # Build 58 | [tasks.tailwind-build] 59 | command = "npm" 60 | args = ["run", "build"] 61 | 62 | [tasks.tailwind-build.windows] 63 | command = "powershell" 64 | args = ["-Command", "npm", "run", "build"] 65 | 66 | [tasks.trunk-build] 67 | install_crate = "trunk" 68 | command = "trunk" 69 | args = ["build", "--dist", "target/dist"] 70 | 71 | [tasks.build] 72 | clear = true 73 | command = "cargo" 74 | args = ["build", "-p", "app"] 75 | dependencies = ["tailwind-build", "trunk-build"] 76 | 77 | [tasks.run] 78 | clear = true 79 | command = "cargo" 80 | args = ["run", "-p", "app"] 81 | dependencies = ["tailwind-build", "trunk-build"] 82 | 83 | [tasks.me1] 84 | clear = true 85 | command = "cargo" 86 | args = ["run", "-p", "app", "--", "test/ME1Save.MassEffectSave"] 87 | dependencies = ["tailwind-build", "trunk-build"] 88 | 89 | [tasks.me1le] 90 | clear = true 91 | command = "cargo" 92 | args = ["run", "-p", "app", "--", "test/ME1LeSave.pcsav"] 93 | dependencies = ["tailwind-build", "trunk-build"] 94 | 95 | [tasks.me2] 96 | clear = true 97 | command = "cargo" 98 | args = ["run", "-p", "app", "--", "test/ME2Save.pcsav"] 99 | dependencies = ["tailwind-build", "trunk-build"] 100 | 101 | [tasks.me3] 102 | clear = true 103 | command = "cargo" 104 | args = ["run", "-p", "app", "--", "test/ME3Save.pcsav"] 105 | dependencies = ["tailwind-build", "trunk-build"] 106 | 107 | # Release 108 | [tasks.tailwind-release] 109 | command = "npm" 110 | args = ["run", "release"] 111 | 112 | [tasks.tailwind-release.windows] 113 | command = "powershell" 114 | args = ["-Command", "npm", "run", "release"] 115 | 116 | [tasks.trunk-release] 117 | install_script = "cargo install trunk" 118 | command = "trunk" 119 | args = ["build", "--dist", "target/dist", "--release"] 120 | 121 | [tasks.release] 122 | clear = true 123 | command = "cargo" 124 | args = ["build", "-p", "app", "--release"] 125 | dependencies = ["tailwind-release", "trunk-release"] 126 | 127 | # Cook 128 | [tasks.cook] 129 | command = "iscc" 130 | args = ["InnoSetup.iss"] 131 | dependencies = ["fmt", "clippy", "test", "release"] 132 | -------------------------------------------------------------------------------- /misc/010 templates/me1_player.bt: -------------------------------------------------------------------------------- 1 | 2 | // Bool 3 | typedef struct { 4 | uint value: 1; 5 | uint : 31; 6 | } bool ; 7 | 8 | string read_bool(bool &b) { 9 | if(b.value == 1) 10 | return "true"; 11 | else 12 | return "false"; 13 | } 14 | 15 | // String 16 | typedef struct { 17 | int len ; 18 | SetBackColor(0x000055 + len); 19 | 20 | // Détection utf8 21 | if (len < 0) { 22 | wchar_t chars[Abs(len)]; 23 | } 24 | else { 25 | char chars[len]; 26 | } 27 | } String ; 28 | 29 | string read_string(String &s) { 30 | if(exists(s.chars)) 31 | return s.chars; 32 | else 33 | return ""; 34 | } 35 | 36 | // Name 37 | typedef struct { 38 | String str; 39 | FSkip(8); 40 | } Name ; 41 | 42 | string read_name(Name &n) { 43 | return read_string(n.str); 44 | } 45 | 46 | // Class 47 | struct Class { 48 | uint index_package; 49 | FSkip(4); 50 | uint index_class; 51 | FSkip(4); 52 | uint index_link; 53 | uint index_object; 54 | FSkip(4); 55 | }; 56 | 57 | // Object 58 | struct Object { 59 | int index_class; 60 | uint index_class_parent; 61 | uint index_link; 62 | uint index_object; 63 | uint index_value; 64 | uint index_archtype_name; 65 | uint64 flag; 66 | uint data_size; 67 | uint data_offset; 68 | FSkip(32); 69 | }; 70 | 71 | // Data 72 | struct Data { 73 | FSkip(4); 74 | uint name_index; 75 | FSkip(4); 76 | uint type_index; 77 | FSkip(4); 78 | uint size; 79 | 80 | local int i = 0; 81 | while (ReadInt64(FTell() + i) != 379) { 82 | i++; 83 | } 84 | FSkip(i); 85 | int none; 86 | int end; 87 | }; 88 | 89 | // SaveGame 90 | struct { 91 | FSeek(0x8); 92 | int header_offset ; 93 | FSeek(header_offset); 94 | struct { 95 | uint magic ; 96 | uint16 low_version; 97 | uint16 high_version; 98 | uint data_offset ; 99 | String upx_name; 100 | uint flags ; 101 | uint names_len; 102 | uint names_offset ; 103 | uint objects_len; 104 | uint objects_offset ; 105 | uint classes_len; 106 | uint classes_offset ; 107 | uint no_mans_land_offset ; 108 | FSkip(16); 109 | uint generations ; 110 | FSkip(36 + generations * 12); 111 | uint compression; 112 | } header ; 113 | struct { 114 | FSeek(header.names_offset); 115 | Name name[header.names_len] ; 116 | } names; 117 | struct { 118 | FSeek(header.classes_offset); 119 | Class class[header.classes_len] ; 120 | } classes ; 121 | struct { 122 | FSeek(header.objects_offset); 123 | Object object[header.objects_len] ; 124 | } objects ; 125 | // No man's land 126 | FSeek(header.data_offset); 127 | struct { 128 | local int i; 129 | for (i = 0; i < header.objects_len; i++) { 130 | FSeek(objects.object[i].data_offset); 131 | Data data; 132 | } 133 | } datas ; 134 | 135 | } MassEffect1 ; 136 | -------------------------------------------------------------------------------- /src/gui/raw_ui.rs: -------------------------------------------------------------------------------- 1 | use std::{any::Any, fmt::Display}; 2 | 3 | use indexmap::IndexMap; 4 | use yew::prelude::*; 5 | 6 | use crate::{ 7 | gui::components::{raw_ui::*, *}, 8 | save_data::{ 9 | mass_effect_1_le::legacy::BaseObject, shared::appearance::LinearColor, Guid, RcCell, RcRef, 10 | }, 11 | }; 12 | 13 | pub trait RawUi 14 | where 15 | Self: Clone + PartialEq + 'static, 16 | { 17 | fn view(&self, label: &str) -> yew::Html; 18 | fn view_opened(&self, label: &str, _opened: bool) -> yew::Html { 19 | self.view(label) 20 | } 21 | } 22 | 23 | pub trait RawUiChildren 24 | where 25 | Self: Clone + PartialEq + 'static, 26 | { 27 | fn children(&self) -> Vec; 28 | } 29 | 30 | // Implémentation des types std 31 | impl RawUi for RcCell { 32 | fn view(&self, label: &str) -> yew::Html { 33 | html! { 34 | 35 | } 36 | } 37 | } 38 | 39 | impl RawUi for RcCell { 40 | fn view(&self, label: &str) -> yew::Html { 41 | html! { 42 | 43 | } 44 | } 45 | } 46 | 47 | impl RawUi for RcCell { 48 | fn view(&self, label: &str) -> yew::Html { 49 | html! { 50 | 51 | } 52 | } 53 | } 54 | 55 | impl RawUi for RcCell { 56 | fn view(&self, label: &str) -> yew::Html { 57 | html! { 58 | 59 | } 60 | } 61 | } 62 | 63 | impl RawUi for RcRef { 64 | fn view(&self, label: &str) -> yew::Html { 65 | html! { 66 | 67 | } 68 | } 69 | } 70 | 71 | impl RawUi for RcRef> 72 | where 73 | T: RawUi, 74 | { 75 | fn view(&self, label: &str) -> yew::Html { 76 | html! { 77 | label={label.to_owned()} option={RcRef::clone(self)} /> 78 | } 79 | } 80 | } 81 | 82 | impl RawUi for RcRef> 83 | where 84 | T: RawUi + Default + Display, 85 | { 86 | fn view(&self, label: &str) -> yew::Html { 87 | // Make Vec of BaseObject not editable 88 | let is_editable = !(self as &dyn Any).is::>>>(); 89 | html! { 90 | label={label.to_owned()} vec={RcRef::clone(self)} {is_editable} /> 91 | } 92 | } 93 | } 94 | 95 | impl RawUi for RcRef> 96 | where 97 | K: Clone + 'static, 98 | V: RawUi + Default, 99 | RcRef>: Into>, 100 | { 101 | fn view(&self, label: &str) -> yew::Html { 102 | html! { 103 | label={label.to_owned()} index_map={RcRef::clone(self).into()} /> 104 | } 105 | } 106 | } 107 | 108 | // Shared 109 | impl RawUi for RcRef { 110 | fn view(&self, label: &str) -> yew::Html { 111 | html! { 112 | 113 | } 114 | } 115 | } 116 | 117 | impl RawUi for RcRef { 118 | fn view(&self, label: &str) -> yew::Html { 119 | html! { 120 | 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/services/rpc.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use serde::{de, Deserialize, Serialize}; 5 | use wasm_bindgen::JsValue; 6 | 7 | mod js { 8 | use js_sys::JsString; 9 | use wasm_bindgen::prelude::*; 10 | 11 | #[wasm_bindgen(module = "/src/services/rpc.js")] 12 | extern "C" { 13 | #[wasm_bindgen(catch)] 14 | pub async fn call(method: &str) -> Result; 15 | 16 | #[wasm_bindgen(catch)] 17 | pub async fn call_with_params(method: &str, params: JsValue) -> Result; 18 | } 19 | } 20 | 21 | // Call 22 | fn from_js_value(js: JsValue) -> Result 23 | where 24 | T: for<'a> de::Deserialize<'a>, 25 | { 26 | serde_wasm_bindgen::from_value(js).map_err(|e| anyhow!(e.to_string())) 27 | } 28 | 29 | async fn call(method: &str) -> Result 30 | where 31 | T: for<'a> de::Deserialize<'a>, 32 | { 33 | js::call(method).await.map(from_js_value).map_err(|e| anyhow!(String::from(e)))? 34 | } 35 | 36 | async fn call_with_params(method: &str, params: P) -> Result 37 | where 38 | P: Serialize, 39 | T: for<'a> de::Deserialize<'a>, 40 | { 41 | let js_params = serde_wasm_bindgen::to_value(¶ms).map_err(|e| anyhow!(e.to_string()))?; 42 | js::call_with_params(method, js_params) 43 | .await 44 | .map(from_js_value) 45 | .map_err(|e| anyhow!(String::from(e)))? 46 | } 47 | 48 | // Commands 49 | pub async fn check_for_update() -> Result<()> { 50 | call("check_for_update").await 51 | } 52 | 53 | pub async fn download_and_install_update() -> Result<()> { 54 | call("download_and_install_update").await 55 | } 56 | 57 | pub async fn open_external_link(link: &str) -> Result<()> { 58 | call_with_params("open_external_link", link).await 59 | } 60 | 61 | pub async fn save_file(rpc_file: RpcFile) -> Result<()> { 62 | call_with_params("save_file", rpc_file).await 63 | } 64 | 65 | pub async fn open_save(last_dir: bool) -> Result> { 66 | call_with_params("open_save", last_dir).await 67 | } 68 | 69 | pub async fn open_command_line_save() -> Result> { 70 | call("open_command_line_save").await 71 | } 72 | 73 | pub async fn save_save_dialog(params: DialogParams) -> Result> { 74 | call_with_params("save_save_dialog", params).await 75 | } 76 | 77 | pub async fn reload_save(path: PathBuf) -> Result { 78 | call_with_params("reload_save", path).await 79 | } 80 | 81 | pub async fn import_head_morph() -> Result> { 82 | call("import_head_morph").await 83 | } 84 | 85 | pub async fn export_head_morph_dialog() -> Result> { 86 | call("export_head_morph_dialog").await 87 | } 88 | 89 | pub async fn load_database(path: &str) -> Result { 90 | call_with_params("load_database", path).await 91 | } 92 | 93 | // Utils 94 | #[derive(Serialize)] 95 | pub struct DialogParams { 96 | pub path: PathBuf, 97 | pub filters: Vec<(&'static str, Vec<&'static str>)>, 98 | } 99 | 100 | #[derive(Deserialize, Serialize)] 101 | pub struct RpcFile { 102 | pub path: PathBuf, 103 | pub file: Base64File, 104 | } 105 | 106 | #[derive(Deserialize, Serialize)] 107 | pub struct Base64File { 108 | pub unencoded_size: usize, 109 | pub base64: String, 110 | } 111 | 112 | impl Base64File { 113 | pub fn decode(self) -> Result> { 114 | let mut vec = vec![0; self.unencoded_size]; 115 | base64::decode_config_slice(self.base64, base64::STANDARD, &mut vec)?; 116 | Ok(vec) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/gui/shared/head_morph.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{Ref, RefMut}; 2 | 3 | use yew::{context::ContextHandle, prelude::*}; 4 | 5 | use crate::{ 6 | gui::{components::Table, raw_ui::RawUiChildren}, 7 | save_data::{shared::appearance::HeadMorph as DataHeadMorph, RcRef}, 8 | services::save_handler::{Action, SaveHandler}, 9 | }; 10 | 11 | pub enum Msg { 12 | Import, 13 | HeadMorphImported(DataHeadMorph), 14 | Export, 15 | RemoveHeadMorph, 16 | } 17 | 18 | #[derive(Properties, PartialEq)] 19 | pub struct Props { 20 | pub head_morph: RcRef>>, 21 | } 22 | 23 | impl Props { 24 | fn head_morph(&self) -> Ref<'_, Option>> { 25 | self.head_morph.borrow() 26 | } 27 | 28 | fn head_morph_mut(&self) -> RefMut<'_, Option>> { 29 | self.head_morph.borrow_mut() 30 | } 31 | } 32 | 33 | pub struct HeadMorph { 34 | _db_handle: ContextHandle, 35 | save_handler: SaveHandler, 36 | } 37 | 38 | impl Component for HeadMorph { 39 | type Message = Msg; 40 | type Properties = Props; 41 | 42 | fn create(ctx: &Context) -> Self { 43 | let (save_handler, _db_handle) = 44 | ctx.link().context::(Callback::noop()).expect("no save handler provider"); 45 | 46 | HeadMorph { _db_handle, save_handler } 47 | } 48 | 49 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 50 | match msg { 51 | Msg::Import => { 52 | let callback = ctx.link().callback(Msg::HeadMorphImported); 53 | self.save_handler.action(Action::ImportHeadMorph(callback)); 54 | false 55 | } 56 | Msg::HeadMorphImported(head_morph) => { 57 | *ctx.props().head_morph_mut() = Some(head_morph.into()); 58 | true 59 | } 60 | Msg::Export => { 61 | if let Some(ref head_morph) = *ctx.props().head_morph() { 62 | self.save_handler.action(Action::ExportHeadMorph(RcRef::clone(head_morph))); 63 | } 64 | false 65 | } 66 | Msg::RemoveHeadMorph => { 67 | ctx.props().head_morph_mut().take(); 68 | true 69 | } 70 | } 71 | } 72 | 73 | fn view(&self, ctx: &Context) -> Html { 74 | let head_morph = ctx.props().head_morph(); 75 | let export_remove = head_morph.is_some().then(|| { 76 | html! { 77 | <> 78 | 81 | {"-"} 82 | 85 | 86 | } 87 | }); 88 | let raw = head_morph.as_ref().map(|head_morph| { 89 | html! { 90 | 91 | { for head_morph.children() } 92 |
93 | } 94 | }); 95 | html! { 96 |
97 |
98 | 101 | { for export_remove } 102 |
103 |
104 | { for raw } 105 |
106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/gui/mass_effect_3/plot_variable.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefMut; 2 | 3 | use indexmap::IndexMap; 4 | use yew::prelude::*; 5 | 6 | use crate::{ 7 | gui::{ 8 | components::{CheckBox, Table}, 9 | raw_ui::RawUi, 10 | }, 11 | save_data::{ 12 | mass_effect_3::plot_db::PlotVariable as PlotVariableDb, shared::plot::BitVec, RcCell, RcRef, 13 | }, 14 | }; 15 | 16 | pub enum Msg { 17 | ChangeBool(usize, bool), 18 | } 19 | 20 | #[derive(Properties, PartialEq)] 21 | pub struct Props { 22 | pub title: Option, 23 | pub booleans: RcRef, 24 | pub variables: RcRef>>, 25 | pub plot_variable: PlotVariableDb, 26 | } 27 | 28 | impl Props { 29 | fn booleans_mut(&self) -> RefMut<'_, BitVec> { 30 | self.booleans.borrow_mut() 31 | } 32 | } 33 | 34 | pub struct PlotVariable {} 35 | 36 | impl Component for PlotVariable { 37 | type Message = Msg; 38 | type Properties = Props; 39 | 40 | fn create(ctx: &Context) -> Self { 41 | let mut this = PlotVariable {}; 42 | this.add_missing_plots(ctx); 43 | this 44 | } 45 | 46 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 47 | match msg { 48 | Msg::ChangeBool(idx, value) => { 49 | if let Some(mut plot) = ctx.props().booleans_mut().get_mut(idx) { 50 | *plot = value; 51 | } 52 | false 53 | } 54 | } 55 | } 56 | 57 | fn changed(&mut self, ctx: &Context) -> bool { 58 | self.add_missing_plots(ctx); 59 | true 60 | } 61 | 62 | fn view(&self, ctx: &Context) -> Html { 63 | let Props { title, booleans, variables, plot_variable, .. } = &ctx.props(); 64 | let PlotVariableDb { booleans: bool_db, variables: var_db } = &plot_variable; 65 | 66 | let booleans = bool_db.iter().map(|(&idx, label)| match booleans.borrow().get(idx) { 67 | Some(value) => html! { 68 | 73 | }, 74 | None => Html::default(), 75 | }); 76 | 77 | let variables = var_db.iter().map(|(db_key, label)| { 78 | let value = variables.borrow().iter().find_map(|(key, value)| { 79 | db_key.eq_ignore_ascii_case(key).then(|| RcCell::clone(value)) 80 | }); 81 | match value { 82 | Some(value) => value.view(label), 83 | None => Html::default(), 84 | } 85 | }); 86 | 87 | html! { 88 | 89 | { for booleans } 90 | { for variables } 91 |
92 | } 93 | } 94 | } 95 | 96 | impl PlotVariable { 97 | fn add_missing_plots(&mut self, ctx: &Context) { 98 | let Props { booleans, variables, plot_variable, .. } = &mut ctx.props(); 99 | let PlotVariableDb { booleans: bool_db, variables: var_db } = &plot_variable; 100 | 101 | // Booleans 102 | if let Some(&max) = bool_db.keys().max() { 103 | let mut booleans = booleans.borrow_mut(); 104 | if max >= booleans.len() { 105 | booleans.resize(max + 1, false); 106 | }; 107 | } 108 | 109 | // Variables 110 | for db_key in var_db.keys().cloned() { 111 | let contains_key = 112 | variables.borrow().iter().any(|(key, _)| db_key.eq_ignore_ascii_case(key)); 113 | if !contains_key { 114 | variables.borrow_mut().entry(db_key).or_default(); 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/save_data/mass_effect_1_le/player.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::save_data::{ 4 | shared::{ 5 | appearance::HeadMorph, 6 | player::{Notoriety, Origin}, 7 | }, 8 | Dummy, 9 | }; 10 | 11 | #[rcize_fields] 12 | #[derive(Deserialize, Serialize, Clone, RawUi)] 13 | pub struct Player { 14 | pub is_female: bool, 15 | pub localized_class_name: i32, 16 | pub player_class: Me1LeClass, 17 | pub level: i32, 18 | pub current_xp: f32, 19 | pub first_name: String, 20 | localized_last_name: i32, 21 | pub origin: Origin, 22 | pub notoriety: Notoriety, 23 | pub specialization_bonus_id: i32, 24 | spectre_rank: u8, 25 | pub talent_points: i32, 26 | talent_pool_points: i32, 27 | mapped_talent: String, 28 | pub head_morph: Option, 29 | pub simple_talents: Vec, 30 | pub complex_talents: Vec, 31 | pub inventory: Inventory, 32 | pub credits: i32, 33 | pub medigel: i32, 34 | pub grenades: f32, 35 | pub omnigel: f32, 36 | pub face_code: String, 37 | armor_overridden: bool, 38 | pub auto_levelup_template_id: i32, 39 | health_per_level: f32, 40 | stability: f32, 41 | race: u8, 42 | toxic: f32, 43 | stamina: i32, 44 | focus: i32, 45 | precision: i32, 46 | coordination: i32, 47 | attribute_primary: u8, 48 | attribute_secondary: u8, 49 | skill_charm: f32, 50 | skill_intimidate: f32, 51 | skill_haggle: f32, 52 | health: f32, 53 | shield: f32, 54 | xp_level: i32, 55 | is_driving: bool, 56 | pub game_options: Vec, 57 | helmet_shown: bool, 58 | _unknown: Dummy<5>, 59 | last_power: String, 60 | health_max: f32, 61 | hotkeys: Vec, 62 | primary_weapon: String, 63 | secondary_weapon: String, 64 | } 65 | 66 | #[derive(Deserialize, Serialize, Clone, RawUi, PartialEq)] 67 | pub enum Me1LeClass { 68 | Soldier, 69 | Engineer, 70 | Adept, 71 | Infiltrator, 72 | Sentinel, 73 | Vanguard, 74 | } 75 | 76 | #[rcize_fields] 77 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 78 | #[display(fmt = "{}", talent_id)] 79 | pub struct SimpleTalent { 80 | pub talent_id: i32, 81 | pub current_rank: i32, 82 | } 83 | 84 | #[rcize_fields] 85 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 86 | #[display(fmt = "{}", talent_id)] 87 | pub struct ComplexTalent { 88 | pub talent_id: i32, 89 | pub current_rank: i32, 90 | pub max_rank: i32, 91 | pub level_offset: i32, 92 | pub levels_per_rank: i32, 93 | pub visual_order: i32, 94 | prereq_talent_ids: Vec, 95 | prereq_talent_ranks: Vec, 96 | } 97 | 98 | #[rcize_fields] 99 | #[derive(Deserialize, Serialize, Clone, Default, RawUi)] 100 | pub struct Inventory { 101 | pub equipment: Vec, 102 | pub quick_slots: Vec, 103 | pub inventory: Vec, 104 | pub buy_pack: Vec, 105 | } 106 | 107 | #[allow(clippy::upper_case_acronyms)] 108 | #[derive(Deserialize, Serialize, Copy, Clone, RawUi)] 109 | pub enum ItemLevel { 110 | None, 111 | I, 112 | II, 113 | III, 114 | IV, 115 | V, 116 | VI, 117 | VII, 118 | VIII, 119 | IX, 120 | X, 121 | } 122 | 123 | impl Default for ItemLevel { 124 | fn default() -> Self { 125 | ItemLevel::None 126 | } 127 | } 128 | 129 | #[rcize_fields] 130 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 131 | #[display(fmt = "")] 132 | pub struct Item { 133 | pub item_id: i32, 134 | pub item_level: ItemLevel, 135 | pub manufacturer_id: i32, 136 | pub plot_conditional_id: i32, 137 | pub new_item: bool, 138 | junk: bool, 139 | pub attached_mods: Vec, 140 | } 141 | 142 | #[rcize_fields] 143 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 144 | #[display(fmt = "")] 145 | pub struct ItemMod { 146 | pub item_id: i32, 147 | pub item_level: ItemLevel, 148 | pub manufacturer_id: i32, 149 | pub plot_conditional_id: i32, 150 | } 151 | 152 | #[rcize_fields] 153 | #[derive(Deserialize, Serialize, Clone, Default, Display, RawUi)] 154 | #[display(fmt = "")] 155 | struct Hotkey { 156 | pawn: i32, 157 | event: i32, 158 | } 159 | -------------------------------------------------------------------------------- /src/gui/components/input_number.rs: -------------------------------------------------------------------------------- 1 | use web_sys::HtmlInputElement; 2 | use yew::prelude::*; 3 | 4 | use crate::{gui::components::Helper, save_data::RcCell}; 5 | 6 | use super::CallbackType; 7 | 8 | #[derive(Clone)] 9 | pub enum NumberType { 10 | Byte(RcCell), 11 | Int(RcCell), 12 | Float(RcCell), 13 | } 14 | 15 | impl PartialEq for NumberType { 16 | fn eq(&self, other: &NumberType) -> bool { 17 | match (self, other) { 18 | (NumberType::Byte(byte), NumberType::Byte(other)) => byte == other, 19 | (NumberType::Int(integer), NumberType::Int(other)) => integer == other, 20 | (NumberType::Float(float), NumberType::Float(other)) => float == other, 21 | _ => false, 22 | } 23 | } 24 | } 25 | 26 | pub enum Msg { 27 | Change(Event), 28 | } 29 | 30 | #[derive(Properties, PartialEq)] 31 | pub struct Props { 32 | pub label: String, 33 | pub value: NumberType, 34 | pub helper: Option<&'static str>, 35 | pub onchange: Option>, 36 | } 37 | 38 | pub struct InputNumber; 39 | 40 | impl Component for InputNumber { 41 | type Message = Msg; 42 | type Properties = Props; 43 | 44 | fn create(_ctx: &Context) -> Self { 45 | InputNumber 46 | } 47 | 48 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 49 | match msg { 50 | Msg::Change(event) => { 51 | if let Some(input) = event.target_dyn_into::() { 52 | let value = input.value_as_number(); 53 | 54 | if value.is_nan() { 55 | return true; 56 | } 57 | 58 | match ctx.props().value { 59 | NumberType::Byte(ref byte) => { 60 | let value = value as u8; 61 | byte.set(value); 62 | 63 | if let Some(ref callback) = ctx.props().onchange { 64 | callback.emit(CallbackType::Byte(value)); 65 | } 66 | } 67 | NumberType::Int(ref integer) => { 68 | let value = value as i32; 69 | integer.set(value); 70 | 71 | if let Some(ref callback) = ctx.props().onchange { 72 | callback.emit(CallbackType::Int(value)); 73 | } 74 | } 75 | NumberType::Float(ref float) => { 76 | let value = value.clamp(f32::MIN as f64, f32::MAX as f64) as f32; 77 | float.set(value); 78 | 79 | if let Some(ref callback) = ctx.props().onchange { 80 | callback.emit(CallbackType::Float(value)); 81 | } 82 | } 83 | } 84 | true 85 | } else { 86 | false 87 | } 88 | } 89 | } 90 | } 91 | 92 | fn view(&self, ctx: &Context) -> Html { 93 | let (value, placeholder) = match ctx.props().value { 94 | NumberType::Byte(ref byte) => (byte.get().to_string(), ""), 95 | NumberType::Int(ref integer) => (integer.get().to_string(), ""), 96 | NumberType::Float(ref float) => { 97 | let mut ryu = ryu::Buffer::new(); 98 | (ryu.format(float.get()).trim_end_matches(".0").to_owned(), "") 99 | } 100 | }; 101 | 102 | let helper = ctx.props().helper.as_ref().map(|&helper| { 103 | html! { 104 | 105 | } 106 | }); 107 | 108 | html! { 109 | 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /app/src/windows/auto_update.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use anyhow::Error; 4 | use lazy_static::lazy_static; 5 | use parking_lot::Mutex; 6 | use serde::Deserialize; 7 | use serde_json::json; 8 | use tokio::{fs, process}; 9 | use wry::application::event_loop::EventLoopProxy; 10 | 11 | use crate::rpc; 12 | 13 | const GITHUB_API: &str = 14 | "https://api.github.com/repos/KarlitosVII/trilogy-save-editor/releases/latest"; 15 | 16 | #[derive(Deserialize, Debug)] 17 | struct GithubResponse { 18 | tag_name: String, 19 | prerelease: bool, 20 | assets: Vec, 21 | } 22 | 23 | #[derive(Deserialize, Debug)] 24 | struct GithubAsset { 25 | // id: usize, 26 | name: String, 27 | // content_type: String, 28 | browser_download_url: String, 29 | size: usize, 30 | } 31 | 32 | lazy_static! { 33 | pub static ref AUTO_UPDATE: AutoUpdate = AutoUpdate::new(); 34 | static ref REQWEST: reqwest::Client = { 35 | reqwest::Client::builder() 36 | .user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),)) 37 | .build() 38 | .expect("Failed to initialize http client") 39 | }; 40 | } 41 | 42 | pub struct AutoUpdate { 43 | update_available: Mutex>, 44 | } 45 | 46 | impl AutoUpdate { 47 | fn new() -> Self { 48 | AutoUpdate { update_available: Mutex::new(None) } 49 | } 50 | 51 | pub async fn check_for_update(&self, proxy: EventLoopProxy) { 52 | let result = async { 53 | let response = REQWEST.get(GITHUB_API).send().await?.json().await?; 54 | let GithubResponse { tag_name, prerelease, assets } = response; 55 | 56 | if !prerelease && tag_name.trim_start_matches('v') != env!("CARGO_PKG_VERSION") { 57 | if let Some(update_available) = 58 | assets.into_iter().find(|asset| asset.name.ends_with("setup.exe")) 59 | { 60 | *self.update_available.lock() = Some(update_available); 61 | let _ = proxy.send_event(rpc::Event::DispatchCustomEvent( 62 | "tse_update_available", 63 | json!({}), 64 | )); 65 | } 66 | } 67 | Ok::<_, Error>(()) 68 | }; 69 | 70 | if let Err(err) = result.await { 71 | let _ = proxy.send_event(rpc::Event::DispatchCustomEvent( 72 | "tse_update_error", 73 | json!({ "error": err.to_string() }), 74 | )); 75 | } 76 | } 77 | 78 | pub async fn download_and_install(&self, proxy: EventLoopProxy) { 79 | let asset = self.update_available.lock().take(); 80 | if let Some(GithubAsset { name, browser_download_url, size }) = asset { 81 | let result = async { 82 | let send_progress = |progress: f64| { 83 | let _ = proxy.send_event(rpc::Event::DispatchCustomEvent( 84 | "tse_update_progress", 85 | json!({ "progress": progress }), 86 | )); 87 | }; 88 | 89 | // Download 90 | let mut response = REQWEST.get(browser_download_url).send().await?; 91 | let mut setup = Vec::with_capacity(size); 92 | 93 | send_progress(0.0); 94 | let size = size as f64; 95 | while let Some(chunk) = response.chunk().await? { 96 | setup.extend(chunk); 97 | send_progress(setup.len() as f64 / size); 98 | } 99 | 100 | // Install 101 | let temp_dir = env::temp_dir().join("trilogy-save-editor"); 102 | let path = temp_dir.join(name); 103 | 104 | // If not exists 105 | if fs::metadata(&temp_dir).await.is_err() { 106 | fs::create_dir(temp_dir).await?; 107 | } 108 | fs::write(&path, setup).await?; 109 | 110 | process::Command::new(path).arg("/SILENT").arg("/NOICONS").spawn()?; 111 | 112 | let _ = proxy.send_event(rpc::Event::CloseWindow); 113 | 114 | Ok::<_, Error>(()) 115 | }; 116 | 117 | if let Err(err) = result.await { 118 | let _ = proxy.send_event(rpc::Event::DispatchCustomEvent( 119 | "tse_update_error", 120 | json!({ "error": err.to_string() }), 121 | )); 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/gui/components/color_picker.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{Ref, RefMut}; 2 | 3 | use anyhow::ensure; 4 | use web_sys::HtmlInputElement; 5 | use yew::prelude::*; 6 | 7 | use crate::{ 8 | gui::components::{CallbackType, InputNumber, NumberType}, 9 | save_data::{shared::appearance::LinearColor, RcRef}, 10 | }; 11 | 12 | pub enum Msg { 13 | R(CallbackType), 14 | G(CallbackType), 15 | B(CallbackType), 16 | A(CallbackType), 17 | Change(Event), 18 | } 19 | 20 | #[derive(Properties, PartialEq)] 21 | pub struct Props { 22 | pub label: String, 23 | pub color: RcRef, 24 | } 25 | 26 | impl Props { 27 | fn color(&self) -> Ref<'_, LinearColor> { 28 | self.color.borrow() 29 | } 30 | 31 | fn color_mut(&self) -> RefMut<'_, LinearColor> { 32 | self.color.borrow_mut() 33 | } 34 | } 35 | 36 | pub struct ColorPicker; 37 | 38 | impl Component for ColorPicker { 39 | type Message = Msg; 40 | type Properties = Props; 41 | 42 | fn create(_ctx: &Context) -> Self { 43 | ColorPicker 44 | } 45 | 46 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 47 | match msg { 48 | Msg::R(CallbackType::Float(value)) => { 49 | ctx.props().color_mut().r = value; 50 | true 51 | } 52 | Msg::G(CallbackType::Float(value)) => { 53 | ctx.props().color_mut().g = value; 54 | true 55 | } 56 | Msg::B(CallbackType::Float(value)) => { 57 | ctx.props().color_mut().b = value; 58 | true 59 | } 60 | Msg::A(CallbackType::Float(value)) => { 61 | ctx.props().color_mut().a = value; 62 | true 63 | } 64 | Msg::Change(event) => { 65 | if let Some(input) = event.target_dyn_into::() { 66 | let color = input.value(); 67 | 68 | let hex_to_rgb = |hex: String| { 69 | ensure!(hex.len() == 7, "invalid color"); 70 | 71 | let r = u8::from_str_radix(&hex[1..3], 16)?; 72 | let g = u8::from_str_radix(&hex[3..5], 16)?; 73 | let b = u8::from_str_radix(&hex[5..7], 16)?; 74 | Ok((r, g, b)) 75 | }; 76 | 77 | if let Ok((r, g, b)) = hex_to_rgb(color) { 78 | let mut color = ctx.props().color_mut(); 79 | color.r = r as f32 / 255.0; 80 | color.g = g as f32 / 255.0; 81 | color.b = b as f32 / 255.0; 82 | return true; 83 | } 84 | } 85 | false 86 | } 87 | _ => unreachable!(), 88 | } 89 | } 90 | 91 | fn view(&self, ctx: &Context) -> Html { 92 | let (r, g, b, a) = { 93 | let colors = ctx.props().color(); 94 | (colors.r, colors.g, colors.b, colors.a) 95 | }; 96 | let hex_color = { 97 | let max_color = r.max(g.max(b)); 98 | let ratio = max_color.max(1.0); 99 | let r = (r * 255.0 / ratio) as u8; 100 | let g = (g * 255.0 / ratio) as u8; 101 | let b = (b * 255.0 / ratio) as u8; 102 | format!("#{:02X}{:02X}{:02X}", r, g, b) 103 | }; 104 | 105 | html! { 106 |
107 | 108 | 109 | 110 | 111 | 121 |
122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/src/rpc/command.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use anyhow::{Error, Result}; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use super::{dialog, Event, RpcUtils}; 10 | 11 | // Commands 12 | pub fn init(utils: &RpcUtils) { 13 | utils.window.set_visible(true); 14 | } 15 | 16 | pub fn minimize(utils: &RpcUtils) { 17 | utils.window.set_minimized(true); 18 | } 19 | 20 | pub fn toggle_maximize(utils: &RpcUtils) { 21 | let is_maximized = utils.window.is_maximized(); 22 | utils.window.set_maximized(!is_maximized); 23 | } 24 | 25 | pub fn drag_window(utils: &RpcUtils) { 26 | let _ = utils.window.drag_window(); 27 | } 28 | 29 | pub fn close(utils: &RpcUtils) { 30 | let _ = utils.event_proxy.send_event(Event::CloseWindow); 31 | } 32 | 33 | #[cfg(target_os = "windows")] 34 | pub fn check_for_update(utils: &RpcUtils) -> Result<()> { 35 | use crate::windows::auto_update::AUTO_UPDATE; 36 | 37 | let proxy = utils.event_proxy.clone(); 38 | tokio::spawn(async move { 39 | AUTO_UPDATE.check_for_update(proxy).await; 40 | }); 41 | 42 | Ok(()) 43 | } 44 | 45 | #[cfg(target_os = "windows")] 46 | pub fn download_and_install_update(utils: &RpcUtils) -> Result<()> { 47 | use crate::windows::auto_update::AUTO_UPDATE; 48 | 49 | let proxy = utils.event_proxy.clone(); 50 | tokio::spawn(async move { 51 | AUTO_UPDATE.download_and_install(proxy).await; 52 | }); 53 | 54 | Ok(()) 55 | } 56 | 57 | #[cfg(not(target_os = "windows"))] 58 | pub fn check_for_update(_: &RpcUtils) -> Result<()> { 59 | Ok(()) 60 | } 61 | 62 | #[cfg(not(target_os = "windows"))] 63 | pub fn download_and_install_update(_: &RpcUtils) -> Result<()> { 64 | Ok(()) 65 | } 66 | 67 | pub fn open_external_link(_: &RpcUtils, link: PathBuf) -> Result<()> { 68 | opener::open(link).map_err(Error::from) 69 | } 70 | 71 | pub fn save_file(_: &RpcUtils, rpc_file: RpcFile) -> Result<()> { 72 | write_file(rpc_file) 73 | } 74 | 75 | pub fn open_save(utils: &RpcUtils, last_dir: bool) -> Result> { 76 | match dialog::open_save(utils.window, last_dir) { 77 | Some(path) => open_file(path).map(Some), 78 | None => Ok(None), 79 | } 80 | } 81 | 82 | pub fn save_save_dialog(utils: &RpcUtils, params: DialogParams) -> Result> { 83 | let result = dialog::save_save(utils.window, params); 84 | Ok(result) 85 | } 86 | 87 | pub fn reload_save(_: &RpcUtils, path: PathBuf) -> Result { 88 | open_file(path) 89 | } 90 | 91 | pub fn import_head_morph(utils: &RpcUtils) -> Result> { 92 | match dialog::import_head_morph(utils.window) { 93 | Some(path) => open_file(path).map(Some), 94 | None => Ok(None), 95 | } 96 | } 97 | 98 | pub fn export_head_morph_dialog(utils: &RpcUtils) -> Result> { 99 | let result = dialog::export_head_morph(utils.window); 100 | Ok(result) 101 | } 102 | 103 | pub fn load_database(_: &RpcUtils, path: PathBuf) -> Result { 104 | #[cfg(not(debug_assertions))] 105 | let path = std::env::current_exe()?.parent().map(|parent| parent.join(&path)).unwrap_or(path); 106 | 107 | open_file(path) 108 | } 109 | 110 | // Utils 111 | fn open_file(path: PathBuf) -> Result { 112 | let file = fs::read(path.canonicalize()?)?; 113 | let unencoded_size = file.len(); 114 | let base64 = base64::encode(file); 115 | Ok(RpcFile { path, file: Base64File { unencoded_size, base64 } }) 116 | } 117 | 118 | fn write_file(rpc_file: RpcFile) -> Result<()> { 119 | let RpcFile { path, file } = rpc_file; 120 | 121 | // Backup if file exists 122 | if path.exists() { 123 | if let Some(ext) = path.extension() { 124 | let mut ext = ext.to_owned(); 125 | ext.push(".bak"); 126 | let to = Path::with_extension(&path, ext); 127 | fs::copy(&path, to)?; 128 | } 129 | } 130 | fs::write(path, file.decode()?)?; 131 | 132 | Ok(()) 133 | } 134 | 135 | #[derive(Deserialize, Default)] 136 | pub struct DialogParams { 137 | pub path: PathBuf, 138 | pub filters: Vec<(String, Vec)>, 139 | } 140 | 141 | #[derive(Deserialize, Serialize, Default)] 142 | pub struct RpcFile { 143 | pub path: PathBuf, 144 | pub file: Base64File, 145 | } 146 | 147 | #[derive(Deserialize, Serialize, Default)] 148 | pub struct Base64File { 149 | unencoded_size: usize, 150 | base64: String, 151 | } 152 | 153 | impl Base64File { 154 | pub fn decode(self) -> Result> { 155 | let mut vec = vec![0; self.unencoded_size]; 156 | base64::decode_config_slice(self.base64, base64::STANDARD, &mut vec)?; 157 | Ok(vec) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /app/src/rpc/mod.rs: -------------------------------------------------------------------------------- 1 | mod command; 2 | mod dialog; 3 | 4 | use std::env; 5 | use std::path::PathBuf; 6 | 7 | use anyhow::{bail, Context, Result}; 8 | use clap::ArgMatches; 9 | use serde_json::{json, Value}; 10 | use wry::{ 11 | application::{ 12 | event_loop::{ControlFlow, EventLoopProxy}, 13 | window::Window, 14 | }, 15 | webview::{RpcRequest, RpcResponse, WebView}, 16 | }; 17 | 18 | macro_rules! notify_commands { 19 | ($req:ident, $utils:ident => [$(command::$command:ident),* $(,)?]) => { 20 | $( 21 | if $req.method == stringify!($command) { 22 | command::$command(&$utils); 23 | return Ok(None); 24 | } 25 | )* 26 | }; 27 | } 28 | 29 | macro_rules! call_commands { 30 | ($req:ident, $utils:ident => [$(command::$command:ident),* $(,)?]) => { 31 | $( 32 | if $req.method == stringify!($command) { 33 | let response = command::$command(&$utils)?; 34 | let js_value = serde_json::to_value(&response).map(Some)?; 35 | return Ok(js_value); 36 | } 37 | )* 38 | }; 39 | } 40 | 41 | macro_rules! call_commands_with_param { 42 | ($req:ident, $utils:ident => [$(command::$command:ident),* $(,)?]) => { 43 | $( 44 | if $req.method == stringify!($command) { 45 | let params = $req.params.take().context("argument required")?; 46 | let value: [_; 1] = serde_json::from_value(params)?; 47 | let value = value.into_iter().next().unwrap_or_default(); 48 | let response = command::$command(&$utils, value)?; 49 | let js_value = serde_json::to_value(&response).map(Some)?; 50 | return Ok(js_value); 51 | } 52 | )* 53 | }; 54 | } 55 | 56 | pub struct RpcUtils<'a> { 57 | pub window: &'a Window, 58 | pub event_proxy: &'a EventLoopProxy, 59 | pub args: &'a ArgMatches, 60 | } 61 | 62 | pub fn rpc_handler(mut req: RpcRequest, utils: RpcUtils) -> Option { 63 | let mut handle_request = || -> Result> { 64 | if req.method == "open_command_line_save" { 65 | let response = if let Some(path) = utils.args.value_of("SAVE") { 66 | let mut path = PathBuf::from(path); 67 | if path.is_relative() { 68 | path = env::current_dir()?.join(path); 69 | } 70 | command::reload_save(&utils, path).map(Some)? 71 | } else { 72 | None 73 | }; 74 | let js_value = serde_json::to_value(&response).map(Some)?; 75 | return Ok(js_value); 76 | } 77 | 78 | notify_commands!(req, utils => [ 79 | command::init, 80 | command::minimize, 81 | command::toggle_maximize, 82 | command::drag_window, 83 | command::close, 84 | ]); 85 | 86 | call_commands!(req, utils => [ 87 | command::check_for_update, 88 | command::download_and_install_update, 89 | command::import_head_morph, 90 | command::export_head_morph_dialog, 91 | ]); 92 | 93 | call_commands_with_param!(req, utils => [ 94 | command::open_external_link, 95 | command::open_save, 96 | command::save_file, 97 | command::save_save_dialog, 98 | command::reload_save, 99 | command::load_database, 100 | ]); 101 | 102 | bail!("Wrong RPC method, got: {}", req.method) 103 | }; 104 | 105 | match handle_request() { 106 | Ok(None) => None, 107 | Ok(Some(response)) => Some(RpcResponse::new_result(req.id.take(), Some(response))), 108 | Err(error) => Some(RpcResponse::new_error(req.id.take(), Some(json!(error.to_string())))), 109 | } 110 | } 111 | 112 | pub enum Event { 113 | CloseWindow, 114 | DispatchCustomEvent(&'static str, serde_json::Value), 115 | } 116 | 117 | pub fn event_handler(event: Event, webview: &WebView, control_flow: &mut ControlFlow) { 118 | match event { 119 | Event::CloseWindow => *control_flow = ControlFlow::Exit, 120 | Event::DispatchCustomEvent(event, detail) => { 121 | let _ = webview.evaluate_script(&format!( 122 | r#" 123 | (() => {{ 124 | const event = new CustomEvent("{event}", {{ 125 | detail: {detail} 126 | }}); 127 | document.dispatchEvent(event); 128 | }})(); 129 | "#, 130 | event = event, 131 | detail = detail, 132 | )); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/gui/mass_effect_3/plot.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use yew::prelude::*; 3 | 4 | use crate::{ 5 | gui::{ 6 | components::{Tab, TabBar}, 7 | mass_effect_1::Me1Plot, 8 | mass_effect_2::Me2Plot, 9 | mass_effect_3::PlotVariable, 10 | shared::{IntPlotType, PlotCategory}, 11 | Theme, 12 | }, 13 | save_data::{ 14 | mass_effect_3::plot_db::Me3PlotDb, 15 | shared::plot::{BitVec, PlotCategory as PlotCategoryDb}, 16 | RcCell, RcRef, 17 | }, 18 | services::database::Databases, 19 | }; 20 | 21 | #[derive(Properties, PartialEq)] 22 | pub struct Props { 23 | pub booleans: RcRef, 24 | pub integers: IntPlotType, 25 | pub variables: RcRef>>, 26 | } 27 | 28 | #[function_component(Me3Plot)] 29 | pub fn me3_plot(props: &Props) -> Html { 30 | let dbs = use_context::().expect("no database provider"); 31 | if let Some(plot_db) = dbs.get_me3_plot() { 32 | let Props { booleans, integers, variables, .. } = props; 33 | let Me3PlotDb { 34 | general, 35 | crew, 36 | romance, 37 | missions, 38 | citadel_dlc, 39 | normandy, 40 | appearances, 41 | weapons_powers, 42 | intel, 43 | } = &*plot_db; 44 | 45 | let view_categories = |categories: &IndexMap| { 46 | categories 47 | .iter() 48 | .map(|(title, category)| { 49 | html! { 50 | 56 | } 57 | }) 58 | .collect::>() 59 | }; 60 | 61 | let categories = [ 62 | ("Crew", crew), 63 | ("Romance", romance), 64 | ("Missions", missions), 65 | ("Normandy", normandy), 66 | ("Citadel DLC", citadel_dlc), 67 | ("Appearances", appearances), 68 | ]; 69 | 70 | let categories = categories.iter().map(|(tab, categories)| { 71 | html_nested! { 72 | 73 |
74 | { for view_categories(categories) } 75 |
76 |
77 | } 78 | }); 79 | 80 | let weapons_powers = weapons_powers.iter().map(|(title, variable)| { 81 | html! { 82 | 88 | } 89 | }); 90 | 91 | html! { 92 | 93 | 94 | 99 | 100 | { for categories } 101 | 102 |
103 | { for weapons_powers } 104 |
105 |
106 | 107 | 112 | 113 | 114 | 118 | 119 | 120 | 125 | 126 |
127 | } 128 | } else { 129 | html! { 130 | <> 131 |

{ "Loading database..." }

132 |
133 | 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 2 | #![cfg_attr(debug_assertions, windows_subsystem = "console")] 3 | #![warn(clippy::all)] 4 | 5 | #[cfg(target_os = "windows")] 6 | mod windows; 7 | 8 | mod rpc; 9 | 10 | use anyhow::Result; 11 | use clap::{Arg, ArgMatches}; 12 | use image::GenericImageView; 13 | use rust_embed::RustEmbed; 14 | use serde_json::json; 15 | use wry::{ 16 | application::{ 17 | dpi::LogicalSize, 18 | event::{Event, WindowEvent}, 19 | event_loop::{ControlFlow, EventLoop}, 20 | window::{Icon, WindowBuilder}, 21 | }, 22 | http::{self, status::StatusCode}, 23 | webview::WebViewBuilder, 24 | }; 25 | 26 | #[derive(RustEmbed)] 27 | #[folder = "../target/dist/"] 28 | struct Asset; 29 | 30 | fn parse_args() -> ArgMatches { 31 | let app = clap::App::new("Trilogy Save Editor") 32 | .version(env!("CARGO_PKG_VERSION")) 33 | .author("by Karlitos") 34 | .about("A save editor for Mass Effect Trilogy (and Legendary)") 35 | .arg(Arg::new("SAVE").help("Mass Effect save file")); 36 | 37 | app.get_matches() 38 | } 39 | 40 | #[tokio::main] 41 | async fn main() -> Result<()> { 42 | #[cfg(target_os = "windows")] 43 | { 44 | // Install WebView2 45 | let should_install_webview2 = std::panic::catch_unwind(|| { 46 | wry::webview::webview_version().expect("Unable to get webview2 version") 47 | }) 48 | .is_err(); 49 | if should_install_webview2 { 50 | if let Err(err) = windows::install_webview2().await { 51 | anyhow::bail!(err) 52 | } 53 | } 54 | } 55 | 56 | let args = parse_args(); 57 | 58 | let event_loop = EventLoop::::with_user_event(); 59 | let window = WindowBuilder::new() 60 | .with_title(format!("Trilogy Save Editor - v{} by Karlitos", env!("CARGO_PKG_VERSION"))) 61 | .with_window_icon(load_icon()) 62 | .with_min_inner_size(LogicalSize::new(600, 300)) 63 | .with_inner_size(LogicalSize::new(1000, 700)) 64 | .with_visible(false) 65 | .with_decorations(false) 66 | .build(&event_loop)?; 67 | 68 | let mut last_maximized_state = window.is_maximized(); 69 | 70 | let proxy = event_loop.create_proxy(); 71 | let webview = WebViewBuilder::new(window)? 72 | .with_initialization_script(include_str!("init.js")) 73 | .with_rpc_handler(move |window, req| { 74 | rpc::rpc_handler(req, rpc::RpcUtils { window, event_proxy: &proxy, args: &args }) 75 | }) 76 | .with_custom_protocol(String::from("tse"), protocol) 77 | .with_url("tse://localhost/")? 78 | .build()?; 79 | 80 | let proxy = event_loop.create_proxy(); 81 | event_loop.run(move |event, _, control_flow| { 82 | *control_flow = ControlFlow::Wait; 83 | 84 | match event { 85 | Event::WindowEvent { event, .. } => match event { 86 | WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, 87 | WindowEvent::Resized(_) => { 88 | let _ = webview.resize(); 89 | let is_maximized = webview.window().is_maximized(); 90 | if is_maximized != last_maximized_state { 91 | last_maximized_state = is_maximized; 92 | let _ = proxy.send_event(rpc::Event::DispatchCustomEvent( 93 | "tse_maximized_state_changed", 94 | json!({ "is_maximized": is_maximized }), 95 | )); 96 | } 97 | } 98 | _ => (), 99 | }, 100 | Event::UserEvent(event) => rpc::event_handler(event, &webview, control_flow), 101 | Event::LoopDestroyed => { 102 | // Clear WebView2 Code Cache 103 | #[cfg(target_os = "windows")] 104 | windows::clear_code_cache(); 105 | } 106 | _ => (), 107 | } 108 | }); 109 | 110 | #[allow(unreachable_code)] 111 | Ok(()) 112 | } 113 | 114 | fn protocol(request: &http::Request) -> wry::Result { 115 | let mut path = request.uri().trim_start_matches("tse://localhost/"); 116 | if path.is_empty() { 117 | path = "index.html" 118 | } 119 | 120 | let response = http::ResponseBuilder::new(); 121 | match Asset::get(path) { 122 | Some(asset) => { 123 | let mime = mime_guess::from_path(path).first_or_octet_stream().to_string(); 124 | response.mimetype(&mime).body(asset.data.into()) 125 | } 126 | None => response.status(StatusCode::NOT_FOUND).body(vec![]), 127 | } 128 | } 129 | 130 | fn load_icon() -> Option { 131 | let image = image::load_from_memory(include_bytes!("../../misc/tse.png")).unwrap(); 132 | let (width, height) = image.dimensions(); 133 | let rgba = image.into_rgba8().into_raw(); 134 | Some(Icon::from_rgba(rgba, width, height).unwrap()) 135 | } 136 | -------------------------------------------------------------------------------- /src/gui/shared/plot_category.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefMut; 2 | 3 | use yew::prelude::*; 4 | 5 | use crate::{ 6 | gui::{ 7 | components::{CheckBox, Table}, 8 | raw_ui::RawUi, 9 | }, 10 | save_data::{ 11 | shared::plot::{BitVec, PlotCategory as PlotCategoryDb}, 12 | RcCell, RcRef, 13 | }, 14 | }; 15 | 16 | use super::IntPlotType; 17 | 18 | pub enum Msg { 19 | ChangeBool(usize, bool), 20 | } 21 | 22 | #[derive(Properties, PartialEq)] 23 | pub struct Props { 24 | pub title: Option, 25 | pub booleans: RcRef, 26 | pub integers: IntPlotType, 27 | pub category: PlotCategoryDb, 28 | #[prop_or(false)] 29 | pub me3_imported_me1: bool, 30 | } 31 | 32 | impl Props { 33 | fn booleans_mut(&self) -> RefMut<'_, BitVec> { 34 | self.booleans.borrow_mut() 35 | } 36 | } 37 | 38 | pub struct PlotCategory; 39 | 40 | impl Component for PlotCategory { 41 | type Message = Msg; 42 | type Properties = Props; 43 | 44 | fn create(ctx: &Context) -> Self { 45 | let mut this = PlotCategory {}; 46 | this.add_missing_plots(ctx); 47 | this 48 | } 49 | 50 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 51 | match msg { 52 | Msg::ChangeBool(idx, value) => { 53 | if let Some(mut plot) = ctx.props().booleans_mut().get_mut(idx) { 54 | *plot = value; 55 | } 56 | false 57 | } 58 | } 59 | } 60 | 61 | fn changed(&mut self, ctx: &Context) -> bool { 62 | self.add_missing_plots(ctx); 63 | true 64 | } 65 | 66 | fn view(&self, ctx: &Context) -> Html { 67 | let Props { title, booleans, integers, category, me3_imported_me1 } = &ctx.props(); 68 | let PlotCategoryDb { booleans: bool_db, integers: int_db } = category; 69 | 70 | let booleans = bool_db.iter().map(|(idx, label)| { 71 | let mut idx = *idx; 72 | if *me3_imported_me1 { 73 | idx += 10_000; 74 | } 75 | match booleans.borrow().get(idx) { 76 | Some(value) => html! { 77 | 82 | }, 83 | None => Html::default(), 84 | } 85 | }); 86 | 87 | let integers = int_db.iter().map(|(idx, label)| { 88 | let mut idx = *idx; 89 | if *me3_imported_me1 { 90 | idx += 10_000; 91 | } 92 | let value = match integers { 93 | IntPlotType::Vec(vec) => vec.borrow().get(idx).map(RcCell::clone), 94 | IntPlotType::IndexMap(index_map) => { 95 | index_map.borrow().get(&(idx as i32)).map(RcCell::clone) 96 | } 97 | }; 98 | match value { 99 | Some(value) => value.view(label), 100 | None => Html::default(), 101 | } 102 | }); 103 | 104 | html! { 105 | 106 | { for booleans } 107 | { for integers } 108 |
109 | } 110 | } 111 | } 112 | 113 | impl PlotCategory { 114 | fn add_missing_plots(&mut self, ctx: &Context) { 115 | let Props { booleans, integers, category, me3_imported_me1, .. } = &mut ctx.props(); 116 | let PlotCategoryDb { booleans: bool_db, integers: int_db } = &category; 117 | 118 | // Booleans 119 | if let Some(&(mut max)) = bool_db.keys().max() { 120 | if *me3_imported_me1 { 121 | max += 10_000; 122 | } 123 | 124 | let mut booleans = booleans.borrow_mut(); 125 | if max >= booleans.len() { 126 | booleans.resize(max + 1, false); 127 | }; 128 | } 129 | 130 | // Ints 131 | match integers { 132 | IntPlotType::Vec(ref vec) => { 133 | if let Some(&(mut max)) = int_db.keys().max() { 134 | if *me3_imported_me1 { 135 | max += 10_000; 136 | } 137 | 138 | let mut vec = vec.borrow_mut(); 139 | if max >= vec.len() { 140 | vec.resize_with(max + 1, Default::default); 141 | }; 142 | } 143 | } 144 | IntPlotType::IndexMap(ref index_map) => { 145 | for mut key in int_db.keys().copied() { 146 | if *me3_imported_me1 { 147 | key += 10_000; 148 | } 149 | index_map.borrow_mut().entry(key as i32).or_default(); 150 | } 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /macros/src/raw_ui.rs: -------------------------------------------------------------------------------- 1 | use heck::ToTitleCase; 2 | use quote::{quote, quote_spanned}; 3 | use syn::punctuated::Punctuated; 4 | use syn::spanned::Spanned; 5 | use syn::token::Comma; 6 | use syn::{self, DeriveInput, Fields, Variant}; 7 | 8 | #[allow(clippy::enum_variant_names)] 9 | pub enum Derive { 10 | RawUi, 11 | RawUiRoot, 12 | RawUiChildren, 13 | } 14 | 15 | pub fn impl_struct( 16 | ast: &DeriveInput, fields: &Fields, raw_ui_impl: Derive, 17 | ) -> proc_macro2::TokenStream { 18 | let fields = match *fields { 19 | Fields::Named(ref fields) => &fields.named, 20 | _ => panic!("non named fields not supported"), 21 | }; 22 | 23 | let name = &ast.ident; 24 | 25 | let view_fields = fields.iter().filter_map(|field| { 26 | (!field.ident.as_ref().unwrap().to_string().starts_with('_')).then(|| { 27 | let field_name = &field.ident; 28 | let field_string = field_name.as_ref().unwrap().to_string().to_title_case(); 29 | quote_spanned! {field.span()=> 30 | crate::gui::raw_ui::RawUi::view(&self.borrow().#field_name, #field_string) 31 | } 32 | }) 33 | }); 34 | 35 | match raw_ui_impl { 36 | Derive::RawUi => quote! { 37 | impl crate::gui::raw_ui::RawUi for crate::save_data::RcRef<#name> { 38 | fn view(&self, label: &str) -> yew::Html { 39 | self.view_opened(label, false) 40 | } 41 | 42 | fn view_opened(&self, label: &str, opened: bool) -> yew::Html { 43 | use crate::gui::components::raw_ui::RawUiStruct; 44 | let fields = [#(#view_fields),*]; 45 | yew::html! { 46 | 47 | { for fields } 48 | 49 | } 50 | } 51 | } 52 | }, 53 | Derive::RawUiRoot => quote! { 54 | impl crate::gui::raw_ui::RawUi for crate::save_data::RcRef<#name> { 55 | fn view(&self, label: &str) -> yew::Html { 56 | self.view_opened(label, false) 57 | } 58 | 59 | fn view_opened(&self, _: &str, _: bool) -> yew::Html { 60 | use crate::gui::components::Table; 61 | let fields = [#(#view_fields),*]; 62 | yew::html! { 63 | 64 | { for fields } 65 |
66 | } 67 | } 68 | } 69 | }, 70 | Derive::RawUiChildren => quote! { 71 | impl crate::gui::raw_ui::RawUiChildren for crate::save_data::RcRef<#name> { 72 | fn children(&self) -> Vec { 73 | vec![#(#view_fields),*] 74 | } 75 | } 76 | }, 77 | } 78 | } 79 | 80 | pub fn impl_enum( 81 | ast: &DeriveInput, variants: &Punctuated, 82 | ) -> proc_macro2::TokenStream { 83 | let name = &ast.ident; 84 | 85 | let from_variants = variants.iter().enumerate().map(|(i, v)| { 86 | let variant = &v.ident; 87 | quote! { 88 | #i => #name::#variant 89 | } 90 | }); 91 | 92 | let from_usize = variants.iter().enumerate().map(|(i, v)| { 93 | let variant = &v.ident; 94 | quote_spanned! {v.span()=> 95 | #name::#variant => #i 96 | } 97 | }); 98 | 99 | let array_variants = variants.iter().map(|v| { 100 | let variant_name = if name == "ItemLevel" { 101 | // ItemLevel exception 102 | v.ident.to_string() 103 | } else { 104 | v.ident.to_string().to_title_case() 105 | }; 106 | quote_spanned! {v.span()=> 107 | #variant_name 108 | } 109 | }); 110 | 111 | quote! { 112 | impl #name { 113 | pub fn variants() -> &'static [&'static str] { 114 | &[#(#array_variants),*] 115 | } 116 | } 117 | 118 | impl From for #name { 119 | fn from(idx: usize) -> Self { 120 | match idx { 121 | #(#from_variants),*, 122 | _ => unreachable!(), 123 | } 124 | } 125 | } 126 | 127 | impl From<#name> for usize { 128 | fn from(from: #name) -> Self { 129 | match from { 130 | #(#from_usize),*, 131 | } 132 | } 133 | } 134 | 135 | impl crate::gui::raw_ui::RawUi for crate::save_data::RcRef<#name> { 136 | fn view(&self, label: &str) -> yew::Html { 137 | use crate::gui::components::raw_ui::RawUiEnum; 138 | 139 | yew::html!{ 140 | label={label.to_owned()} items={#name::variants()} value={self.clone()} /> 141 | } 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/gui/mass_effect_1_le/bonus_talents.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | use crate::{ 4 | gui::components::Table, 5 | save_data::{ 6 | mass_effect_1_le::player::{ComplexTalent, SimpleTalent}, 7 | RcRef, 8 | }, 9 | }; 10 | 11 | const BONUS_TALENTS: &[(i32, &[i32], &str)] = &[ 12 | (50, &[248], "Lift"), 13 | (49, &[247], "Throw"), 14 | (56, &[249], "Warp"), 15 | (57, &[250], "Singularity"), 16 | (63, &[251], "Barrier"), 17 | (64, &[252], "Stasis"), 18 | (86, &[254], "Damping"), 19 | (91, &[256], "Hacking"), 20 | (84, &[253], "Electronics"), 21 | (93, &[255], "Decryption"), 22 | (98, &[257], "First Aid"), 23 | (99, &[257, 258], "Medicine"), 24 | (15, &[244], "Shotguns"), 25 | (7, &[245], "Assault Rifles"), 26 | (21, &[246], "Sniper Rifles"), 27 | ]; 28 | 29 | pub enum Msg { 30 | ToggleBonusTalent(usize), 31 | } 32 | 33 | #[derive(Properties, PartialEq)] 34 | pub struct Props { 35 | pub talent_list: RcRef>, 36 | pub simple_talents: RcRef>>, 37 | pub complex_talents: RcRef>>, 38 | pub helper: Option<&'static str>, 39 | pub onselect: Callback>, 40 | } 41 | 42 | pub struct BonusTalents; 43 | 44 | impl Component for BonusTalents { 45 | type Message = Msg; 46 | type Properties = Props; 47 | 48 | fn create(_ctx: &Context) -> Self { 49 | BonusTalents 50 | } 51 | 52 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 53 | match msg { 54 | Msg::ToggleBonusTalent(talent_idx) => { 55 | let Props { simple_talents, complex_talents, onselect, .. } = &ctx.props(); 56 | 57 | let (complex_id, simple_ids, _) = BONUS_TALENTS[talent_idx]; 58 | 59 | let found = complex_talents.borrow().iter().enumerate().find_map(|(i, talent)| { 60 | (talent.borrow().talent_id() == complex_id) 61 | .then(|| (i, talent.borrow().current_rank())) 62 | }); 63 | 64 | let callback = if let Some((idx, spent_points)) = found { 65 | complex_talents.borrow_mut().remove(idx); 66 | 67 | simple_talents.borrow_mut().retain(|talent| { 68 | let talent_id = talent.borrow().talent_id(); 69 | !simple_ids.contains(&talent_id) 70 | }); 71 | 72 | Some(spent_points) 73 | } else { 74 | let mut complex = ComplexTalent::default(); 75 | complex.set_talent_id(complex_id); 76 | complex.set_max_rank(12); 77 | complex.set_level_offset(-1); 78 | complex.set_levels_per_rank(1); 79 | complex.set_visual_order(85); 80 | 81 | complex_talents.borrow_mut().push(complex.into()); 82 | 83 | for &simple_id in simple_ids { 84 | let mut simple = SimpleTalent::default(); 85 | simple.set_talent_id(simple_id); 86 | simple.set_current_rank(1); 87 | 88 | simple_talents.borrow_mut().push(simple.into()); 89 | } 90 | None 91 | }; 92 | 93 | onselect.emit(callback); 94 | false 95 | } 96 | } 97 | } 98 | 99 | fn view(&self, ctx: &Context) -> Html { 100 | let Props { talent_list, complex_talents, helper, .. } = &ctx.props(); 101 | 102 | let selectables = 103 | BONUS_TALENTS.iter().enumerate().filter_map(|(i, &(complex_id, _, talent_label))| { 104 | talent_list.borrow().iter().any(|&filter| filter == complex_id).then(|| { 105 | let selected = complex_talents 106 | .borrow() 107 | .iter() 108 | .any(|talent| talent.borrow().talent_id() == complex_id); 109 | 110 | html! { 111 | 125 | } 126 | }) 127 | }); 128 | 129 | html! { 130 | 131 | { for selectables } 132 |
133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/gui/shared/bonus_powers.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | use crate::{ 4 | gui::components::Table, 5 | save_data::{ 6 | mass_effect_2::player::Power as Me2Power, mass_effect_3::player::Power as Me3Power, RcRef, 7 | }, 8 | }; 9 | 10 | #[derive(Clone)] 11 | pub enum BonusPowerType { 12 | Me2(RcRef>>), 13 | Me3(RcRef>>), 14 | } 15 | 16 | impl PartialEq for BonusPowerType { 17 | fn eq(&self, other: &BonusPowerType) -> bool { 18 | match (self, other) { 19 | (BonusPowerType::Me2(me2_powers), BonusPowerType::Me2(other)) => me2_powers == other, 20 | (BonusPowerType::Me3(me3_powers), BonusPowerType::Me3(other)) => me3_powers == other, 21 | _ => false, 22 | } 23 | } 24 | } 25 | 26 | pub enum Msg { 27 | ToggleBonusPower(String, String), 28 | } 29 | 30 | #[derive(Properties, PartialEq)] 31 | pub struct Props { 32 | pub power_list: &'static [(&'static str, &'static str, &'static str)], 33 | pub powers: BonusPowerType, 34 | pub helper: Option<&'static str>, 35 | } 36 | 37 | pub struct BonusPowers; 38 | 39 | impl Component for BonusPowers { 40 | type Message = Msg; 41 | type Properties = Props; 42 | 43 | fn create(_ctx: &Context) -> Self { 44 | BonusPowers 45 | } 46 | 47 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 48 | match msg { 49 | Msg::ToggleBonusPower(power_name, power_class_name) => { 50 | match ctx.props().powers { 51 | BonusPowerType::Me2(ref powers) => { 52 | let idx = powers.borrow().iter().enumerate().find_map(|(i, power)| { 53 | power 54 | .borrow() 55 | .power_class_name() 56 | .eq_ignore_ascii_case(&power_class_name) 57 | .then(|| i) 58 | }); 59 | 60 | if let Some(idx) = idx { 61 | powers.borrow_mut().remove(idx); 62 | } else { 63 | let power = Me2Power::default(); 64 | *power.name.borrow_mut() = power_name; 65 | *power.power_class_name.borrow_mut() = power_class_name; 66 | powers.borrow_mut().push(power.into()); 67 | } 68 | } 69 | BonusPowerType::Me3(ref powers) => { 70 | let idx = powers.borrow().iter().enumerate().find_map(|(i, power)| { 71 | power 72 | .borrow() 73 | .power_class_name() 74 | .eq_ignore_ascii_case(&power_class_name) 75 | .then(|| i) 76 | }); 77 | 78 | if let Some(idx) = idx { 79 | powers.borrow_mut().remove(idx); 80 | } else { 81 | let power = Me3Power::default(); 82 | *power.name.borrow_mut() = power_name; 83 | *power.power_class_name.borrow_mut() = power_class_name; 84 | powers.borrow_mut().push(power.into()); 85 | } 86 | } 87 | } 88 | 89 | true 90 | } 91 | } 92 | } 93 | 94 | fn view(&self, ctx: &Context) -> Html { 95 | let Props { power_list, powers, helper } = &ctx.props(); 96 | 97 | let selectables = power_list.iter().map(|&(power_name, power_class_name, power_label)| { 98 | let selected = match powers { 99 | BonusPowerType::Me2(powers) => powers.borrow() 100 | .iter() 101 | .any(|power| power.borrow().power_class_name().eq_ignore_ascii_case(power_class_name)), 102 | BonusPowerType::Me3(powers) => powers.borrow() 103 | .iter() 104 | .any(|power| power.borrow().power_class_name().eq_ignore_ascii_case(power_class_name)), 105 | }; 106 | 107 | html! { 108 | 122 | } 123 | }); 124 | 125 | html! { 126 | 127 | { for selectables } 128 |
129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /InnoSetup.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define AppName "Trilogy Save Editor" 5 | #define AppVersion "2.2.1" 6 | #define AppPublisher "Karlitos" 7 | #define AppURL "https://github.com/KarlitosVII/trilogy-save-editor" 8 | #define AppExeName "trilogy-save-editor.exe" 9 | 10 | [Setup] 11 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 12 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 13 | AppId={{6A0B979E-271B-4E50-A4C3-487C8E584070} 14 | AppName={#AppName} 15 | AppVersion={#AppVersion} 16 | ;AppVerName={#AppName} {#AppVersion} 17 | AppPublisher={#AppPublisher} 18 | AppPublisherURL={#AppURL} 19 | AppSupportURL={#AppURL} 20 | AppUpdatesURL={#AppURL} 21 | ;DefaultDirName={reg:HKLM\SOFTWARE\BioWare\Mass Effect Legendary Edition,Install Dir|{autopf}}\{#AppName} 22 | DefaultDirName={localappdata}\{#AppName} 23 | DefaultGroupName={#AppName} 24 | AllowNoIcons=yes 25 | ;LicenseFile=LICENSE.txt 26 | ;InfoBeforeFile=before.txt 27 | ;InfoAfterFile=after.txt 28 | ; Remove the following line to run in administrative install mode (install for all users.) 29 | PrivilegesRequired=lowest 30 | OutputDir=target 31 | OutputBaseFilename=trilogy-save-editor_{#AppVersion}_setup 32 | Compression=lzma2/max 33 | SolidCompression=yes 34 | WizardStyle=modern 35 | ; "ArchitecturesAllowed=x64" specifies that Setup cannot run on 36 | ; anything but x64. 37 | ArchitecturesAllowed=x64 38 | ; "ArchitecturesInstallIn64BitMode=x64" requests that the install be 39 | ; done in "64-bit mode" on x64, meaning it should use the native 40 | ; 64-bit Program Files directory and the 64-bit view of the registry. 41 | ArchitecturesInstallIn64BitMode=x64 42 | UsePreviousAppDir=yes 43 | RestartIfNeededByRun=no 44 | 45 | [Languages] 46 | Name: "english"; MessagesFile: "compiler:Default.isl" 47 | Name: "french"; MessagesFile: "compiler:Languages\French.isl" 48 | Name: "german"; MessagesFile: "compiler:Languages\German.isl" 49 | Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl" 50 | Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl" 51 | Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl" 52 | Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" 53 | Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl" 54 | 55 | [Tasks] 56 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 57 | 58 | [Files] 59 | Source: "target\release\{#AppExeName}"; DestDir: "{app}"; Flags: ignoreversion 60 | Source: "databases\*"; DestDir: "{app}\databases"; Flags: ignoreversion 61 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 62 | 63 | [UninstallDelete] 64 | Type: filesandordirs; Name: "{app}\{#AppExeName}.WebView2" 65 | 66 | [Icons] 67 | Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}" 68 | Name: "{group}\{cm:ProgramOnTheWeb,{#AppName}}"; Filename: "{#AppURL}" 69 | Name: "{group}\{cm:UninstallProgram,{#AppName}}"; Filename: "{uninstallexe}" 70 | Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon 71 | 72 | [Run] 73 | Filename: "{tmp}\MicrosoftEdgeWebview2Setup.exe"; Parameters: "/install"; Flags: skipifdoesntexist 74 | Filename: "{app}\{#AppExeName}"; Flags: nowait postinstall skipifnotsilent 75 | Filename: "{app}\{#AppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 76 | 77 | [Code] 78 | var 79 | DownloadPage: TDownloadWizardPage; 80 | 81 | function OnDownloadProgress(const Url, FileName: String; const Progress, ProgressMax: Int64): Boolean; 82 | begin 83 | if Progress = ProgressMax then 84 | Log(Format('Successfully downloaded file to {tmp}: %s', [FileName])); 85 | Result := True; 86 | end; 87 | 88 | procedure InitializeWizard; 89 | begin 90 | DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadProgress); 91 | end; 92 | 93 | function NextButtonClick(CurPageID: Integer): Boolean; 94 | var Dummy: String; 95 | begin 96 | if CurPageID = wpReady then 97 | begin 98 | if not RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'pv', Dummy) then 99 | begin 100 | DownloadPage.Clear; 101 | DownloadPage.Add('https://go.microsoft.com/fwlink/p/?LinkId=2124703', 'MicrosoftEdgeWebview2Setup.exe', ''); 102 | DownloadPage.Show; 103 | try 104 | try 105 | DownloadPage.Download; // This downloads the files to {tmp} 106 | Result := True; 107 | except 108 | if DownloadPage.AbortedByUser then 109 | Log('Aborted by user.') 110 | else 111 | SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK); 112 | Result := False; 113 | end; 114 | finally 115 | DownloadPage.Hide; 116 | end; 117 | end else 118 | Result := True; 119 | end else 120 | Result := True; 121 | end; -------------------------------------------------------------------------------- /src/gui/components/table.rs: -------------------------------------------------------------------------------- 1 | use gloo::timers::future::TimeoutFuture; 2 | use yew::prelude::*; 3 | 4 | use crate::gui::components::Helper; 5 | 6 | pub enum Msg { 7 | Toggle, 8 | } 9 | 10 | #[derive(Properties, PartialEq)] 11 | pub struct Props { 12 | pub title: Option, 13 | pub children: Children, 14 | #[prop_or(true)] 15 | pub opened: bool, 16 | pub helper: Option<&'static str>, 17 | } 18 | 19 | pub struct Table { 20 | opened: bool, 21 | } 22 | 23 | impl Component for Table { 24 | type Message = Msg; 25 | type Properties = Props; 26 | 27 | fn create(ctx: &Context) -> Self { 28 | Table { opened: ctx.props().opened } 29 | } 30 | 31 | fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { 32 | match msg { 33 | Msg::Toggle => { 34 | self.opened = !self.opened; 35 | true 36 | } 37 | } 38 | } 39 | 40 | fn changed(&mut self, ctx: &Context) -> bool { 41 | self.opened = ctx.props().opened; 42 | true 43 | } 44 | 45 | fn view(&self, ctx: &Context) -> Html { 46 | let Props { title, children, helper, .. } = &ctx.props(); 47 | let opened = title.is_none() || self.opened; 48 | 49 | let title = title.as_ref().map(|title| { 50 | let chevron = if opened { "table-chevron-down" } else { "table-chevron-right" }; 51 | let helper = helper.as_ref().map(|&helper| { 52 | html! { 53 | 54 | } 55 | }); 56 | 57 | html! { 58 |
59 | 78 |
79 | } 80 | }); 81 | 82 | let chunks = opened.then(|| { 83 | let mut rows = children.iter().map(|child| { 84 | html! { 85 |
89 | {child} 90 |
91 | } 92 | }); 93 | 94 | const CHUNK_SIZE: usize = 40; 95 | let chunks = (0..children.len() / CHUNK_SIZE + 1).map(|i| { 96 | html! { 97 | 98 | { for rows.by_ref().take(CHUNK_SIZE) } 99 | 100 | } 101 | }); 102 | 103 | html! { for chunks } 104 | }); 105 | 106 | html! { 107 |
108 | { for title } 109 | { for chunks } 110 |
111 | } 112 | } 113 | } 114 | 115 | enum ChunkMsg { 116 | Render, 117 | } 118 | 119 | #[derive(Properties, PartialEq)] 120 | struct ChunkProps { 121 | children: Children, 122 | position: usize, 123 | } 124 | 125 | struct RowChunk { 126 | should_render: bool, 127 | } 128 | 129 | impl Component for RowChunk { 130 | type Message = ChunkMsg; 131 | type Properties = ChunkProps; 132 | 133 | fn create(ctx: &Context) -> Self { 134 | let should_render = ctx.props().position == 0; 135 | if !should_render { 136 | let position = ctx.props().position as u32; 137 | ctx.link().send_future(async move { 138 | TimeoutFuture::new(17 * position).await; 139 | ChunkMsg::Render 140 | }); 141 | } 142 | RowChunk { should_render } 143 | } 144 | 145 | fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { 146 | match msg { 147 | ChunkMsg::Render => { 148 | self.should_render = true; 149 | true 150 | } 151 | } 152 | } 153 | 154 | fn changed(&mut self, ctx: &Context) -> bool { 155 | if ctx.props().position == 0 { 156 | true 157 | } else { 158 | ctx.link().send_future(async { 159 | TimeoutFuture::new(0).await; 160 | ChunkMsg::Render 161 | }); 162 | false 163 | } 164 | } 165 | 166 | fn view(&self, ctx: &Context) -> Html { 167 | let content = self.should_render.then(|| ctx.props().children.iter().collect::()); 168 | html! { for content } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/gui/components/auto_update.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Error}; 2 | use gloo::{ 3 | events::EventListener, 4 | storage::{LocalStorage, Storage}, 5 | utils, 6 | }; 7 | use js_sys::Date; 8 | use serde::Deserialize; 9 | use wasm_bindgen::JsCast; 10 | use wasm_bindgen_futures as futures; 11 | use web_sys::CustomEvent; 12 | use yew::prelude::*; 13 | 14 | use crate::services::rpc; 15 | 16 | enum UpdateState { 17 | None, 18 | UpdateAvailable, 19 | DownloadProgress(f64), 20 | } 21 | 22 | pub enum Msg { 23 | UpdateAvailable, 24 | InstallUpdate, 25 | DownloadProgress(f64), 26 | Error(Error), 27 | } 28 | 29 | #[derive(Properties, PartialEq)] 30 | pub struct Props { 31 | pub onerror: Callback, 32 | } 33 | 34 | pub struct AutoUpdate { 35 | _update_listener: EventListener, 36 | _progress_listener: EventListener, 37 | _error_listener: EventListener, 38 | update_state: UpdateState, 39 | } 40 | 41 | impl Component for AutoUpdate { 42 | type Message = Msg; 43 | type Properties = Props; 44 | 45 | fn create(ctx: &Context) -> Self { 46 | let update_listener = { 47 | let link = ctx.link().clone(); 48 | EventListener::new(&utils::document(), "tse_update_available", move |_| { 49 | link.send_message(Msg::UpdateAvailable); 50 | }) 51 | }; 52 | 53 | let progress_listener = { 54 | let link = ctx.link().clone(); 55 | EventListener::new(&utils::document(), "tse_update_progress", move |event| { 56 | if let Some(event) = event.dyn_ref::() { 57 | #[derive(Deserialize)] 58 | struct Progress { 59 | progress: f64, 60 | } 61 | 62 | let Progress { progress } = serde_wasm_bindgen::from_value(event.detail()) 63 | .expect("Failed to parse Progress"); 64 | link.send_message(Msg::DownloadProgress(progress)); 65 | } 66 | }) 67 | }; 68 | 69 | let error_listener = { 70 | let link = ctx.link().clone(); 71 | EventListener::new(&utils::document(), "tse_update_error", move |event| { 72 | if let Some(event) = event.dyn_ref::() { 73 | #[derive(Deserialize)] 74 | struct Error { 75 | error: String, 76 | } 77 | 78 | let Error { error } = serde_wasm_bindgen::from_value(event.detail()) 79 | .expect("Failed to parse Error"); 80 | let error = anyhow!(error).context("Auto update error"); 81 | link.send_message(Msg::Error(error)); 82 | } 83 | }) 84 | }; 85 | 86 | AutoUpdate::check_for_update(); 87 | 88 | AutoUpdate { 89 | _update_listener: update_listener, 90 | _progress_listener: progress_listener, 91 | _error_listener: error_listener, 92 | update_state: UpdateState::None, 93 | } 94 | } 95 | 96 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 97 | match msg { 98 | Msg::UpdateAvailable => { 99 | self.update_state = UpdateState::UpdateAvailable; 100 | true 101 | } 102 | Msg::InstallUpdate => { 103 | futures::spawn_local(async { 104 | LocalStorage::delete("last_update_check"); 105 | let _ = rpc::download_and_install_update().await; 106 | }); 107 | false 108 | } 109 | Msg::DownloadProgress(progress) => { 110 | self.update_state = UpdateState::DownloadProgress(progress); 111 | true 112 | } 113 | Msg::Error(err) => { 114 | self.update_state = UpdateState::None; 115 | ctx.props().onerror.emit(err); 116 | true 117 | } 118 | } 119 | } 120 | 121 | fn view(&self, ctx: &Context) -> Html { 122 | match self.update_state { 123 | UpdateState::UpdateAvailable => html! { 124 |
125 |
{"A new update is available"}
126 | 131 |
132 | }, 133 | UpdateState::DownloadProgress(progress) => html! { 134 |
{ format!("Downloading update: {}%", (progress * 100.0) as usize) }
135 | }, 136 | UpdateState::None => Default::default(), 137 | } 138 | } 139 | } 140 | 141 | impl AutoUpdate { 142 | fn check_for_update() { 143 | let should_check = LocalStorage::get("last_update_check") 144 | .map(|date: f64| (Date::now() - date) > 86_400_000.0) // 24h 145 | .unwrap_or(true); 146 | 147 | if should_check { 148 | futures::spawn_local(async { 149 | let _ = rpc::check_for_update().await; 150 | let _ = LocalStorage::set("last_update_check", Date::now()); 151 | }); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/gui/components/select.rs: -------------------------------------------------------------------------------- 1 | use gloo::utils; 2 | use web_sys::HtmlElement; 3 | use yew::prelude::*; 4 | 5 | pub enum Msg { 6 | Open, 7 | Close, 8 | Blur, 9 | Select(usize), 10 | } 11 | 12 | #[derive(Properties, PartialEq)] 13 | pub struct Props { 14 | pub options: &'static [&'static str], 15 | pub current_idx: usize, 16 | pub onselect: Callback, 17 | #[prop_or(true)] 18 | pub sized: bool, 19 | } 20 | 21 | pub struct Select { 22 | select_ref: NodeRef, 23 | drop_down_ref: NodeRef, 24 | current_idx: usize, 25 | opened: bool, 26 | } 27 | 28 | impl Component for Select { 29 | type Message = Msg; 30 | type Properties = Props; 31 | 32 | fn create(ctx: &Context) -> Self { 33 | Select { 34 | select_ref: Default::default(), 35 | drop_down_ref: Default::default(), 36 | current_idx: ctx.props().current_idx, 37 | opened: false, 38 | } 39 | } 40 | 41 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 42 | match msg { 43 | Msg::Open => { 44 | self.opened = true; 45 | true 46 | } 47 | Msg::Close => { 48 | self.opened = false; 49 | true 50 | } 51 | Msg::Blur => { 52 | if let Some(select) = self.select_ref.cast::() { 53 | let _ = select.blur(); 54 | } 55 | false 56 | } 57 | Msg::Select(idx) => { 58 | self.current_idx = idx; 59 | ctx.props().onselect.emit(idx); 60 | ctx.link().send_message(Msg::Blur); 61 | false 62 | } 63 | } 64 | } 65 | 66 | fn changed(&mut self, ctx: &Context) -> bool { 67 | self.current_idx = ctx.props().current_idx; 68 | if self.opened { 69 | ctx.link().send_message(Msg::Blur); 70 | false 71 | } else { 72 | true 73 | } 74 | } 75 | 76 | fn rendered(&mut self, _ctx: &Context, _first_render: bool) { 77 | // Drop down open upward if bottom > viewport_height 78 | if let Some(drop_down) = self.drop_down_ref.cast::() { 79 | let viewport_height = utils::document().document_element().unwrap().client_height(); 80 | let rect = drop_down.get_bounding_client_rect(); 81 | let top = rect.top() as i32; 82 | let bottom = rect.bottom() as i32; 83 | let height = bottom - top; 84 | 85 | if height < top - 70 && bottom > viewport_height - 10 { 86 | if let Some(select) = self.select_ref.cast::() { 87 | let height = select.offset_height(); 88 | let _ = drop_down.style().set_property("bottom", &format!("{}px", height)); 89 | } 90 | } else { 91 | let _ = drop_down.style().set_property("bottom", "auto"); 92 | } 93 | } 94 | } 95 | 96 | fn view(&self, ctx: &Context) -> Html { 97 | let drop_down = self.opened.then(|| { 98 | let options = ctx.props().options.iter().enumerate().map(|(idx, option)| { 99 | let selected = idx == self.current_idx; 100 | html! { 101 | 113 | { option } 114 | 115 | } 116 | }); 117 | html! { for options } 118 | }); 119 | 120 | let size = if ctx.props().sized { "w-[200px]" } else { "min-w-[60px]" }; 121 | 122 | let onclick = if !self.opened { 123 | ctx.link().callback(|_| Msg::Open) 124 | } else { 125 | ctx.link().callback(|_| Msg::Blur) 126 | }; 127 | 128 | html! { 129 |
138 | 150 | { ctx.props().options[self.current_idx] } 151 | 152 |
167 | { for drop_down } 168 |
169 |
170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/gui/components/tab_bar.rs: -------------------------------------------------------------------------------- 1 | use gloo::{events::EventListener, utils}; 2 | use wasm_bindgen::JsCast; 3 | use web_sys::PopStateEvent; 4 | use yew::{html::Scope, prelude::*}; 5 | 6 | use crate::gui::Theme; 7 | 8 | const MAIN_BUTTON: i16 = 0; 9 | 10 | pub enum Msg { 11 | TabClicked(MouseEvent, String), 12 | MainTabChanged(String), 13 | } 14 | 15 | #[derive(Properties, PartialEq)] 16 | pub struct Props { 17 | pub children: ChildrenWithProps, 18 | #[prop_or(false)] 19 | pub is_main_tab_bar: bool, 20 | } 21 | 22 | pub struct TabBar { 23 | main_tab_listener: Option, 24 | current_tab: String, 25 | } 26 | 27 | impl Component for TabBar { 28 | type Message = Msg; 29 | type Properties = Props; 30 | 31 | fn create(ctx: &Context) -> Self { 32 | let current_tab = Self::first_tab(&ctx.props().children); 33 | let main_tab_listener = ctx.props().is_main_tab_bar.then(|| { 34 | let link = ctx.link().clone(); 35 | Self::event_listener(link) 36 | }); 37 | 38 | // TODO: Tab history 39 | 40 | TabBar { current_tab, main_tab_listener } 41 | } 42 | 43 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 44 | match msg { 45 | Msg::TabClicked(event, title) => { 46 | if event.button() == MAIN_BUTTON { 47 | self.current_tab = title; 48 | true 49 | } else { 50 | false 51 | } 52 | } 53 | Msg::MainTabChanged(main_tab) => { 54 | let children = &ctx.props().children; 55 | if children.iter().any(|child| child.props.title == main_tab) { 56 | self.current_tab = main_tab; 57 | } else { 58 | self.current_tab = Self::first_tab(children); 59 | } 60 | true 61 | } 62 | } 63 | } 64 | 65 | fn changed(&mut self, ctx: &Context) -> bool { 66 | let is_main_tab_bar = ctx.props().is_main_tab_bar; 67 | let is_event_listening = self.main_tab_listener.is_some(); 68 | 69 | if !is_main_tab_bar && is_event_listening { 70 | self.main_tab_listener = None; 71 | } else if is_main_tab_bar && !is_event_listening { 72 | let link = ctx.link().clone(); 73 | self.main_tab_listener = Some(Self::event_listener(link)); 74 | } 75 | 76 | // Go to first tab if current tab doesn't exist 77 | let children = &ctx.props().children; 78 | if !children.iter().any(|child| child.props.title == self.current_tab) { 79 | self.current_tab = Self::first_tab(children); 80 | } 81 | true 82 | } 83 | 84 | fn view(&self, ctx: &Context) -> Html { 85 | let tabs = ctx.props().children.iter().map(|child| { 86 | let title = child.props.title.clone(); 87 | let onmousedown = (title != self.current_tab).then(|| { 88 | let title = title.clone(); 89 | ctx.link().callback(move |event| Msg::TabClicked(event, title.clone())) 90 | }); 91 | html! { 92 | 106 | { title } 107 | 108 | } 109 | }); 110 | 111 | let content = ctx.props().children.iter().find_map(|content| { 112 | (content.props.title == self.current_tab).then(|| { 113 | html! { 114 |
122 | { content } 123 |
124 | } 125 | }) 126 | }); 127 | 128 | html! { 129 |
130 |
131 | { for tabs } 132 |
133 | { for content } 134 |
135 | } 136 | } 137 | } 138 | 139 | impl TabBar { 140 | fn event_listener(link: Scope) -> EventListener { 141 | EventListener::new(&utils::window(), "popstate", { 142 | move |event| { 143 | if let Some(event) = event.dyn_ref::() { 144 | let main_tab: String = 145 | serde_wasm_bindgen::from_value(event.state()).unwrap_or_default(); 146 | link.send_message(Msg::MainTabChanged(main_tab)); 147 | } 148 | } 149 | }) 150 | } 151 | 152 | fn first_tab(children: &ChildrenWithProps) -> String { 153 | children.iter().next().map(|child| child.props.title.clone()).unwrap_or_default() 154 | } 155 | } 156 | 157 | #[derive(Properties, PartialEq)] 158 | pub struct TabProps { 159 | pub title: String, 160 | #[prop_or_default] 161 | pub children: Children, 162 | pub theme: Option, 163 | } 164 | 165 | pub struct Tab; 166 | 167 | impl Component for Tab { 168 | type Message = (); 169 | type Properties = TabProps; 170 | 171 | fn create(_ctx: &Context) -> Self { 172 | Tab 173 | } 174 | 175 | fn view(&self, ctx: &Context) -> Html { 176 | ctx.props().children.iter().collect::() 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/gui/components/raw_ui/raw_ui_vec.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::Any, 3 | cell::{Ref, RefMut}, 4 | fmt::Display, 5 | marker::PhantomData, 6 | }; 7 | 8 | use yew::prelude::*; 9 | 10 | use crate::{ 11 | gui::{components::Table, raw_ui::RawUi}, 12 | save_data::RcRef, 13 | }; 14 | 15 | pub enum Msg { 16 | Toggle, 17 | Add, 18 | Remove(usize), 19 | } 20 | 21 | #[derive(Properties, PartialEq)] 22 | pub struct Props 23 | where 24 | T: RawUi + Default + Display, 25 | { 26 | pub label: String, 27 | pub vec: RcRef>, 28 | #[prop_or(true)] 29 | pub is_editable: bool, 30 | } 31 | 32 | impl Props 33 | where 34 | T: RawUi + Default + Display, 35 | { 36 | fn vec(&self) -> Ref<'_, Vec> { 37 | self.vec.borrow() 38 | } 39 | 40 | fn vec_mut(&self) -> RefMut<'_, Vec> { 41 | self.vec.borrow_mut() 42 | } 43 | } 44 | 45 | pub struct RawUiVec 46 | where 47 | T: RawUi + Default + Display, 48 | { 49 | _marker: PhantomData, 50 | opened: bool, 51 | new_item_idx: usize, 52 | } 53 | 54 | impl Component for RawUiVec 55 | where 56 | T: RawUi + Default + Display, 57 | { 58 | type Message = Msg; 59 | type Properties = Props; 60 | 61 | fn create(_ctx: &Context) -> Self { 62 | RawUiVec { _marker: PhantomData, opened: false, new_item_idx: 0 } 63 | } 64 | 65 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 66 | match msg { 67 | Msg::Toggle => { 68 | self.opened = !self.opened; 69 | if self.opened { 70 | // Prevent last item to reopen 71 | self.new_item_idx = ctx.props().vec().len(); 72 | } 73 | true 74 | } 75 | Msg::Add => { 76 | // Open added item 77 | self.new_item_idx = ctx.props().vec().len(); 78 | 79 | ctx.props().vec_mut().push(Default::default()); 80 | true 81 | } 82 | Msg::Remove(idx) => { 83 | ctx.props().vec_mut().remove(idx); 84 | true 85 | } 86 | } 87 | } 88 | 89 | fn view(&self, ctx: &Context) -> Html { 90 | let chevron = if self.opened { "table-chevron-down" } else { "table-chevron-right" }; 91 | 92 | let content = self 93 | .opened 94 | .then(|| { 95 | let vec = ctx.props().vec(); 96 | let is_editable = ctx.props().is_editable; 97 | 98 | // Exceptions 99 | macro_rules! display_idx { 100 | ($vec:ident => $($type:ty)*) => { 101 | $((&*$vec as &dyn Any).is::>>()) ||* 102 | } 103 | } 104 | let display_idx = display_idx!(vec => u8 i32 f32 bool String); 105 | 106 | let items = vec.iter().enumerate().map(|(idx, item)| { 107 | let label = item.to_string(); 108 | let opened = self.new_item_idx == idx; 109 | let item = if display_idx || label.is_empty() { 110 | item.view_opened(&idx.to_string(), opened) 111 | } else { 112 | item.view_opened(&label, opened) 113 | }; 114 | 115 | let remove = is_editable.then(|| html!{ 116 | 132 | }); 133 | 134 | html! { 135 |
136 | { for remove } 137 | { item } 138 |
139 | } 140 | }); 141 | 142 | let empty = (!is_editable && vec.is_empty()).then(|| html!{ "" }).into_iter(); 143 | 144 | let add = is_editable.then(|| html!{ 145 | 150 | }).into_iter(); 151 | 152 | html! { 153 |
154 | 155 | { for items } 156 | { for empty } 157 | { for add } 158 |
159 |
160 | } 161 | }); 162 | 163 | html! { 164 |
165 |
166 | 180 |
181 | { for content } 182 |
183 | } 184 | } 185 | } 186 | --------------------------------------------------------------------------------