From a0c89c624fffbfddf4577c73cccb05cd410ef79a Mon Sep 17 00:00:00 2001 From: C J Silverio Date: Wed, 29 Nov 2023 15:18:18 -0800 Subject: [PATCH] Dry-coded the poison indicator feature. There's a new optional per-slot element in layouts v2 (electric boogaloo). This element displays a poison indicator if the item in the slot is poisoned. It's only meaningful for left/right hand slots, but we make no effort to enforce this; hud items in those slots will never say they're poisoned. The indicator looks like this: ```toml [left.poison] offset = { x = 0.0, y = 50.0 } [left.poison.indicator] svg = "icons/indicator_poison.svg" size = { x = 23.0, y = 23.0 } color = { r = 255, g = 255, b = 255, a = 255 } ``` HUD items have a function `is_poisoned()` for checking this bit. This call goes back to C++ to look at the associated bound object's extra data to see if it's poisoned. We are checking this on every draw loop right now. I'm not sure caching it would be a win, because I have not yet investigated whether there's any reasonable invalidation notification. The renderer renders this layout element if the item is poisoned and if the color for the image has non-zero alpha. This feature is only supported on layouts v2. This addresses the poison indicator part of bug #38. --- data/SKSE/plugins/SoulsyHUD_Layout.toml | 14 +++ .../resources/icons/indicator_poison.svg | 24 +++++ layouts/square/LayoutV2.toml | 7 +- layouts/square/SoulsyHUD_Layout.toml | 32 +++--- src/data/huditem.rs | 11 +- src/game/equippable.cpp | 2 +- src/game/gear.cpp | 10 ++ src/game/gear.h | 1 + src/game/player.cpp | 6 ++ src/game/player.h | 1 + src/layouts/layout_v1.rs | 11 ++ src/layouts/layout_v2.rs | 102 +++++++++++++++--- src/lib.rs | 8 ++ src/renderer/ui_renderer.cpp | 14 +++ 14 files changed, 198 insertions(+), 45 deletions(-) create mode 100644 data/SKSE/plugins/resources/icons/indicator_poison.svg diff --git a/data/SKSE/plugins/SoulsyHUD_Layout.toml b/data/SKSE/plugins/SoulsyHUD_Layout.toml index 498680d1..a42685eb 100644 --- a/data/SKSE/plugins/SoulsyHUD_Layout.toml +++ b/data/SKSE/plugins/SoulsyHUD_Layout.toml @@ -179,6 +179,13 @@ color = { r = 255, g = 255, b = 255, a = 255 } offset = { x = 60.0, y = 0.0 } size = { x = 30.0, y = 30.0 } +[left.poison] +offset = { x = 0.0, y = 50.0 } +[left.poison.indicator] +svg = "icons/indicator_poison.svg" +size = { x = 23.0, y = 23.0 } +color = { r = 255, g = 255, b = 255, a = 255 } + [[left.text]] offset = { x = 125.0, y = 107.0 } color = { r = 255, g = 255, b = 255, a = 255 } @@ -205,6 +212,13 @@ color = { r = 255, g = 255, b = 255, a = 255 } offset = { x = -60.0, y = 0.0 } size = { x = 30.0, y = 30.0 } +[right.poison] +offset = { x = 0.0, y = 50.0 } +[right.poison.indicator] +svg = "icons/indicator_poison.svg" +size = { x = 23.0, y = 23.0 } +color = { r = 255, g = 255, b = 255, a = 255 } + [[right.text]] color = { r = 255, g = 255, b = 255, a = 255 } offset = { x = 10.0, y = 52.0 } diff --git a/data/SKSE/plugins/resources/icons/indicator_poison.svg b/data/SKSE/plugins/resources/icons/indicator_poison.svg new file mode 100644 index 00000000..d6540d32 --- /dev/null +++ b/data/SKSE/plugins/resources/icons/indicator_poison.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/layouts/square/LayoutV2.toml b/layouts/square/LayoutV2.toml index 90979334..66ba8d0e 100644 --- a/layouts/square/LayoutV2.toml +++ b/layouts/square/LayoutV2.toml @@ -2,11 +2,6 @@ global_scale = 2.0 -# A named location for the HUD. Use this as a shortcut for common anchor -# points. These work no matter what the player's screen resolution is, because this -# is turned into a location point at run-time. -# Values: bottom_left, bottom_right, top_left, top_right, center, -# center_top, center_bottom, left_center, right_center anchor_name = "bottom_left" # You can also specify the anchor point like this if a named anchor point @@ -113,9 +108,9 @@ orientation = "horizontal" # defaults to vertical [right.progress_circle] # same -# NOT YET IMPLEMENTED. [right.poison] offset = { x = 20.0, y = 20.0 } +[right.poison.indicator] svg = "poison.svg" color = {r = 160, g = 240, b = 2, a = 255 } # same color as poison. consider using the color names? size = { x = 10.0, y = 10.0 } diff --git a/layouts/square/SoulsyHUD_Layout.toml b/layouts/square/SoulsyHUD_Layout.toml index 39024cb5..1b2ba9be 100644 --- a/layouts/square/SoulsyHUD_Layout.toml +++ b/layouts/square/SoulsyHUD_Layout.toml @@ -95,34 +95,20 @@ color = { r = 255, g = 255, b = 255, a = 128 } # fill in a value, name it surrounded with curly braces, like the # example. # Possible values: count, name, kind, + [[right.text]] alignment = "right" offset = { x = 0.0, y = 0.0 } -color = { r = 255, g = 255, b = 255, a = 0 } +color = { r = 255, g = 255, b = 255, a = 255 } font_size = 18.0 contents = "{count} {name}" -# Optional burn time/enchant charge bar. NOT YET IMPLEMENTED. -[right.progress_bar] -offset = { x = 20.0, y = 20.0 } -background = "bar_bg.svg" -color = { r = 255, g = 255, b = 255, a = 255 } -size = { x = 10.0 , y = 100.0 } # this is how to scale the bar, irrespective of orientation -orientation = "horizontal" # defaults to vertical - -# NOT YET IMPLEMENTED. -[right.progress_arc] -# this might be an option - -# NOT YET IMPLEMENTED. -[right.progress_circle] -# same - -# NOT YET IMPLEMENTED. +# An optional indicator for showing if this item is poisoned. [right.poison] offset = { x = 20.0, y = 20.0 } +[right.poison.indicator] svg = "poison.svg" -color = {r = 160, g = 240, b = 2, a = 255 } # same color as poison. consider using the color names? +color = {r = 160, g = 240, b = 2, a = 255 } size = { x = 10.0, y = 10.0 } # ----- left hand slot @@ -149,6 +135,14 @@ svg = "hotkey_bg.svg" size = { x = 30.0 , y = 30.0} color = { r = 255, g = 255, b = 255, a = 128 } +# An optional indicator for showing if this item is poisoned. +[left.poison] +offset = { x = 20.0, y = 20.0 } +[left.poison.indicator] +svg = "poison.svg" +color = {r = 160, g = 240, b = 2, a = 255 } +size = { x = 10.0, y = 10.0 } + [[left.text]] alignment = "left" offset = { x = -50.0, y = 55.0 } diff --git a/src/data/huditem.rs b/src/data/huditem.rs index f145bfc7..6cb03190 100644 --- a/src/data/huditem.rs +++ b/src/data/huditem.rs @@ -2,12 +2,13 @@ use std::collections::HashMap; use std::ffi::CString; use std::fmt::Display; +use cxx::let_cxx_string; use strfmt::strfmt; use super::base::BaseType; use super::HasIcon; use crate::images::icons::Icon; -use crate::plugin::{Color, ItemCategory}; +use crate::plugin::{weaponIsPoisoned, Color, ItemCategory}; /// A TESForm item that the player can use or equip, with the data /// that drives the HUD cached for fast access. @@ -163,8 +164,12 @@ impl HudItem { } pub fn is_poisoned(&self) -> bool { - // TODO track this somehow - false + if !self.is_weapon() { + false + } else { + let_cxx_string!(form_spec = self.form_string()); + weaponIsPoisoned(&form_spec) + } } /// Charge as a float from 0.0 to 1.0 inclusive. For enchanted weapons diff --git a/src/game/equippable.cpp b/src/game/equippable.cpp index 9a63e116..c444783f 100644 --- a/src/game/equippable.cpp +++ b/src/game/equippable.cpp @@ -235,7 +235,7 @@ namespace equippable alchemy_potion->ForEachKeyword(KeywordAccumulator::collect); auto& keywords = KeywordAccumulator::mKeywords; rust::Box item = hud_item_from_keywords( - ItemCategory::Food, *keywords, std::move(chonker), form_string, count, false); + ItemCategory::Food, *keywords, std::move(chonker), form_string, count, false, false); return item; } else diff --git a/src/game/gear.cpp b/src/game/gear.cpp index dc574c31..70cc7412 100644 --- a/src/game/gear.cpp +++ b/src/game/gear.cpp @@ -102,6 +102,16 @@ namespace game return worn; } + bool isItemPoisoned(const RE::TESForm* form) + { + auto* the_player = RE::PlayerCharacter::GetSingleton(); + RE::TESBoundObject* obj = nullptr; + RE::ExtraDataList* extra = nullptr; + auto count = boundObjectForForm(form, player, obj, extra_data); + if (extra_data) { return extra_data->HasType(RE::ExtraDataType::kPoison); } + return false; + } + void equipItemByFormAndSlot(RE::TESForm* form, RE::BGSEquipSlot*& slot, RE::PlayerCharacter*& player) { auto slot_is_left = slot == left_hand_equip_slot(); diff --git a/src/game/gear.h b/src/game/gear.h index 00c4a42e..a50fa0e2 100644 --- a/src/game/gear.h +++ b/src/game/gear.h @@ -15,6 +15,7 @@ namespace game RE::TESBoundObject*& outval, RE::ExtraDataList*& outextra); + bool isItemPoisoned(const RE::TESForm* form); bool isItemWorn(RE::TESBoundObject*& object, RE::PlayerCharacter*& the_player); // bottleneck for equipping everything void equipItemByFormAndSlot(RE::TESForm* form, RE::BGSEquipSlot*& slot, RE::PlayerCharacter*& the_player); diff --git a/src/game/player.cpp b/src/game/player.cpp index fb5cad05..e58ee400 100644 --- a/src/game/player.cpp +++ b/src/game/player.cpp @@ -38,6 +38,12 @@ namespace player return useAltGrip; } + bool weaponIsPoisoned(const std::string& form_spec) + { + auto* const form = formSpecToFormItem(form_spec); + return game::isItemPoisoned(form); + } + rust::String specEquippedLeft() { auto* player = RE::PlayerCharacter::GetSingleton(); diff --git a/src/game/player.h b/src/game/player.h index 40e09513..f6cea18e 100644 --- a/src/game/player.h +++ b/src/game/player.h @@ -49,6 +49,7 @@ namespace player void consumePotion(const std::string& form_spec); void poison_weapon(RE::PlayerCharacter*& a_player, RE::AlchemyItem*& a_poison, uint32_t a_count); + bool weaponIsPoisoned(const std::string& form_spec); bool hasItemOrSpell(const std::string& form_spec); uint32_t itemCount(const std::string& form_spec); diff --git a/src/layouts/layout_v1.rs b/src/layouts/layout_v1.rs index 49d4f2aa..09f94672 100644 --- a/src/layouts/layout_v1.rs +++ b/src/layouts/layout_v1.rs @@ -186,6 +186,17 @@ impl HudLayout1 { hotkey_bg_size: slot.hotkey_size.scale(self.global_scale), hotkey_bg_color: slot.hotkey_bg_color.clone(), hotkey_bg_image: "key_bg.svg".to_string(), + + poison_image: "".to_string(), + poison_color: Color { + r: 0, + g: 0, + b: 0, + a: 0, + }, + poison_center: Point { x: 0.0, y: 0.0 }, + poison_size: Point { x: 0.0, y: 0.0 }, + text, } } diff --git a/src/layouts/layout_v2.rs b/src/layouts/layout_v2.rs index aba15d8d..679c0b82 100644 --- a/src/layouts/layout_v2.rs +++ b/src/layouts/layout_v2.rs @@ -69,6 +69,7 @@ pub struct SlotElement { background: Option, hotkey: Option, progress_bar: Option, + poison: Option, } impl HudLayout2 { @@ -116,6 +117,12 @@ impl HudLayout2 { .map(|xs| self.flatten_text(xs, ¢er)) .collect(); + let poison = slot.poison.clone().unwrap_or_default(); + let poison_image = poison.indicator.svg; + let poison_size = poison.indicator.size.scale(self.global_scale); + let poison_color = poison.indicator.color; + let poison_center = center.translate(&poison.offset.scale(self.global_scale)); + SlotFlattened { element, center: center.clone(), @@ -131,6 +138,10 @@ impl HudLayout2 { hotkey_bg_size: hkbg.size.scale(self.global_scale), hotkey_bg_color: hkbg.color, hotkey_bg_image: hkbg.svg, + poison_size, + poison_image, + poison_color, + poison_center, text, } } @@ -217,6 +228,21 @@ pub struct ProgressElement { color: Color, } +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +pub struct PoisonElement { + offset: Point, + indicator: ImageElement, +} + +impl Default for PoisonElement { + fn default() -> Self { + PoisonElement { + offset: Point { x: 0.0, y: 0.0 }, + indicator: ImageElement::default(), + } + } +} + impl From<&HudLayout2> for LayoutFlattened { fn from(v: &HudLayout2) -> Self { let mut slots = vec![ @@ -259,21 +285,62 @@ mod tests { use super::*; use crate::layouts::{resolutionHeight, Layout}; - // #[test] - // #[ignore] - // fn default_layout_valid() { - // // The github runner compilation step can't find this file. I have no idea why not. - // let buf = include_str!("../../data/SKSE/plugins/SoulsyHUD_layout.toml"); - // let builtin: Layout = toml::from_str(buf).expect("layout should be valid toml"); - // match builtin { - // Layout::Version1(_) => unreachable!(), - // Layout::Version2(v) => { - // assert_eq!(v.anchor_name, NamedAnchor::BottomLeft); - // assert_eq!(v.anchor_point().x, 150.0); - // assert_eq!(v.anchor_point().y, 1290.0); - // } - // } - // } + #[test] + fn default_layout_valid() { + // The github runner compilation step can't find this file. I have no idea why not. + let buf = include_str!("../../data/SKSE/plugins/SoulsyHUD_layout.toml"); + match toml::from_str::(buf) { + Ok(v) => { + assert_eq!(v.anchor_point().x, 150.0); + } + Err(e) => { + eprintln!("{e:#?}"); + unreachable!(); + } + } + let builtin: Layout = toml::from_str(buf).expect("layout should be valid toml"); + match builtin { + Layout::Version1(_) => unreachable!(), + Layout::Version2(v) => { + assert_eq!(v.anchor_name, NamedAnchor::BottomLeft); + assert_eq!(v.anchor_point().x, 150.0); + assert_eq!(v.anchor_point().y, 1290.0); + let right_poison = v + .right + .poison + .as_ref() + .expect("the right slot should have a poison indicator"); + assert_eq!( + right_poison.indicator.svg, + "icons/indicator_poison.svg".to_string() + ); + let _left_poison = v + .left + .poison + .as_ref() + .expect("the left slot should have a poison indicator"); + + let flattened = Layout::Version2(v.clone()).flatten(); + assert_eq!(flattened.anchor, v.anchor_point()); + let right_slot = flattened + .slots + .iter() + .find(|slot| slot.element == HudElement::Right) + .expect("the flattened layout needs to have a right slot"); + assert_eq!(right_slot.poison_image, right_poison.indicator.svg); + assert_eq!(right_slot.poison_color, right_poison.indicator.color); + let slot_center = Point { + x: flattened.anchor.x + (v.right.offset.x * flattened.global_scale), + y: flattened.anchor.y + (v.right.offset.y * flattened.global_scale), + }; + assert_eq!(right_slot.center, slot_center); + assert_eq!( + right_slot.poison_center, + slot_center.translate(&right_poison.offset) + ); + } + } + } #[test] fn centered_layout_valid() { @@ -377,6 +444,9 @@ mod tests { }; assert_eq!(right_slot.center, slot_center); assert_eq!(layout.right.text.len(), right_slot.text.len()); - assert_eq!(layout.right.text[0].font_size * layout.global_scale, right_slot.text[0].font_size); + assert_eq!( + layout.right.text[0].font_size * layout.global_scale, + right_slot.text[0].font_size + ); } } diff --git a/src/lib.rs b/src/lib.rs index 28cac476..35ac7e90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,6 +127,11 @@ pub mod plugin { hotkey_bg_color: Color, hotkey_bg_image: String, + poison_size: Point, + poison_center: Point, + poison_color: Color, + poison_image: String, + text: Vec, } @@ -485,6 +490,9 @@ pub mod plugin { /// Check if the player still has items from this form in their inventory. fn hasItemOrSpell(form_spec: &CxxString) -> bool; + /// Is this weapon poisoned? + fn weaponIsPoisoned(form_spec: &CxxString) -> bool; + /// Does the player have a bow or crossbow equipped? fn hasRangedEquipped() -> bool; /// Get a vec of form specs for all relevant ammo in the player's inventory. diff --git a/src/renderer/ui_renderer.cpp b/src/renderer/ui_renderer.cpp index eb7f48f2..7e8a8133 100644 --- a/src/renderer/ui_renderer.cpp +++ b/src/renderer/ui_renderer.cpp @@ -454,6 +454,7 @@ namespace ui drawText(entrytxt, textPos, label.font_size, label.color, label.alignment); } + // Draw the hotkey reminder if asked. if (slotLayout.hotkey_color.a > 0) { const auto hk_im_center = ImVec2(slotLayout.hotkey_center.x, slotLayout.hotkey_center.y); @@ -471,6 +472,19 @@ namespace ui static_cast(slotLayout.hotkey_size.y - 2.0f)); drawElement(texture, hk_im_center, size, 0.f, slotLayout.hotkey_color); } + + // Finally, the poisoned indicator. + if (slotLayout.poison_color.a > 0 && entry->is_poisoned()) + { + const auto poison_img = std::string(slotLayout.poison_image); + if (lazyLoadHudImage(poison_img)) + { + const auto poison_center = ImVec2(slotLayout.poison_center.x, slotLayout.poison_center.y); + const auto [texture, width, height] = HUD_IMAGES_MAP[poison_img]; + const auto size = ImVec2(slotLayout.poison_size.x, slotLayout.poison_size.y); + drawElement(texture, poison_center, size, 0.f, slotLayout.poison_color); + } + } } // draw_animations_frame();