diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | Cargo.toml | 17 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | src/assets/colorlight.png | bin | 0 -> 6324 bytes | |||
| -rw-r--r-- | src/assets/dx8.png | bin | 0 -> 13162 bytes | |||
| -rw-r--r-- | src/assets/dx9.png | bin | 0 -> 1610 bytes | |||
| -rw-r--r-- | src/assets/portal_projectile.pcf | bin | 0 -> 102749 bytes | |||
| -rw-r--r-- | src/assets/portalgun.pcf | bin | 0 -> 23835 bytes | |||
| -rw-r--r-- | src/assets/portals.pcf | bin | 0 -> 35908 bytes | |||
| -rw-r--r-- | src/assets/strider_bluebeam.png | bin | 0 -> 3104 bytes | |||
| -rw-r--r-- | src/assets/v_portalgun.png | bin | 0 -> 291006 bytes | |||
| -rw-r--r-- | src/assets/w_portalgun.png | bin | 0 -> 84016 bytes | |||
| -rw-r--r-- | src/gui.rs | 147 | ||||
| -rw-r--r-- | src/main.rs | 355 |
14 files changed, 524 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3611fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/.idea +Cargo.lock
\ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9cf9c03 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "portal-tools" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[profile.release] +codegen-units = 1 +opt-level = "z" + +[dependencies] +vtf = "0.1.4" +image = "0.23.14" +native-windows-gui = "1.0.12" +native-windows-derive = "1.0.4" +hex = "0.4.3" +imageproc = "*"
\ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7bfc527 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +## portal-tools +it changes colors in portal diff --git a/src/assets/colorlight.png b/src/assets/colorlight.png Binary files differnew file mode 100644 index 0000000..4b5615b --- /dev/null +++ b/src/assets/colorlight.png diff --git a/src/assets/dx8.png b/src/assets/dx8.png Binary files differnew file mode 100644 index 0000000..f2938cd --- /dev/null +++ b/src/assets/dx8.png diff --git a/src/assets/dx9.png b/src/assets/dx9.png Binary files differnew file mode 100644 index 0000000..1aa1f0f --- /dev/null +++ b/src/assets/dx9.png diff --git a/src/assets/portal_projectile.pcf b/src/assets/portal_projectile.pcf Binary files differnew file mode 100644 index 0000000..5bb777b --- /dev/null +++ b/src/assets/portal_projectile.pcf diff --git a/src/assets/portalgun.pcf b/src/assets/portalgun.pcf Binary files differnew file mode 100644 index 0000000..e3e3d91 --- /dev/null +++ b/src/assets/portalgun.pcf diff --git a/src/assets/portals.pcf b/src/assets/portals.pcf Binary files differnew file mode 100644 index 0000000..4577584 --- /dev/null +++ b/src/assets/portals.pcf diff --git a/src/assets/strider_bluebeam.png b/src/assets/strider_bluebeam.png Binary files differnew file mode 100644 index 0000000..2b0899e --- /dev/null +++ b/src/assets/strider_bluebeam.png diff --git a/src/assets/v_portalgun.png b/src/assets/v_portalgun.png Binary files differnew file mode 100644 index 0000000..9a8452a --- /dev/null +++ b/src/assets/v_portalgun.png diff --git a/src/assets/w_portalgun.png b/src/assets/w_portalgun.png Binary files differnew file mode 100644 index 0000000..1f63f81 --- /dev/null +++ b/src/assets/w_portalgun.png diff --git a/src/gui.rs b/src/gui.rs new file mode 100644 index 0000000..a3b80e4 --- /dev/null +++ b/src/gui.rs @@ -0,0 +1,147 @@ +use native_windows_derive::NwgUi; +use native_windows_gui::*; + +#[derive(NwgUi, Default)] +pub struct PortalTools { + // layout and window + #[nwg_control(flags: "WINDOW|VISIBLE", size: (420, 200), title: "Portal Tools")] + pub window: Window, + + #[nwg_layout(parent: window, spacing: 2)] + layout: GridLayout, + + // blue color + #[nwg_control(text: "Blue")] + #[nwg_layout_item(layout: layout, row: 0, col: 0)] + _blue_label: Label, + + #[nwg_control(text: "40a0ff")] + #[nwg_layout_item(layout: layout, row: 0, col: 1, col_span: 3)] + pub blue_box: TextInput, + + #[nwg_layout_item(layout: layout, row: 0, col: 4)] + #[nwg_control(text: "Pick")] + #[nwg_events(OnButtonClick: [PortalTools::pick_blue])] + _blue_button: Button, + + // orange color + #[nwg_control(text: "Orange")] + #[nwg_layout_item(layout: layout, row: 1, col: 0)] + _orange_label: Label, + + #[nwg_control(parent: window, text: "ffa020")] + #[nwg_layout_item(layout: layout, row: 1, col: 1, col_span: 3)] + pub orange_box: TextInput, + + #[nwg_layout_item(layout: layout, row: 1, col: 4)] + #[nwg_control(text: "Pick")] + #[nwg_events(OnButtonClick: [PortalTools::pick_orange])] + _orange_button: Button, + + // prop carry color + #[nwg_control(text: "Carry")] + #[nwg_layout_item(layout: layout, row: 2, col: 0)] + _carry_label: Label, + + #[nwg_control(parent: window, text: "f2caa7")] + #[nwg_layout_item(layout: layout, row: 2, col: 1, col_span: 3)] + pub carry_box: TextInput, + + #[nwg_layout_item(layout: layout, row: 2, col: 4)] + #[nwg_control(text: "Pick")] + #[nwg_events(OnButtonClick: [PortalTools::pick_carry])] + _carry_button: Button, + + // gun color + #[nwg_control(text: "Portal Gun")] + #[nwg_layout_item(layout: layout, row: 3, col: 0)] + _gun_label: Label, + + #[nwg_control(parent: window, text: "ffffff")] + #[nwg_layout_item(layout: layout, row: 3, col: 1, col_span: 3)] + pub gun_box: TextInput, + + #[nwg_layout_item(layout: layout, row: 3, col: 4)] + #[nwg_control(text: "Pick")] + #[nwg_events(OnButtonClick: [PortalTools::pick_gun])] + _gun_button: Button, + + // game dir + #[nwg_control(text: "Game")] + #[nwg_layout_item(layout: layout, row: 4, col: 0)] + _game_label: Label, + + #[nwg_control(parent: window, text: "")] + #[nwg_layout_item(layout: layout, row: 4, col: 1, col_span: 3)] + pub game_box: TextInput, + + #[nwg_layout_item(layout: layout, row: 4, col: 4)] + #[nwg_control(text: "Browse")] + #[nwg_events(OnButtonClick: [PortalTools::pick_game])] + _game_button: Button, + + // options and apply button + #[nwg_layout_item(layout: layout, row: 5, col: 1)] + #[nwg_control(text: "Crosshair")] + pub crosshair_check: CheckBox, + + #[nwg_layout_item(layout: layout, row: 5, col: 2)] + #[nwg_control(text: "Portals")] + pub portals_check: CheckBox, + + #[nwg_layout_item(layout: layout, row: 5, col: 3)] + #[nwg_control(text: "Particles")] + pub particles_check: CheckBox, + + #[nwg_layout_item(layout: layout, row: 5, col: 4)] + #[nwg_control(text: "Portal Gun")] + pub gun_check: CheckBox, + + #[nwg_layout_item(layout: layout, row: 5, col: 0)] + #[nwg_control(text: "Apply")] + #[nwg_events(OnButtonClick: [PortalTools::apply])] + apply_button: Button, + + #[nwg_resource] + picker: ColorDialog, + + #[nwg_resource(action: FileDialogAction::OpenDirectory, multiselect: false)] + browser: FileDialog, +} + +impl PortalTools { + fn pick_blue(&self) { + if self.picker.run(Some(&self.window)) { + let c = self.picker.color(); + self.blue_box + .set_text(&format!("{:02x}{:02x}{:02x}", c[0], c[1], c[2])); + } + } + fn pick_orange(&self) { + if self.picker.run(Some(&self.window)) { + let c = self.picker.color(); + self.orange_box + .set_text(&format!("{:02x}{:02x}{:02x}", c[0], c[1], c[2])); + } + } + fn pick_carry(&self) { + if self.picker.run(Some(&self.window)) { + let c = self.picker.color(); + self.carry_box + .set_text(&format!("{:02x}{:02x}{:02x}", c[0], c[1], c[2])); + } + } + fn pick_gun(&self) { + if self.picker.run(Some(&self.window)) { + let c = self.picker.color(); + self.gun_box + .set_text(&format!("{:02x}{:02x}{:02x}", c[0], c[1], c[2])); + } + } + fn pick_game(&self) { + if self.browser.run(Some(&self.window)) { + let path = self.browser.get_selected_item().unwrap(); + self.game_box.set_text(path.to_str().unwrap()); + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5ee8a3e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,355 @@ +#![windows_subsystem = "windows"] +mod gui; + +use native_windows_gui as nwg; +use nwg::NativeUi; + +use image::{DynamicImage, Rgba, GenericImageView, Pixel, Rgb}; +use std::path::{Path, PathBuf}; + +fn multiply_image_to_vtf(base: DynamicImage, c: &Rgba<u8>) -> Vec<u8> { + let result = image::DynamicImage::ImageRgba8(imageproc::map::map_colors( + &base.into_luma_alpha8(), + |px| { + let mut color = c.map_without_alpha(|c| (c as f32 * (px.0[0] as f32 / 255.)) as u8); + color.channels_mut()[3] = px.0[1]; + + color + } + )); + + vtf::create(result, vtf::ImageFormat::Rgba8888).unwrap() +} + +impl gui::PortalTools { + fn apply(&self) { + match self.apply_result() { + Ok(()) => nwg::modal_info_message(&self.window, "Portal Tools", "Success!"), + Err(s) => nwg::modal_info_message(&self.window, "Portal Tools", s.as_str()), + }; + } + + fn apply_result(&self) -> Result<(), String> { + let base = self.game_box.text(); + if !((PathBuf::from(format!("{}/portal/bin/client.dll", base))).exists() + && (PathBuf::from(format!("{}/hl2.exe", base))).exists()) + { + return Err("Invalid game directory.".to_string()); + } + + for c in &[self.blue_box.text(), self.orange_box.text(), self.carry_box.text(), self.gun_box.text()] { + hex::decode(c).map_err(|e| e.to_string())?; + } + + if self.steampipe() { + // create steampipe custom folders :vomit: + std::fs::create_dir_all( + PathBuf::from(format!("{}/portal/custom/portal_tools/materials/models/weapons/v_models/v_portalgun", base)) + ).map_err(|e| e.to_string()); + + std::fs::create_dir_all( + PathBuf::from(format!("{}/portal/custom/portal_tools/materials/models/weapons/w_models/portalgun", base)) + ).map_err(|e| e.to_string()); + + std::fs::create_dir_all( + PathBuf::from(format!("{}/portal/custom/portal_tools/materials/models/portals", base)) + ).map_err(|e| e.to_string()); + + std::fs::create_dir_all( + PathBuf::from(format!("{}/portal/custom/portal_tools/materials/sprites", base)) + ).map_err(|e| e.to_string()); + + std::fs::create_dir_all( + PathBuf::from(format!("{}/portal/custom/portal_tools/particles", base)) + ).map_err(|e| e.to_string()); + } + + if self.crosshair_check.check_state() == nwg::CheckBoxState::Checked { + self.apply_crosshair()? + } + if self.portals_check.check_state() == nwg::CheckBoxState::Checked { + self.apply_portals()? + } + if self.particles_check.check_state() == nwg::CheckBoxState::Checked { + self.apply_particles()? + } + if self.gun_check.check_state() == nwg::CheckBoxState::Checked { + self.apply_gun()? + } + Ok(()) + } + + fn apply_gun(&self) -> Result<(), String> { + let v_gun_trans = image::load_from_memory(&include_bytes!("assets/v_portalgun.png")[..]).unwrap(); + let w_gun_trans = image::load_from_memory(&include_bytes!("assets/w_portalgun.png")[..]).unwrap(); + + let gun_hex = hex::decode(&self.gun_box.text()).unwrap(); + let gun_color = Rgb::<u8>::from_slice(&gun_hex[..]).to_rgba(); + + // color viewmodel + let v_gun = image::DynamicImage::new_rgba8(v_gun_trans.width(), v_gun_trans.height()); + let mut v_gun = image::DynamicImage::ImageRgba8(imageproc::map::map_colors(&v_gun, |_| gun_color)); + image::imageops::overlay(&mut v_gun, &v_gun_trans, 0, 0); + + // color world model + let w_gun = image::DynamicImage::new_rgba8(w_gun_trans.width(), w_gun_trans.height()); + let mut w_gun = image::DynamicImage::ImageRgba8(imageproc::map::map_colors(&w_gun, |_| gun_color)); + image::imageops::overlay(&mut w_gun, &w_gun_trans, 0, 0); + + std::fs::write( + format!("{}/materials/models/weapons/v_models/v_portalgun/v_portalgun.vtf", self.prefix()), + vtf::create(v_gun, vtf::ImageFormat::Rgba8888).map_err(|e| e.to_string())? + ) + .map_err(|e| e.to_string())?; + + std::fs::write( + format!("{}/materials/models/weapons/w_models/portalgun/w_portalgun.vtf", self.prefix()), + vtf::create(w_gun, vtf::ImageFormat::Rgba8888).map_err(|e| e.to_string())? + ) + .map_err(|e| e.to_string())?; + + Ok(()) + } + // change the portal colors + fn apply_portals(&self) -> Result<(), String> { + let dx8_grey = + image::load_from_memory( + &include_bytes!("assets/dx8.png")[..], + ).unwrap(); + + let dx9_grey = + image::load_from_memory( + &include_bytes!("assets/dx9.png")[..], + ).unwrap(); + + let strider_greybeam = + image::load_from_memory( + &include_bytes!("assets/strider_bluebeam.png")[..], + ).unwrap(); + + let greylight = + image::load_from_memory( + &include_bytes!("assets/colorlight.png")[..], + ).unwrap(); + + + let blue_hex = hex::decode(&self.blue_box.text()).unwrap(); + let blue = Rgb::<u8>::from_slice(&blue_hex[..]).to_rgba(); + + let orange_hex = hex::decode(&self.orange_box.text()).unwrap(); + let orange = Rgb::<u8>::from_slice(&orange_hex[..]).to_rgba(); + + + // do the thing + fn x(p: String, i: &DynamicImage, c: &Rgba<u8>) -> Result<(), String> { + std::fs::write( + p, + multiply_image_to_vtf(i.clone(), c), + ) + .map_err(|e| e.to_string()) + } + + // dx9 + x( + format!("{}/materials/models/portals/portal-blue-color.vtf", self.prefix()), + &dx9_grey, + &blue + )?; + + x( + format!("{}/materials/models/portals/portal-orange-color.vtf", self.prefix()), + &dx9_grey, + &orange + )?; + + // dx8 + x( + format!("{}/materials/models/portals/portal-blue-color-dx8.vtf", self.prefix()), + &dx8_grey, + &blue + )?; + + x( + format!("{}/materials/models/portals/portal-orange-color-dx8.vtf", self.prefix()), + &dx8_grey, + &orange + )?; + + // sprites + x( + format!("{}/materials/sprites/strider_bluebeam.vtf", self.prefix()), + &strider_greybeam, + &blue, + )?; + + x( + format!("{}/materials/sprites/bluelight.vtf", self.prefix()), + &greylight, + &blue, + )?; + + x( + format!("{}/materials/sprites/orangelight.vtf", self.prefix()), + &greylight, + &orange, + )?; + + Ok(()) + } + + // change the particle colors + fn apply_particles(&self) -> Result<(), String> { + let portal_projectile_df = include_bytes!("assets/portal_projectile.pcf"); + let portalgun_df = include_bytes!("assets/portalgun.pcf"); + let portals_df = include_bytes!("assets/portals.pcf"); + + let mut portal_projectile = portal_projectile_df.to_vec(); + let mut portals = portals_df.to_vec(); + let mut portalgun = portalgun_df.to_vec(); + + let blue_hex = hex::decode(&self.blue_box.text()).unwrap(); + let orange_hex = hex::decode(&self.orange_box.text()).unwrap(); + + for c in portal_projectile_df.windows(3).enumerate() { + if c.1 == &[0x8C, 0xFF, 0xDB] { + // replace the bytes with the color + portal_projectile[c.0] = blue_hex[0]; + portal_projectile[c.0 + 1] = blue_hex[1]; + portal_projectile[c.0 + 2] = blue_hex[2]; + } else if c.1 == &[0xE6, 0x61, 0x00] { + portal_projectile[c.0] = orange_hex[0]; + portal_projectile[c.0 + 1] = orange_hex[1]; + portal_projectile[c.0 + 2] = orange_hex[2]; + } + } + + for c in portals_df.windows(3).enumerate() { + if c.1 == &[0x8C, 0xFF, 0xDB] { + portals[c.0] = blue_hex[0]; + portals[c.0 + 1] = blue_hex[1]; + portals[c.0 + 2] = blue_hex[2]; + } else if c.1 == &[0xE6, 0x61, 0x00] { + portals[c.0] = orange_hex[0]; + portals[c.0 + 1] = orange_hex[1]; + portals[c.0 + 2] = orange_hex[2]; + } + } + + for c in portalgun_df.windows(3).enumerate() { + if c.1 == &[0x8C, 0xFF, 0xDB] { + portalgun[c.0] = blue_hex[0]; + portalgun[c.0 + 1] = blue_hex[1]; + portalgun[c.0 + 2] = blue_hex[2]; + } else if c.1 == &[0xE6, 0x61, 0x00] { + portalgun[c.0] = orange_hex[0]; + portalgun[c.0 + 1] = orange_hex[1]; + portalgun[c.0 + 2] = orange_hex[2]; + } + } + + std::fs::write(format!("{}/particles/portalgun.pcf", self.prefix()), portalgun).map_err(|e| e.to_string())?; + std::fs::write(format!("{}/particles/portal_projectile.pcf", self.prefix()), portal_projectile).map_err(|e| e.to_string())?; + std::fs::write(format!("{}/particles/portals.pcf", self.prefix()), portals).map_err(|e| e.to_string())?; + Ok(()) + } + + // apply crosshair changes + fn apply_crosshair(&self) -> Result<(), String> { + let mut cdll = std::fs::read( + format!("{}/portal/bin/client.dll", self.game_box.text()) + ) + .map_err(|e| e.to_string())?; + + struct Color { + r: u8, + g: u8, + b: u8, + } + + impl From<&[u8]> for Color { + fn from(s: &[u8]) -> Self { + Self { + r: s[0], + g: s[1], + b: s[2], + } + } + } + + let bl = Color::from(hex::decode(self.blue_box.text()).unwrap().as_slice()); + let or = Color::from(hex::decode(self.orange_box.text()).unwrap().as_slice()); + let ca = Color::from(hex::decode(self.carry_box.text()).unwrap().as_slice()); + + if !self.steampipe() { + let patch = [ + 0x8B, 0x44, 0x24, 0x08, 0x83, 0xE8, 0x00, 0x74, 0x37, 0x83, 0xE8, 0x01, 0xB1, 0xFF, + 0x74, 0x20, 0x83, 0xE8, 0x01, 0x8B, 0x44, 0x24, 0x04, 0xC6, 0x00, or.r, 0xC6, 0x40, + 0x03, 0xFF, 0x74, 0x07, 0x88, 0x48, 0x01, 0x88, 0x48, 0x02, 0xC3, 0xC6, 0x40, 0x01, + or.g, 0xC6, 0x40, 0x02, or.b, 0xC3, 0x8B, 0x44, 0x24, 0x04, 0xC6, 0x00, bl.r, 0xC6, + 0x40, 0x01, bl.g, 0xC6, 0x40, 0x02, bl.b, 0xC3, 0x8B, 0x44, 0x24, 0x04, 0xC6, 0x00, + ca.r, 0xC6, 0x40, 0x01, ca.g, 0xC6, 0x40, 0x02, ca.b, 0xC6, + ]; + + let pos = if let Some(n) = cdll + .windows(8) + .position(|s| s == [0x40, 0x03, 0xFF, 0xC3, 0xCC, 0xCC, 0xCC, 0xCC]) + { + n - patch.len() + } else { + return Err("invalid client.dll".to_string()); + }; + + for i in 0..patch.len() { + cdll[i + pos] = patch[i]; + } + } else { + cdll[0x001c7a49] = bl.r; + cdll[0x001c7a49 + 1] = bl.g; + cdll[0x001c7a49 + 2] = bl.b; + + cdll[0x001c7a3e] = or.r; + cdll[0x001c7a3e + 1] = or.g; + cdll[0x001c7a3e + 2] = or.b; + + cdll[0x001c7a54] = ca.r; + cdll[0x001c7a54 + 1] = ca.g; + cdll[0x001c7a54 + 2] = ca.b; + } + + if let Err(e) = std::fs::write(format!("{}/portal/bin/client.dll", self.game_box.text()), cdll) { + Err(e.to_string()) + } else { + Ok(()) + } + } + + fn steampipe(&self) -> bool { + Path::new(&format!("{}/portal/portal_pak_dir.vpk", &self.game_box.text())) + .exists() + } + + fn prefix(&self) -> String { + if self.steampipe() { + format!("{}/portal/custom/portal_tools/", &self.game_box.text()) + } else { + format!("{}/portal/", &self.game_box.text()) + } + } +} + +fn main() { + nwg::init().expect("Failed to init Native Windows GUI"); + + let mut font = nwg::Font::default(); + + nwg::Font::builder() + .family("Segoe UI") + .size(14) + .build(&mut font); + + nwg::Font::set_global_default(Some(font)); + + let _calc = gui::PortalTools::build_ui(Default::default()).expect("Failed to build UI"); + + nwg::dispatch_thread_events(); +} |
