aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/assets/colorlight.pngbin0 -> 6324 bytes
-rw-r--r--src/assets/dx8.pngbin0 -> 13162 bytes
-rw-r--r--src/assets/dx9.pngbin0 -> 1610 bytes
-rw-r--r--src/assets/portal_projectile.pcfbin0 -> 102749 bytes
-rw-r--r--src/assets/portalgun.pcfbin0 -> 23835 bytes
-rw-r--r--src/assets/portals.pcfbin0 -> 35908 bytes
-rw-r--r--src/assets/strider_bluebeam.pngbin0 -> 3104 bytes
-rw-r--r--src/assets/v_portalgun.pngbin0 -> 291006 bytes
-rw-r--r--src/assets/w_portalgun.pngbin0 -> 84016 bytes
-rw-r--r--src/gui.rs147
-rw-r--r--src/main.rs355
11 files changed, 502 insertions, 0 deletions
diff --git a/src/assets/colorlight.png b/src/assets/colorlight.png
new file mode 100644
index 0000000..4b5615b
--- /dev/null
+++ b/src/assets/colorlight.png
Binary files differ
diff --git a/src/assets/dx8.png b/src/assets/dx8.png
new file mode 100644
index 0000000..f2938cd
--- /dev/null
+++ b/src/assets/dx8.png
Binary files differ
diff --git a/src/assets/dx9.png b/src/assets/dx9.png
new file mode 100644
index 0000000..1aa1f0f
--- /dev/null
+++ b/src/assets/dx9.png
Binary files differ
diff --git a/src/assets/portal_projectile.pcf b/src/assets/portal_projectile.pcf
new file mode 100644
index 0000000..5bb777b
--- /dev/null
+++ b/src/assets/portal_projectile.pcf
Binary files differ
diff --git a/src/assets/portalgun.pcf b/src/assets/portalgun.pcf
new file mode 100644
index 0000000..e3e3d91
--- /dev/null
+++ b/src/assets/portalgun.pcf
Binary files differ
diff --git a/src/assets/portals.pcf b/src/assets/portals.pcf
new file mode 100644
index 0000000..4577584
--- /dev/null
+++ b/src/assets/portals.pcf
Binary files differ
diff --git a/src/assets/strider_bluebeam.png b/src/assets/strider_bluebeam.png
new file mode 100644
index 0000000..2b0899e
--- /dev/null
+++ b/src/assets/strider_bluebeam.png
Binary files differ
diff --git a/src/assets/v_portalgun.png b/src/assets/v_portalgun.png
new file mode 100644
index 0000000..9a8452a
--- /dev/null
+++ b/src/assets/v_portalgun.png
Binary files differ
diff --git a/src/assets/w_portalgun.png b/src/assets/w_portalgun.png
new file mode 100644
index 0000000..1f63f81
--- /dev/null
+++ b/src/assets/w_portalgun.png
Binary files differ
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();
+}