diff --git a/.gitmodules b/.gitmodules index ebda3358..68121c78 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "extern/CommonLibSSE-NG"] path = extern/CommonLibSSE-NG url = https://github.com/ceejbot/CommonLibSSE-NG + branch = ceej/no-vr-build diff --git a/CMakeLists.txt b/CMakeLists.txt index e67808b5..03802cb3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ if(PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR) endif() set(NAME "SoulsyHUD") -set(VERSION 0.14.1.0) +set(VERSION 0.15.0.0) project( ${NAME} @@ -212,16 +212,16 @@ add_subdirectory(${CommonLibPath} ${CommonLibName} EXCLUDE_FROM_ALL) target_include_directories( ${PROJECT_NAME} PRIVATE - ${CMAKE_CURRENT_BINARY_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src/config ${CMAKE_CURRENT_SOURCE_DIR}/src/game - ${CMAKE_CURRENT_SOURCE_DIR}/src/migrate_me + ${CMAKE_CURRENT_SOURCE_DIR}/src/log ${CMAKE_CURRENT_SOURCE_DIR}/src/plugin ${CMAKE_CURRENT_SOURCE_DIR}/src/renderer ${CMAKE_CURRENT_SOURCE_DIR}/src/util ${CARGO_TARGET_DIR}/cxxbridge/soulsy/src ${CARGO_TARGET_DIR}/cxxbridge + ${CMAKE_CURRENT_BINARY_DIR}/include ) # The last few of these are surprising, but are pulled in by some of the rust crates diff --git a/Cargo.lock b/Cargo.lock index d4bae23b..3aa1deb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -789,6 +789,7 @@ dependencies = [ "simplelog", "strfmt", "strum", + "textcode", "toml", ] @@ -859,6 +860,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textcode" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383d5314770f8b7b2129936c78638db0dad1cfc9e7ef78706d4ccf9f0a0677fd" + [[package]] name = "time" version = "0.3.25" diff --git a/Cargo.toml b/Cargo.toml index cf5f1ae3..869ce79e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,39 +1,40 @@ [package] -name = "soulsy" -description = "A minimal Souls-like HUD & hotkeys mod for Skyrim AE. SKSE plugin." -version = "0.14.1" -edition = "2021" +authors = ["C J Silverio "] +description = "A minimal Souls-like HUD & hotkeys mod for Skyrim AE. SKSE plugin." +edition = "2021" +keywords = ["c++", "skyrim"] +license = "GPL-3.0" +name = "soulsy" +readme = "README.md" rust-version = "1.71.1" -authors = ["C J Silverio "] -license = "GPL-3.0" -readme = "README.md" -keywords = ["skyrim", "c++"] +version = "0.14.1" [lib] crate-type = ["staticlib"] [dependencies] -bincode = "2.0.0-rc.3" -cxx = { version = "1.0.111", features = ["c++20"] } -enumset = "1.1.3" -eyre = "0.6.9" -log = "0.4.20" -lru = "0.12.1" +bincode = "2.0.0-rc.3" +cxx = { version = "1.0.111", features = ["c++20"] } +enumset = "1.1.3" +eyre = "0.6.9" +log = "0.4.20" +lru = "0.12.1" once_cell = "1.18.0" -resvg = "0.37.0" -rust-ini = "0.20.0" -serde = { version = "1.0.193", features = ["derive"] } +resvg = "0.37.0" +rust-ini = "0.20.0" +serde = { version = "1.0.193", features = ["derive"] } simplelog = "0.12.1" -strfmt = "0.2.4" -strum = { version = "0.25.0", features = ["derive"] } -toml = "0.8.6" +strfmt = "0.2.4" +strum = { version = "0.25.0", features = ["derive"] } +textcode = "0.2.2" +toml = "0.8.6" [build-dependencies] cxx-build = "1.0.111" [dev-dependencies] -petname = { version = "1.1.3", default-features = false, features = ["std_rng", "default_dictionary"] } -rand = "0.8.5" +petname = { version = "1.1.3", default-features = false, features = ["default_dictionary", "std_rng"] } +rand = "0.8.5" [profile.release] debug = true diff --git a/cmake/sourcelist.cmake b/cmake/sourcelist.cmake index b3333a10..7625aee3 100644 --- a/cmake/sourcelist.cmake +++ b/cmake/sourcelist.cmake @@ -16,11 +16,11 @@ set(headers ${headers} src/renderer/animation_handler.h src/renderer/image_path.h src/renderer/ui_renderer.h + src/soulsy.h src/util/constant.h src/util/helpers.h src/util/key_path.h src/util/offset.h - src/util/string_util.h ) set(sources ${sources} ${headers} diff --git a/docs/article-theming.md b/docs/article-theming.md index e5bbcabd..b088bef5 100644 --- a/docs/article-theming.md +++ b/docs/article-theming.md @@ -14,8 +14,6 @@ SoulsyHUD provides decent defaults for all of these. However, all of these eleme You can use any Truetype font to render text in the HUD. If the font supports it, you can generate glyphs for character sets beyond the usual Western alphabet glyphs. The HUD should be able to render any valid [UTF-8 character](https://www.utf8.com) if the font includes glyphs for that character. -⚠️ There are some characters the game menus can display that are invalid UTF-8 characters. Skyrim's Flash menus support an older text encoding called [UCS-2](https://en.wikipedia.org/wiki/Universal_Coded_Character_Set), and some characters in that encoding are not converted properly to UTF-8. I have encountered two mods that have item names that can't be represented properly. Fixing this bug is on my list for post-1.0. See [the ucs2-rs library](https://lib.rs/crates/ucs2). Even simple text is nothing but simple, it turns out. - Put the `.ttf` file in `SKSE/plugins/resources/fonts` and name it in the layout. The `font_size` option specifies what size to generate Imgui font billboard data. Text will look best when rendered at this size, so make this match whatever size most of your HUD text is. Here's the full set of font options as they'd appear in a layout file: diff --git a/extern/CommonLibSSE-NG b/extern/CommonLibSSE-NG index bcb3f251..3838fe49 160000 --- a/extern/CommonLibSSE-NG +++ b/extern/CommonLibSSE-NG @@ -1 +1 @@ -Subproject commit bcb3f2514f57ecb7255eadac7e039f79c07fe89c +Subproject commit 3838fe49a93bda641e3c313149cb9c8b069f3f37 diff --git a/installer/core/SoulsyHUD_KID.ini b/installer/core/SoulsyHUD_KID.ini index 0cac7156..207f446c 100644 --- a/installer/core/SoulsyHUD_KID.ini +++ b/installer/core/SoulsyHUD_KID.ini @@ -35,25 +35,6 @@ Keyword = Soulsy_BoundShield|Magic Effect|*BoundShield Keyword = Soulsy_BoundSword|Magic Effect|*BoundSword Keyword = Soulsy_BoundGreatword|Magic Effect|*BoundGreatsword -; some icon hints; unused -; Keyword = Soulsy_ArtBall|Magic Effect|*Ball|H -; Keyword = Soulsy_ArtBlast|Magic Effect|*Blast|H -; Keyword = Soulsy_ArtBolt|Magic Effect|*Bolt|H -; Keyword = Soulsy_ArtBreath|Magic Effect|*Breath|H -; Keyword = Soulsy_ArtChainLightning|Magic Effect|*Chain|H -; Keyword = Soulsy_ArtFlame|Magic Effect|*Flame|H -; Keyword = Soulsy_ArtFlame|Magic Effect|*Fire|H -; Keyword = Soulsy_ArtLightning|Magic Effect|*Lightning|H -; Keyword = Soulsy_ArtProjectile|Magic Effect|*Projectile|H -; Keyword = Soulsy_ArtSpike|Magic Effect|*Spear|H -; Keyword = Soulsy_ArtSpike|Magic Effect|*Spike|H -; Keyword = Soulsy_ArtSpike|Magic Effect|*Shard|H -; Keyword = Soulsy_ArtStorm|Magic Effect|*Storm|H -; Keyword = Soulsy_ArtTornado|Magic Effect|*Cyclone|H -; Keyword = Soulsy_ArtTornado|Magic Effect|*Tornado|H -; Keyword = Soulsy_ArtWall|Magic Effect|*Wall|H -; Keyword = Soulsy_ArtLeaf|Magic Effect|*Oak|-H - ; Some powers we'd like to assign keywords from OCF to, mostly ; for misc mods not yet covered by OCF that I use. ; mcCampingLight.esp @@ -66,80 +47,472 @@ Keyword = OCF_InvColorRed|Armor|0x809~EldenRingLantern.esp Keyword = OCF_InvColorSun|Armor|0xD63~EldenRingLantern.esp Keyword = OCF_InvColorWhite|Armor|0x805~EldenRingLantern.esp -; Magic Effects associated with shouts. -Keyword = Soulsy_Shout_AnimalAllegiance|Magic Effect|0x9e0c8 -Keyword = Soulsy_Shout_AnimalAllegiance|Magic Effect|0x9e0ca -Keyword = Soulsy_Shout_AnimalAllegiance|Magic Effect|0x9e0cb -Keyword = Soulsy_Shout_AuraWhisper|Magic Effect|0x10319e -Keyword = Soulsy_Shout_AuraWhisper|Magic Effect|0x10e4fd -Keyword = Soulsy_Shout_AuraWhisper|Magic Effect|0x8afcb -Keyword = Soulsy_Shout_BattleFury|Magic Effect|0x40200c5 -Keyword = Soulsy_Shout_BattleFury|Magic Effect|0x40200c8 -Keyword = Soulsy_Shout_BattleFury|Magic Effect|0x403cecd -Keyword = Soulsy_Shout_BattleFury|Magic Effect|0x403cece -Keyword = Soulsy_Shout_BattleFury|Magic Effect|0x403cf2e -Keyword = Soulsy_Shout_BattleFury|Magic Effect|0x403cf2f -Keyword = Soulsy_Shout_BecomeEthereal|Magic Effect|0x64d68 -Keyword = Soulsy_Shout_BendWill|Magic Effect|0x40179e0 -Keyword = Soulsy_Shout_BendWill|Magic Effect|0x401f138 -Keyword = Soulsy_Shout_BendWill|Magic Effect|0x401ff1f -Keyword = Soulsy_Shout_CallDragon|Magic Effect|0x49454 -Keyword = Soulsy_Shout_CallDragon|Magic Effect|0xfead1 -Keyword = Soulsy_Shout_CallOfValor|Magic Effect|0x51963 -Keyword = Soulsy_Shout_CallOfValor|Magic Effect|0x51965 -Keyword = Soulsy_Shout_CallOfValor|Magic Effect|0x51966 -Keyword = Soulsy_Shout_ClearSkies|Magic Effect|0x78b9d -Keyword = Soulsy_Shout_ClearSkies|Magic Effect|0x78b9f -Keyword = Soulsy_Shout_ClearSkies|Magic Effect|0x78ba1 -Keyword = Soulsy_Shout_Cyclone|Magic Effect|0x40200bd -Keyword = Soulsy_Shout_Cyclone|Magic Effect|0x40200bf -Keyword = Soulsy_Shout_Cyclone|Magic Effect|0x4028ebb -Keyword = Soulsy_Shout_Disarm|Magic Effect|0x8bb26 -Keyword = Soulsy_Shout_Disarm|Magic Effect|0xcd088 -Keyword = Soulsy_Shout_Disarm|Magic Effect|0xcd089 -Keyword = Soulsy_Shout_Dismay|Magic Effect|0x23959 -Keyword = Soulsy_Shout_Dismay|Magic Effect|0x2395c -Keyword = Soulsy_Shout_Dismay|Magic Effect|0x2395d -Keyword = Soulsy_Shout_Dismay|Magic Effect|0x7b6ba -Keyword = Soulsy_Shout_Dismay|Magic Effect|0x7b6bb -Keyword = Soulsy_Shout_Dismay|Magic Effect|0x7b6bc -Keyword = Soulsy_Shout_DragonAspect|Magic Effect|0x401df90 -Keyword = Soulsy_Shout_DragonAspect|Magic Effect|0x401df97 -Keyword = Soulsy_Shout_DragonAspect|Magic Effect|0x4021730 -Keyword = Soulsy_Shout_Dragonrend|Magic Effect|0x17192 -Keyword = Soulsy_Shout_Dragonrend|Magic Effect|0x3ea30 -Keyword = Soulsy_Shout_Dragonrend|Magic Effect|0x44255 -Keyword = Soulsy_Shout_DrainVitality|Magic Effect|0x2008448 -Keyword = Soulsy_Shout_DrainVitality|Magic Effect|0x2008449 -Keyword = Soulsy_Shout_DrainVitality|Magic Effect|0x200844a -Keyword = Soulsy_Shout_ElementalFury|Magic Effect|0x2c56f -Keyword = Soulsy_Shout_ElementalFury|Magic Effect|0x2c593 -Keyword = Soulsy_Shout_FireBreath|Magic Effect|0x20e16 -Keyword = Soulsy_Shout_FireBreath|Magic Effect|0x562ea -Keyword = Soulsy_Shout_FireBreath|Magic Effect|0x562eb -Keyword = Soulsy_Shout_FrostBreath|Magic Effect|0x5d16f -Keyword = Soulsy_Shout_FrostBreath|Magic Effect|0x5d170 -Keyword = Soulsy_Shout_FrostBreath|Magic Effect|0x5d171 -Keyword = Soulsy_Shout_IceForm|Magic Effect|0x4020e96 -Keyword = Soulsy_Shout_IceForm|Magic Effect|0xa0366 -Keyword = Soulsy_Shout_KynesPeace|Magic Effect|0x82a36 -Keyword = Soulsy_Shout_KynesPeace|Magic Effect|0x82a37 -Keyword = Soulsy_Shout_KynesPeace|Magic Effect|0x82a38 -Keyword = Soulsy_Shout_MarkedForDeath|Magic Effect|0x10319c -Keyword = Soulsy_Shout_MarkedForDeath|Magic Effect|0x103a9d -Keyword = Soulsy_Shout_Slowtime|Magic Effect|0x48acd -Keyword = Soulsy_Shout_SoulTear|Magic Effect|0x2007cb3 -Keyword = Soulsy_Shout_SoulTear|Magic Effect|0x2007cb4 -Keyword = Soulsy_Shout_SoulTear|Magic Effect|0x2007cb5 -Keyword = Soulsy_Shout_StormCall|Magic Effect|0xd5e81 -Keyword = Soulsy_Shout_StormCall|Magic Effect|0xe3f09 -Keyword = Soulsy_Shout_StormCall|Magic Effect|0xe3f0a -Keyword = Soulsy_Shout_SummonDurnehviir|Magic Effect|0x20030d3 -Keyword = Soulsy_Shout_SummonDurnehviir|Magic Effect|0x2010ec0 -Keyword = Soulsy_Shout_ThrowVoice|Magic Effect|0x7430d -Keyword = Soulsy_Shout_UnrelentingForce|Magic Effect|0x13e08 -Keyword = Soulsy_Shout_UnrelentingForce|Magic Effect|0x7f82e -Keyword = Soulsy_Shout_UnrelentingForce|Magic Effect|0x7f82f -Keyword = Soulsy_Shout_WhirlwindSprint|Magic Effect|0x2f7b9 -Keyword = Soulsy_Shout_WhirlwindSprint|Magic Effect|0x4372f -Keyword = Soulsy_Shout_WhirlwindSprint|Magic Effect|0x43730 +; Spells associated with shouts. +; We can't assign keywords to shouts directly, so we use their spells. + +; Skyrim.esm +Keyword = Soulsy_Shout_AnimalAllegiance|Spell|VoiceAnimalAllegiance1|NONE|100 +Keyword = Soulsy_Shout_AnimalAllegiance|Spell|VoiceAnimalAllegiance2|NONE|100 +Keyword = Soulsy_Shout_AnimalAllegiance|Spell|VoiceAnimalAllegiance3|NONE|100 +Keyword = Soulsy_Shout_AuraWhisper|Spell|VoiceAuraWhisper1|NONE|100 +Keyword = Soulsy_Shout_AuraWhisper|Spell|VoiceAuraWhisper2|NONE|100 +Keyword = Soulsy_Shout_AuraWhisper|Spell|VoiceAuraWhisper3|NONE|100 +Keyword = Soulsy_Shout_BecomeEthereal|Spell|VoiceBecomeEthereal1|NONE|100 +Keyword = Soulsy_Shout_BecomeEthereal|Spell|VoiceBecomeEthereal2|NONE|100 +Keyword = Soulsy_Shout_BecomeEthereal|Spell|VoiceBecomeEthereal3|NONE|100 +Keyword = Soulsy_Shout_CallDragon|Spell|VoiceCallDragon01|NONE|100 +Keyword = Soulsy_Shout_CallDragon|Spell|VoiceCallDragon03|NONE|100 +Keyword = Soulsy_Shout_CallOfValor|Spell|VoiceCallHero1|NONE|100 +Keyword = Soulsy_Shout_CallOfValor|Spell|VoiceCallHero2|NONE|100 +Keyword = Soulsy_Shout_CallOfValor|Spell|VoiceCallHero3|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|VoiceClearSkies1|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|VoiceClearSkies2|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|VoiceClearSkies3|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|VoiceClearSkiesSelf1|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|VoiceClearSkiesSelf2|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|VoiceClearSkiesSelf3|NONE|100 +Keyword = Soulsy_Shout_Disarm|Spell|VoiceDisarm1|NONE|100 +Keyword = Soulsy_Shout_Disarm|Spell|VoiceDisarm2|NONE|100 +Keyword = Soulsy_Shout_Disarm|Spell|VoiceDisarm3|NONE|100 +Keyword = Soulsy_Shout_Dismay|Spell|VoiceDismayingShout1|NONE|100 +Keyword = Soulsy_Shout_Dismay|Spell|VoiceDismayingShout2|NONE|100 +Keyword = Soulsy_Shout_Dismay|Spell|VoiceDismayingShout3|NONE|100 +Keyword = Soulsy_Shout_Dragonrend|Spell|VoiceDragonrend1|NONE|100 +Keyword = Soulsy_Shout_Dragonrend|Spell|VoiceDragonrend2|NONE|100 +Keyword = Soulsy_Shout_Dragonrend|Spell|VoiceDragonrend3|NONE|100 +Keyword = Soulsy_Shout_ElementalFury|Spell|VoiceElementalFury1|NONE|100 +Keyword = Soulsy_Shout_ElementalFury|Spell|VoiceElementalFury2|NONE|100 +Keyword = Soulsy_Shout_ElementalFury|Spell|VoiceElementalFury3|NONE|100 +Keyword = Soulsy_Shout_FireBreath|Spell|VoiceFireBreath1|NONE|100 +Keyword = Soulsy_Shout_FireBreath|Spell|VoiceFireBreath2|NONE|100 +Keyword = Soulsy_Shout_FireBreath|Spell|VoiceFireBreath3|NONE|100 +Keyword = Soulsy_Shout_FrostBreath|Spell|VoiceFrostBreath1|NONE|100 +Keyword = Soulsy_Shout_FrostBreath|Spell|VoiceFrostBreath2|NONE|100 +Keyword = Soulsy_Shout_FrostBreath|Spell|VoiceFrostBreath3|NONE|100 +Keyword = Soulsy_Shout_IceForm|Spell|VoiceIceForm1|NONE|100 +Keyword = Soulsy_Shout_IceForm|Spell|VoiceIceForm2|NONE|100 +Keyword = Soulsy_Shout_IceForm|Spell|VoiceIceForm3|NONE|100 +Keyword = Soulsy_Shout_KynesPeace|Spell|VoiceKynesPeace1|NONE|100 +Keyword = Soulsy_Shout_KynesPeace|Spell|VoiceKynesPeace2|NONE|100 +Keyword = Soulsy_Shout_KynesPeace|Spell|VoiceKynesPeace3|NONE|100 +Keyword = Soulsy_Shout_MarkedForDeath|Spell|VoiceMarkedforDeath1|NONE|100 +Keyword = Soulsy_Shout_MarkedForDeath|Spell|VoiceMarkedforDeath2|NONE|100 +Keyword = Soulsy_Shout_MarkedForDeath|Spell|VoiceMarkedforDeath3|NONE|100 +Keyword = Soulsy_Shout_SlowTime|Spell|VoiceSlowTime1|NONE|100 +Keyword = Soulsy_Shout_SlowTime|Spell|VoiceSlowTime2|NONE|100 +Keyword = Soulsy_Shout_SlowTime|Spell|VoiceSlowTime3|NONE|100 +Keyword = Soulsy_Shout_Stormcall|Spell|VoiceStormCall1|NONE|100 +Keyword = Soulsy_Shout_Stormcall|Spell|VoiceStormCall2|NONE|100 +Keyword = Soulsy_Shout_Stormcall|Spell|VoiceStormCall3|NONE|100 +Keyword = Soulsy_Shout_ThrowVoice|Spell|VoiceThrowVoice|NONE|100 +Keyword = Soulsy_Shout_UnrelentingForce|Spell|VoiceUnrelentingForce1|NONE|100 +Keyword = Soulsy_Shout_UnrelentingForce|Spell|VoiceUnrelentingForce2|NONE|100 +Keyword = Soulsy_Shout_UnrelentingForce|Spell|VoiceUnrelentingForce3|NONE|100 +Keyword = Soulsy_Shout_WhirlwindSprint|Spell|VoiceWhirlwindSprint1|NONE|100 +Keyword = Soulsy_Shout_WhirlwindSprint|Spell|VoiceWhirlwindSprint2|NONE|100 +Keyword = Soulsy_Shout_WhirlwindSprint|Spell|VoiceWhirlwindSprint3|NONE|100 + +; Dragonborn.esm +Keyword = Soulsy_Shout_DragonAspect|Spell|DLC2DragonAspectArmsSpell|NONE|100 +Keyword = Soulsy_Shout_DragonAspect|Spell|DLC2DragonAspectBodySpell|NONE|100 +Keyword = Soulsy_Shout_DragonAspect|Spell|DLC2DragonAspectHeadSpell|NONE|100 +Keyword = Soulsy_Shout_BattleFury|Spell|DLC2VoiceBattleFury01|NONE|100 +Keyword = Soulsy_Shout_BattleFury|Spell|DLC2VoiceBattleFury02|NONE|100 +Keyword = Soulsy_Shout_BattleFury|Spell|DLC2VoiceBattleFury03|NONE|100 +Keyword = Soulsy_Shout_BendWill|Spell|DLC2VoiceBendWill1|NONE|100 +Keyword = Soulsy_Shout_BendWill|Spell|DLC2VoiceBendWill2|NONE|100 +Keyword = Soulsy_Shout_BendWill|Spell|DLC2VoiceBendWill3|NONE|100 +Keyword = Soulsy_Shout_Cyclone|Spell|DLC2VoiceCyclone01|NONE|100 +Keyword = Soulsy_Shout_Cyclone|Spell|DLC2VoiceCyclone02|NONE|100 +Keyword = Soulsy_Shout_Cyclone|Spell|DLC2VoiceCyclone03|NONE|100 +Keyword = Soulsy_Shout_ElementalFury|Spell|DLC2VoiceElementalFury1|NONE|100 +Keyword = Soulsy_Shout_ElementalFury|Spell|DLC2VoiceElementalFury2|NONE|100 +Keyword = Soulsy_Shout_ElementalFury|Spell|DLC2VoiceElementalFury3|NONE|100 + +; Dawnguard.esm +Keyword = Soulsy_Shout_SummonDurnehviir|Spell|DLC1SummonDragon|NONE|100 +Keyword = Soulsy_Shout_DrainVitality|Spell|DLC1VoiceDrainVitality1|NONE|100 +Keyword = Soulsy_Shout_DrainVitality|Spell|DLC1VoiceDrainVitality2|NONE|100 +Keyword = Soulsy_Shout_DrainVitality|Spell|DLC1VoiceDrainVitality3|NONE|100 +Keyword = Soulsy_Shout_SoulTear|Spell|DLC1VoiceSoulTear1|NONE|100 +Keyword = Soulsy_Shout_SoulTear|Spell|DLC1VoiceSoulTear2|NONE|100 +Keyword = Soulsy_Shout_SoulTear|Spell|DLC1VoiceSoulTear3|NONE|100 +Keyword = Soulsy_Shout_SoulCairnSummon|Spell|DLC1VoiceUndeadSummon1|NONE|100 +Keyword = Soulsy_Shout_SoulCairnSummon|Spell|DLC1VoiceUndeadSummon2|NONE|100 +Keyword = Soulsy_Shout_SoulCairnSummon|Spell|DLC1VoiceUndeadSummon3|NONE|100 + +; ForcefulTongue.esp +Keyword = Soulsy_Shout_SummonDurnehviir|Spell|FT_DLC1SummonDurnehviir|NONE|100 +Keyword = Soulsy_Shout_DrainVitality|Spell|FT_DLC1VoiceDrainVitality01|NONE|100 +Keyword = Soulsy_Shout_DrainVitality|Spell|FT_DLC1VoiceDrainVitality02|NONE|100 +Keyword = Soulsy_Shout_DrainVitality|Spell|FT_DLC1VoiceDrainVitality03|NONE|100 +Keyword = Soulsy_Shout_SoulTear|Spell|FT_DLC1VoiceSoulTear1|NONE|100 +Keyword = Soulsy_Shout_SoulTear|Spell|FT_DLC1VoiceSoulTear2|NONE|100 +Keyword = Soulsy_Shout_SoulTear|Spell|FT_DLC1VoiceSoulTear3|NONE|100 +Keyword = Soulsy_Shout_BattleFury|Spell|FT_DLC2VoiceBattleFury01|NONE|100 +Keyword = Soulsy_Shout_BattleFury|Spell|FT_DLC2VoiceBattleFury02|NONE|100 +Keyword = Soulsy_Shout_BattleFury|Spell|FT_DLC2VoiceBattleFury03|NONE|100 +Keyword = Soulsy_Shout_BendWill|Spell|FT_DLC2VoiceBendWill01|NONE|100 +Keyword = Soulsy_Shout_BendWill|Spell|FT_DLC2VoiceBendWill02|NONE|100 +Keyword = Soulsy_Shout_BendWill|Spell|FT_DLC2VoiceBendWill03|NONE|100 +Keyword = Soulsy_Shout_Cyclone|Spell|FT_DLC2VoiceCyclone01|NONE|100 +Keyword = Soulsy_Shout_Cyclone|Spell|FT_DLC2VoiceCyclone02|NONE|100 +Keyword = Soulsy_Shout_Cyclone|Spell|FT_DLC2VoiceCyclone03|NONE|100 +Keyword = Soulsy_Shout_DragonAspect|Spell|FT_DLC2VoiceDragonAspectArmsSpell|NONE|100 +Keyword = Soulsy_Shout_DragonAspect|Spell|FT_DLC2VoiceDragonAspectBodySpell|NONE|100 +Keyword = Soulsy_Shout_DragonAspect|Spell|FT_DLC2VoiceDragonAspectCombatSpell|NONE|100 +Keyword = Soulsy_Shout_DragonAspect|Spell|FT_DLC2VoiceDragonAspectHeadSpell|NONE|100 +Keyword = Soulsy_Shout_DragonAspect|Spell|FT_DLC2VoiceDragonAspectSummon|NONE|100 +Keyword = Soulsy_Shout_ElementalFury|Spell|FT_DLC2VoiceElementalFury1|NONE|100 +Keyword = Soulsy_Shout_ElementalFury|Spell|FT_DLC2VoiceElementalFury2|NONE|100 +Keyword = Soulsy_Shout_ElementalFury|Spell|FT_DLC2VoiceElementalFury3|NONE|100 +Keyword = Soulsy_Shout_AnimalAllegiance|Spell|FT_VoiceAnimalAllegiance01|NONE|100 +Keyword = Soulsy_Shout_AnimalAllegiance|Spell|FT_VoiceAnimalAllegiance02|NONE|100 +Keyword = Soulsy_Shout_AnimalAllegiance|Spell|FT_VoiceAnimalAllegiance03|NONE|100 +Keyword = Soulsy_Shout_AuraWhisper|Spell|FT_VoiceAuraWhisper1|NONE|100 +Keyword = Soulsy_Shout_AuraWhisper|Spell|FT_VoiceAuraWhisper2|NONE|100 +Keyword = Soulsy_Shout_AuraWhisper|Spell|FT_VoiceAuraWhisper3|NONE|100 +Keyword = Soulsy_Shout_BecomeEthereal|Spell|FT_VoiceBecomeEthereal1|NONE|100 +Keyword = Soulsy_Shout_BecomeEthereal|Spell|FT_VoiceBecomeEthereal2|NONE|100 +Keyword = Soulsy_Shout_BecomeEthereal|Spell|FT_VoiceBecomeEthereal3|NONE|100 +Keyword = Soulsy_Shout_CallDragon|Spell|FT_VoiceCallDragon|NONE|100 +Keyword = Soulsy_Shout_CallDragon|Spell|FT_VoiceCallOdahviing|NONE|100 +Keyword = Soulsy_Shout_CallOfValor|Spell|FT_VoiceCallOfValor01|NONE|100 +Keyword = Soulsy_Shout_CallOfValor|Spell|FT_VoiceCallOfValor02|NONE|100 +Keyword = Soulsy_Shout_CallOfValor|Spell|FT_VoiceCallOfValor03|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|FT_VoiceClearSkies01|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|FT_VoiceClearSkies02|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|FT_VoiceClearSkies03|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|FT_VoiceClearSkiesSelf01|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|FT_VoiceClearSkiesSelf02|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|FT_VoiceClearSkiesSelf03|NONE|100 +Keyword = Soulsy_Shout_Disarm|Spell|FT_VoiceDisarm01|NONE|100 +Keyword = Soulsy_Shout_Disarm|Spell|FT_VoiceDisarm02|NONE|100 +Keyword = Soulsy_Shout_Disarm|Spell|FT_VoiceDisarm03|NONE|100 +Keyword = Soulsy_Shout_Dismay|Spell|FT_VoiceDismay01|NONE|100 +Keyword = Soulsy_Shout_Dismay|Spell|FT_VoiceDismay02|NONE|100 +Keyword = Soulsy_Shout_Dismay|Spell|FT_VoiceDismay03|NONE|100 +Keyword = Soulsy_Shout_Dragonrend|Spell|FT_VoiceDragonrend01|NONE|100 +Keyword = Soulsy_Shout_Dragonrend|Spell|FT_VoiceDragonrend02|NONE|100 +Keyword = Soulsy_Shout_Dragonrend|Spell|FT_VoiceDragonrend03|NONE|100 +Keyword = Soulsy_Shout_ElementalFury|Spell|FT_VoiceElementalFury1|NONE|100 +Keyword = Soulsy_Shout_ElementalFury|Spell|FT_VoiceElementalFury2|NONE|100 +Keyword = Soulsy_Shout_ElementalFury|Spell|FT_VoiceElementalFury3|NONE|100 +Keyword = Soulsy_Shout_FireBreath|Spell|FT_VoiceFireBreath01|NONE|100 +Keyword = Soulsy_Shout_FireBreath|Spell|FT_VoiceFireBreath02|NONE|100 +Keyword = Soulsy_Shout_FireBreath|Spell|FT_VoiceFireBreath03|NONE|100 +Keyword = Soulsy_Shout_FrostBreath|Spell|FT_VoiceFrostBreath1|NONE|100 +Keyword = Soulsy_Shout_FrostBreath|Spell|FT_VoiceFrostBreath2|NONE|100 +Keyword = Soulsy_Shout_FrostBreath|Spell|FT_VoiceFrostBreath3|NONE|100 +Keyword = Soulsy_Shout_IceForm|Spell|FT_VoiceIceForm01|NONE|100 +Keyword = Soulsy_Shout_IceForm|Spell|FT_VoiceIceForm02|NONE|100 +Keyword = Soulsy_Shout_IceForm|Spell|FT_VoiceIceForm03|NONE|100 +Keyword = Soulsy_Shout_KynesPeace|Spell|FT_VoiceKynesPeace01|NONE|100 +Keyword = Soulsy_Shout_KynesPeace|Spell|FT_VoiceKynesPeace02|NONE|100 +Keyword = Soulsy_Shout_KynesPeace|Spell|FT_VoiceKynesPeace03|NONE|100 +Keyword = Soulsy_Shout_MarkedForDeath|Spell|FT_VoiceMarkedForDeath1|NONE|100 +Keyword = Soulsy_Shout_MarkedForDeath|Spell|FT_VoiceMarkedForDeath2|NONE|100 +Keyword = Soulsy_Shout_MarkedForDeath|Spell|FT_VoiceMarkedForDeath3|NONE|100 +Keyword = Soulsy_Shout_PhantomForm|Spell|FT_VoicePhantomForm1|NONE|100 +Keyword = Soulsy_Shout_PhantomForm|Spell|FT_VoicePhantomForm2|NONE|100 +Keyword = Soulsy_Shout_PhantomForm|Spell|FT_VoicePhantomForm3|NONE|100 +Keyword = Soulsy_Shout_SlowTime|Spell|FT_VoiceSlowTime1|NONE|100 +Keyword = Soulsy_Shout_SlowTime|Spell|FT_VoiceSlowTime2|NONE|100 +Keyword = Soulsy_Shout_SlowTime|Spell|FT_VoiceSlowTime3|NONE|100 +Keyword = Soulsy_Shout_Stormcall|Spell|FT_VoiceStormCall01|NONE|100 +Keyword = Soulsy_Shout_Stormcall|Spell|FT_VoiceStormCall02|NONE|100 +Keyword = Soulsy_Shout_Stormcall|Spell|FT_VoiceStormCall03|NONE|100 +Keyword = Soulsy_Shout_Stormcall|Spell|FT_VoiceStormCallBolt01|NONE|100 +Keyword = Soulsy_Shout_Stormcall|Spell|FT_VoiceStormCallBolt02|NONE|100 +Keyword = Soulsy_Shout_Stormcall|Spell|FT_VoiceStormCallBolt03|NONE|100 +Keyword = Soulsy_Shout_ThrowVoice|Spell|FT_VoiceThrowVoice|NONE|100 +Keyword = Soulsy_Shout_UnrelentingForce|Spell|FT_VoiceUnrelentingForce01|NONE|100 +Keyword = Soulsy_Shout_UnrelentingForce|Spell|FT_VoiceUnrelentingForce02|NONE|100 +Keyword = Soulsy_Shout_UnrelentingForce|Spell|FT_VoiceUnrelentingForce03|NONE|100 +Keyword = Soulsy_Shout_WhirlwindSprint|Spell|FT_VoiceWhirlwindSprint1|NONE|100 +Keyword = Soulsy_Shout_WhirlwindSprint|Spell|FT_VoiceWhirlwindSprint2|NONE|100 +Keyword = Soulsy_Shout_WhirlwindSprint|Spell|FT_VoiceWhirlwindSprint3|NONE|100 + +; Stormcrown.esp +Keyword = Soulsy_Shout_StormCall|Spell|MAG_StormCallLightningBolt01|NONE|100 +Keyword = Soulsy_Shout_StormCall|Spell|MAG_StormCallLightningBolt02|NONE|100 +Keyword = Soulsy_Shout_StormCall|Spell|MAG_StormCallLightningBolt03|NONE|100 +Keyword = Soulsy_Shout_SummonDurnehviir|Spell|MAG_SummonDurnehviir01|NONE|100 +Keyword = Soulsy_Shout_AnimalAllegiance|Spell|MAG_VoiceAnimalAlly01|NONE|100 +Keyword = Soulsy_Shout_AnimalAllegiance|Spell|MAG_VoiceAnimalAlly02|NONE|100 +Keyword = Soulsy_Shout_AnimalAllegiance|Spell|MAG_VoiceAnimalAlly03|NONE|100 +Keyword = Soulsy_Shout_AuraWhisper|Spell|MAG_VoiceAuraWhisper01|NONE|100 +Keyword = Soulsy_Shout_AuraWhisper|Spell|MAG_VoiceAuraWhisper02|NONE|100 +Keyword = Soulsy_Shout_AuraWhisper|Spell|MAG_VoiceAuraWhisper03|NONE|100 +Keyword = Soulsy_Shout_BattleFury|Spell|MAG_VoiceBattleFury01|NONE|100 +Keyword = Soulsy_Shout_BattleFury|Spell|MAG_VoiceBattleFury02|NONE|100 +Keyword = Soulsy_Shout_BattleFury|Spell|MAG_VoiceBattleFury03|NONE|100 +Keyword = Soulsy_Shout_BecomeEthereal|Spell|MAG_VoiceBecomeEthereal01|NONE|100 +Keyword = Soulsy_Shout_BecomeEthereal|Spell|MAG_VoiceBecomeEthereal02|NONE|100 +Keyword = Soulsy_Shout_BecomeEthereal|Spell|MAG_VoiceBecomeEthereal03|NONE|100 +Keyword = Soulsy_Shout_BendWill|Spell|MAG_VoiceBendWill01|NONE|100 +Keyword = Soulsy_Shout_BendWill|Spell|MAG_VoiceBendWill02|NONE|100 +Keyword = Soulsy_Shout_BendWill|Spell|MAG_VoiceBendWill03|NONE|100 +Keyword = Soulsy_Shout_CallDragon|Spell|MAG_VoiceCallDragon01|NONE|100 +Keyword = Soulsy_Shout_CallDragon|Spell|MAG_VoiceCallDragon03|NONE|100 +Keyword = Soulsy_Shout_CallOfValor|Spell|MAG_VoiceCallOfValor01|NONE|100 +Keyword = Soulsy_Shout_CallOfValor|Spell|MAG_VoiceCallOfValor02|NONE|100 +Keyword = Soulsy_Shout_CallOfValor|Spell|MAG_VoiceCallOfValor03|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|MAG_VoiceClearSkies01|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|MAG_VoiceClearSkies02|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|MAG_VoiceClearSkies03|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|MAG_VoiceClearSkiesSelf01|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|MAG_VoiceClearSkiesSelf02|NONE|100 +Keyword = Soulsy_Shout_ClearSkies|Spell|MAG_VoiceClearSkiesSelf03|NONE|100 +Keyword = Soulsy_Shout_Cyclone|Spell|MAG_VoiceCyclone01|NONE|100 +Keyword = Soulsy_Shout_Cyclone|Spell|MAG_VoiceCyclone02|NONE|100 +Keyword = Soulsy_Shout_Cyclone|Spell|MAG_VoiceCyclone03|NONE|100 +Keyword = Soulsy_Shout_Dismay|Spell|MAG_VoiceDismayShout01|NONE|100 +Keyword = Soulsy_Shout_Dismay|Spell|MAG_VoiceDismayShout02|NONE|100 +Keyword = Soulsy_Shout_Dismay|Spell|MAG_VoiceDismayShout03|NONE|100 +Keyword = Soulsy_Shout_DragonAspect|Spell|MAG_VoiceDragonAspect01|NONE|100 +Keyword = Soulsy_Shout_DragonAspect|Spell|MAG_VoiceDragonAspect02|NONE|100 +Keyword = Soulsy_Shout_DragonAspect|Spell|MAG_VoiceDragonAspect03|NONE|100 +Keyword = Soulsy_Shout_Dragonrend|Spell|MAG_VoiceDragonrend01|NONE|100 +Keyword = Soulsy_Shout_Dragonrend|Spell|MAG_VoiceDragonrend02|NONE|100 +Keyword = Soulsy_Shout_Dragonrend|Spell|MAG_VoiceDragonrend03|NONE|100 +Keyword = Soulsy_Shout_DrainVitality|Spell|MAG_VoiceDrainVitality01|NONE|100 +Keyword = Soulsy_Shout_DrainVitality|Spell|MAG_VoiceDrainVitality02|NONE|100 +Keyword = Soulsy_Shout_DrainVitality|Spell|MAG_VoiceDrainVitality03|NONE|100 +Keyword = Soulsy_Shout_ElementalFury|Spell|MAG_VoiceElementalFury01|NONE|100 +Keyword = Soulsy_Shout_ElementalFury|Spell|MAG_VoiceElementalFury02|NONE|100 +Keyword = Soulsy_Shout_ElementalFury|Spell|MAG_VoiceElementalFury03|NONE|100 +Keyword = Soulsy_Shout_FireBreath|Spell|MAG_VoiceFireBreath01|NONE|100 +Keyword = Soulsy_Shout_FireBreath|Spell|MAG_VoiceFireBreath02|NONE|100 +Keyword = Soulsy_Shout_FireBreath|Spell|MAG_VoiceFireBreath03|NONE|100 +Keyword = Soulsy_Shout_FrostBreath|Spell|MAG_VoiceFrostBreath01|NONE|100 +Keyword = Soulsy_Shout_FrostBreath|Spell|MAG_VoiceFrostBreath02|NONE|100 +Keyword = Soulsy_Shout_FrostBreath|Spell|MAG_VoiceFrostBreath03|NONE|100 +Keyword = Soulsy_Shout_FrostBreath|Spell|MAG_VoiceFrostSlowSpell|NONE|100 +Keyword = Soulsy_Shout_IceForm|Spell|MAG_VoiceIceForm01|NONE|100 +Keyword = Soulsy_Shout_IceForm|Spell|MAG_VoiceIceForm02|NONE|100 +Keyword = Soulsy_Shout_IceForm|Spell|MAG_VoiceIceForm03|NONE|100 +Keyword = Soulsy_Shout_IceForm|Spell|MAG_VoiceIceFormSlowSpell|NONE|100 +Keyword = Soulsy_Shout_KynesPeace|Spell|MAG_VoiceKynesPeace01|NONE|100 +Keyword = Soulsy_Shout_KynesPeace|Spell|MAG_VoiceKynesPeace02|NONE|100 +Keyword = Soulsy_Shout_KynesPeace|Spell|MAG_VoiceKynesPeace03|NONE|100 +Keyword = Soulsy_Shout_LightningBreath|Spell|MAG_VoiceLightningBreath01|NONE|100 +Keyword = Soulsy_Shout_LightningBreath|Spell|MAG_VoiceLightningBreath02|NONE|100 +Keyword = Soulsy_Shout_LightningBreath|Spell|MAG_VoiceLightningBreath03|NONE|100 +Keyword = Soulsy_Shout_MarkedForDeath|Spell|MAG_VoiceMarkedForDeath01|NONE|100 +Keyword = Soulsy_Shout_MarkedForDeath|Spell|MAG_VoiceMarkedForDeath02|NONE|100 +Keyword = Soulsy_Shout_MarkedForDeath|Spell|MAG_VoiceMarkedForDeath03|NONE|100 +Keyword = Soulsy_Shout_PoisonBreath|Spell|MAG_VoicePoisonBreath01|NONE|100 +Keyword = Soulsy_Shout_PoisonBreath|Spell|MAG_VoicePoisonBreath02|NONE|100 +Keyword = Soulsy_Shout_PoisonBreath|Spell|MAG_VoicePoisonBreath03|NONE|100 +Keyword = Soulsy_Shout_SlowTime|Spell|MAG_VoiceSlowTime01|NONE|100 +Keyword = Soulsy_Shout_SlowTime|Spell|MAG_VoiceSlowTime02|NONE|100 +Keyword = Soulsy_Shout_SlowTime|Spell|MAG_VoiceSlowTime03|NONE|100 +Keyword = Soulsy_Shout_SoulTear|Spell|MAG_VoiceSoulTear01|NONE|100 +Keyword = Soulsy_Shout_SoulTear|Spell|MAG_VoiceSoulTear02|NONE|100 +Keyword = Soulsy_Shout_SoulTear|Spell|MAG_VoiceSoulTear03|NONE|100 +Keyword = Soulsy_Shout_SoulTear|Spell|MAG_VoiceSoulTearReanimateSpell|NONE|100 +Keyword = Soulsy_Shout_Stormcall|Spell|MAG_VoiceStormCall01|NONE|100 +Keyword = Soulsy_Shout_Stormcall|Spell|MAG_VoiceStormCall02|NONE|100 +Keyword = Soulsy_Shout_Stormcall|Spell|MAG_VoiceStormCall03|NONE|100 +Keyword = Soulsy_Shout_Disarm|Spell|MAG_VoiceSubdue01|NONE|100 +Keyword = Soulsy_Shout_Disarm|Spell|MAG_VoiceSubdue02|NONE|100 +Keyword = Soulsy_Shout_Disarm|Spell|MAG_VoiceSubdue03|NONE|100 +Keyword = Soulsy_Shout_ThrowVoice|Spell|MAG_VoiceThrowVoice|NONE|100 +Keyword = Soulsy_Shout_UnrelentingForce|Spell|MAG_VoiceUnrelentingForce01|NONE|100 +Keyword = Soulsy_Shout_UnrelentingForce|Spell|MAG_VoiceUnrelentingForce02|NONE|100 +Keyword = Soulsy_Shout_UnrelentingForce|Spell|MAG_VoiceUnrelentingForce03|NONE|100 +Keyword = Soulsy_Shout_WhirlwindSprint|Spell|MAG_VoiceWhirlwindSprint01|NONE|100 +Keyword = Soulsy_Shout_WhirlwindSprint|Spell|MAG_VoiceWhirlwindSprint02|NONE|100 +Keyword = Soulsy_Shout_WhirlwindSprint|Spell|MAG_VoiceWhirlwindSprint03|NONE|100 + +; Thunderchild - Epic Shout Package.esp +Keyword = Soulsy_Shout_AlessiasLove|Spell|TC_AlessiasLove_Spell1|NONE|100 +Keyword = Soulsy_Shout_AlessiasLove|Spell|TC_AlessiasLove_Spell2|NONE|100 +Keyword = Soulsy_Shout_AlessiasLove|Spell|TC_AlessiasLove_Spell3|NONE|100 +Keyword = Soulsy_Shout_Annihilate|Spell|TC_Annihilate_Spell1|NONE|100 +Keyword = Soulsy_Shout_Annihilate|Spell|TC_Annihilate_Spell1_Blast|NONE|100 +Keyword = Soulsy_Shout_Annihilate|Spell|TC_Annihilate_Spell1_Bolt|NONE|100 +Keyword = Soulsy_Shout_Annihilate|Spell|TC_Annihilate_Spell2|NONE|100 +Keyword = Soulsy_Shout_Annihilate|Spell|TC_Annihilate_Spell2Old_Bolt|NONE|100 +Keyword = Soulsy_Shout_Annihilate|Spell|TC_Annihilate_Spell2_Blast|NONE|100 +Keyword = Soulsy_Shout_Annihilate|Spell|TC_Annihilate_Spell2_Bolt|NONE|100 +Keyword = Soulsy_Shout_Annihilate|Spell|TC_Annihilate_Spell3|NONE|100 +Keyword = Soulsy_Shout_ArcaneHelix|Spell|TC_ArcaneHelix_Spell1|NONE|100 +Keyword = Soulsy_Shout_ArcaneHelix|Spell|TC_ArcaneHelix_Spell2|NONE|100 +Keyword = Soulsy_Shout_ArcaneHelix|Spell|TC_ArcaneHelix_Spell3|NONE|100 +Keyword = Soulsy_Shout_Armageddon|Spell|TC_Armageddon_Spell1|NONE|100 +Keyword = Soulsy_Shout_Armageddon|Spell|TC_Armageddon_Spell1_Bolt|NONE|100 +Keyword = Soulsy_Shout_Armageddon|Spell|TC_Armageddon_Spell2|NONE|100 +Keyword = Soulsy_Shout_Armageddon|Spell|TC_Armageddon_Spell2_Bolt|NONE|100 +Keyword = Soulsy_Shout_Armageddon|Spell|TC_Armageddon_Spell3|NONE|100 +Keyword = Soulsy_Shout_Armageddon|Spell|TC_Armageddon_Spell3_Bolt|NONE|100 +Keyword = Soulsy_Shout_Curse|Spell|TC_Curse_Spell1|NONE|100 +Keyword = Soulsy_Shout_Curse|Spell|TC_Curse_Spell2|NONE|100 +Keyword = Soulsy_Shout_Curse|Spell|TC_Curse_Spell3|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell1|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell1_DamageProc0|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell1_DamageProc1|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell1_DamageProc2|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell1_DamageProc3|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell1_DamageProc4|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell2|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell2_DamageProc0|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell2_DamageProc1|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell2_DamageProc2|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell2_DamageProc3|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell2_DamageProc4|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell2_Subspell|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell2_TimeStop|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell3|NONE|100 +Keyword = Soulsy_Shout_DanceOfTheDead|Spell|TC_DanceOfTheDead_Spell3_Nova|NONE|100 +Keyword = Soulsy_Shout_Earthquake|Spell|TC_Earthquake_Spell1|NONE|100 +Keyword = Soulsy_Shout_Earthquake|Spell|TC_Earthquake_Spell1_Bolt|NONE|100 +Keyword = Soulsy_Shout_Earthquake|Spell|TC_Earthquake_Spell1_VoiceBlast|NONE|100 +Keyword = Soulsy_Shout_Earthquake|Spell|TC_Earthquake_Spell2|NONE|100 +Keyword = Soulsy_Shout_Earthquake|Spell|TC_Earthquake_Spell2_Aoe|NONE|100 +Keyword = Soulsy_Shout_Earthquake|Spell|TC_Earthquake_Spell3|NONE|100 +Keyword = Soulsy_Shout_EssenceRip|Spell|TC_EssenceRip_Spell1|NONE|100 +Keyword = Soulsy_Shout_EssenceRip|Spell|TC_EssenceRip_Spell1_Blast|NONE|100 +Keyword = Soulsy_Shout_EssenceRip|Spell|TC_EssenceRip_Spell1_Bolt|NONE|100 +Keyword = Soulsy_Shout_EssenceRip|Spell|TC_EssenceRip_Spell2|NONE|100 +Keyword = Soulsy_Shout_EssenceRip|Spell|TC_EssenceRip_Spell2_Blast|NONE|100 +Keyword = Soulsy_Shout_EssenceRip|Spell|TC_EssenceRip_Spell2_Bolt|NONE|100 +Keyword = Soulsy_Shout_EssenceRip|Spell|TC_EssenceRip_Spell3|NONE|100 +Keyword = Soulsy_Shout_EssenceRip|Spell|TC_EssenceRip_Spell3_Blast|NONE|100 +Keyword = Soulsy_Shout_EssenceRip|Spell|TC_EssenceRip_Spell3_Bolt|NONE|100 +Keyword = Soulsy_Shout_Evocation|Spell|TC_Evocation_Spell1|NONE|100 +Keyword = Soulsy_Shout_Evocation|Spell|TC_Evocation_Spell1_Blast|NONE|100 +Keyword = Soulsy_Shout_Evocation|Spell|TC_Evocation_Spell2|NONE|100 +Keyword = Soulsy_Shout_Evocation|Spell|TC_Evocation_Spell2_Left|NONE|100 +Keyword = Soulsy_Shout_Evocation|Spell|TC_Evocation_Spell2_Right|NONE|100 +Keyword = Soulsy_Shout_Evocation|Spell|TC_Evocation_Spell3|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell1|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell2|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell3|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell3_DamageProc0|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell3_DamageProc1|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell3_DamageProc2|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell3_DamageProc3|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell3_DamageProc4|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell3_DamageProc5|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell3_DamageProc6|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell3_DamageProc7|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell3_DamageProc8|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell3_DamageProc9|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell3_DetonateMarker|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell3_SpammedSpell|NONE|100 +Keyword = Soulsy_Shout_Iceborn|Spell|TC_Iceborn_Spell3_Timefreeze|NONE|100 +Keyword = Soulsy_Shout_JonesShadow|Spell|TC_JonesShadow_Spell1|NONE|100 +Keyword = Soulsy_Shout_JonesShadow|Spell|TC_JonesShadow_Spell2|NONE|100 +Keyword = Soulsy_Shout_JonesShadow|Spell|TC_JonesShadow_Spell3|NONE|100 +Keyword = Soulsy_Shout_Kingsbane|Spell|TC_Kingsbane_Spell1|NONE|100 +Keyword = Soulsy_Shout_Kingsbane|Spell|TC_Kingsbane_Spell2|NONE|100 +Keyword = Soulsy_Shout_Kingsbane|Spell|TC_Kingsbane_Spell3|NONE|100 +Keyword = Soulsy_Shout_Kingsbane|Spell|TC_Kingsbane_Spell3_ArcBolt_Left|NONE|100 +Keyword = Soulsy_Shout_Kingsbane|Spell|TC_Kingsbane_Spell3_ArcBolt_Right|NONE|100 +Keyword = Soulsy_Shout_Kingsbane|Spell|TC_Kingsbane_Spell3_SpammedSpell|NONE|100 +Keyword = Soulsy_Shout_Kingsbane|Spell|TC_Kingsbane_Spell3_Timefreeze|NONE|100 +Keyword = Soulsy_Shout_Lifestream|Spell|TC_Lifestream_Spell1|NONE|100 +Keyword = Soulsy_Shout_Lifestream|Spell|TC_Lifestream_Spell2|NONE|100 +Keyword = Soulsy_Shout_Lifestream|Spell|TC_Lifestream_Spell3|NONE|100 +Keyword = Soulsy_Shout_Lifestream|Spell|TC_Lifestream_Spell3_Proc|NONE|100 +Keyword = Soulsy_Shout_LightningShield|Spell|TC_LightningShield_Spell1|NONE|100 +Keyword = Soulsy_Shout_LightningShield|Spell|TC_LightningShield_Spell1_Shock|NONE|100 +Keyword = Soulsy_Shout_LightningShield|Spell|TC_LightningShield_Spell2|NONE|100 +Keyword = Soulsy_Shout_LightningShield|Spell|TC_LightningShield_Spell2_DispersionWave|NONE|100 +Keyword = Soulsy_Shout_LightningShield|Spell|TC_LightningShield_Spell2_Shock|NONE|100 +Keyword = Soulsy_Shout_LightningShield|Spell|TC_LightningShield_Spell3|NONE|100 +Keyword = Soulsy_Shout_Geomagnetism|Spell|TC_MagnetPull_Spell1|NONE|100 +Keyword = Soulsy_Shout_Geomagnetism|Spell|TC_MagnetPull_Spell1_Blast|NONE|100 +Keyword = Soulsy_Shout_Geomagnetism|Spell|TC_MagnetPull_Spell1_Bolt|NONE|100 +Keyword = Soulsy_Shout_Geomagnetism|Spell|TC_MagnetPull_Spell2|NONE|100 +Keyword = Soulsy_Shout_Geomagnetism|Spell|TC_MagnetPull_Spell2_Blast|NONE|100 +Keyword = Soulsy_Shout_Geomagnetism|Spell|TC_MagnetPull_Spell2_Bolt|NONE|100 +Keyword = Soulsy_Shout_Geomagnetism|Spell|TC_MagnetPull_Spell3|NONE|100 +Keyword = Soulsy_Shout_Geomagnetism|Spell|TC_MagnetPull_Spell3_Bolt|NONE|100 +Keyword = Soulsy_Shout_Geomagnetism|Spell|TC_MagnetPull_Spell3_SpammedSpell|NONE|100 +Keyword = Soulsy_Shout_Geomagnetism|Spell|TC_MagnetPull_Spell3_VoiceBlast|NONE|100 +Keyword = Soulsy_Shout_Oblivion|Spell|TC_Oblivion_Spell1|NONE|100 +Keyword = Soulsy_Shout_Oblivion|Spell|TC_Oblivion_Spell2|NONE|100 +Keyword = Soulsy_Shout_Oblivion|Spell|TC_Oblivion_Spell3|NONE|100 +Keyword = Soulsy_Shout_Oblivion|Spell|TC_Oblivion_Spell3_Bolt|NONE|100 +Keyword = Soulsy_Shout_PhantomDecoy|Spell|TC_PhantomDecoy_Spell1|NONE|100 +Keyword = Soulsy_Shout_PhantomDecoy|Spell|TC_PhantomDecoy_Spell1_Ab|NONE|100 +Keyword = Soulsy_Shout_PhantomDecoy|Spell|TC_PhantomDecoy_Spell2|NONE|100 +Keyword = Soulsy_Shout_PhantomDecoy|Spell|TC_PhantomDecoy_Spell2_Ab|NONE|100 +Keyword = Soulsy_Shout_PhantomDecoy|Spell|TC_PhantomDecoy_Spell3|NONE|100 +Keyword = Soulsy_Shout_PhantomDecoy|Spell|TC_PhantomDecoy_Spell3_Blast1|NONE|100 +Keyword = Soulsy_Shout_PhantomDecoy|Spell|TC_PhantomDecoy_Spell3_Blast2|NONE|100 +Keyword = Soulsy_Shout_Riftwalk|Spell|TC_Riftwalk_Spell1|NONE|100 +Keyword = Soulsy_Shout_Riftwalk|Spell|TC_Riftwalk_Spell1_Ability|NONE|100 +Keyword = Soulsy_Shout_Riftwalk|Spell|TC_Riftwalk_Spell1_VoiceBlast|NONE|100 +Keyword = Soulsy_Shout_Riftwalk|Spell|TC_Riftwalk_Spell2|NONE|100 +Keyword = Soulsy_Shout_Riftwalk|Spell|TC_Riftwalk_Spell2_Bolt|NONE|100 +Keyword = Soulsy_Shout_Riftwalk|Spell|TC_Riftwalk_Spell2_VoiceBlast|NONE|100 +Keyword = Soulsy_Shout_Riftwalk|Spell|TC_Riftwalk_Spell3|NONE|100 +Keyword = Soulsy_Shout_Shattersphere|Spell|TC_Shattersphere_Blast1|NONE|100 +Keyword = Soulsy_Shout_Shattersphere|Spell|TC_Shattersphere_Blast2|NONE|100 +Keyword = Soulsy_Shout_Shattersphere|Spell|TC_Shattersphere_Blast3|NONE|100 +Keyword = Soulsy_Shout_Shattersphere|Spell|TC_Shattersphere_Bolt1|NONE|100 +Keyword = Soulsy_Shout_Shattersphere|Spell|TC_Shattersphere_Bolt2|NONE|100 +Keyword = Soulsy_Shout_Shattersphere|Spell|TC_Shattersphere_Bolt3|NONE|100 +Keyword = Soulsy_Shout_Shattersphere|Spell|TC_Shattersphere_Spell1|NONE|100 +Keyword = Soulsy_Shout_Shattersphere|Spell|TC_Shattersphere_Spell2|NONE|100 +Keyword = Soulsy_Shout_Shattersphere|Spell|TC_Shattersphere_Spell3|NONE|100 +Keyword = Soulsy_Shout_Shattersphere|Spell|TC_Shattersphere_Spell3New_Proc|NONE|100 +Keyword = Soulsy_Shout_ShorsWrath|Spell|TC_ShorsWrath_Spell1|NONE|100 +Keyword = Soulsy_Shout_ShorsWrath|Spell|TC_ShorsWrath_Spell2|NONE|100 +Keyword = Soulsy_Shout_ShorsWrath|Spell|TC_ShorsWrath_Spell3|NONE|100 +Keyword = Soulsy_Shout_ShorsWrath|Spell|TC_ShorsWrath_Spell3_Hero|NONE|100 +Keyword = Soulsy_Shout_ShroudOfSnowfall|Spell|TC_ShroudOfSnowfall_Spell1|NONE|100 +Keyword = Soulsy_Shout_ShroudOfSnowfall|Spell|TC_ShroudOfSnowfall_Spell1_CasterIsInStorm|NONE|100 +Keyword = Soulsy_Shout_ShroudOfSnowfall|Spell|TC_ShroudOfSnowfall_Spell1_Stealth|NONE|100 +Keyword = Soulsy_Shout_ShroudOfSnowfall|Spell|TC_ShroudOfSnowfall_Spell1_VoiceBlast|NONE|100 +Keyword = Soulsy_Shout_ShroudOfSnowfall|Spell|TC_ShroudOfSnowfall_Spell2|NONE|100 +Keyword = Soulsy_Shout_ShroudOfSnowfall|Spell|TC_ShroudOfSnowfall_Spell2_CasterIsInStorm|NONE|100 +Keyword = Soulsy_Shout_ShroudOfSnowfall|Spell|TC_ShroudOfSnowfall_Spell2_Stealth|NONE|100 +Keyword = Soulsy_Shout_ShroudOfSnowfall|Spell|TC_ShroudOfSnowfall_Spell2_VoiceBlast|NONE|100 +Keyword = Soulsy_Shout_ShroudOfSnowfall|Spell|TC_ShroudOfSnowfall_Spell3|NONE|100 +Keyword = Soulsy_Shout_ShroudOfSnowfall|Spell|TC_ShroudOfSnowfall_Spell3_CasterIsInStorm|NONE|100 +Keyword = Soulsy_Shout_ShroudOfSnowfall|Spell|TC_ShroudOfSnowfall_Spell3_Stealth|NONE|100 +Keyword = Soulsy_Shout_ShroudOfSnowfall|Spell|TC_ShroudOfSnowfall_Spell3_VoiceBlast|NONE|100 +Keyword = Soulsy_Shout_SpeakUntoTheStars|Spell|TC_SpeakUntoTheStars_Spell1|NONE|100 +Keyword = Soulsy_Shout_SpeakUntoTheStars|Spell|TC_SpeakUntoTheStars_Spell2|NONE|100 +Keyword = Soulsy_Shout_SpeakUntoTheStars|Spell|TC_SpeakUntoTheStars_Spell2_Projectile|NONE|100 +Keyword = Soulsy_Shout_SpeakUntoTheStars|Spell|TC_SpeakUntoTheStars_Spell3|NONE|100 +Keyword = Soulsy_Shout_SpeakUntoTheStars|Spell|TC_SpeakUntoTheStars_Spell3_Projectile|NONE|100 +Keyword = Soulsy_Shout_SplinterTwins|Spell|TC_SplinterTwins_Spell1|NONE|100 +Keyword = Soulsy_Shout_SplinterTwins|Spell|TC_SplinterTwins_Spell1_Ab|NONE|100 +Keyword = Soulsy_Shout_SplinterTwins|Spell|TC_SplinterTwins_Spell2|NONE|100 +Keyword = Soulsy_Shout_SplinterTwins|Spell|TC_SplinterTwins_Spell2_Ab|NONE|100 +Keyword = Soulsy_Shout_SplinterTwins|Spell|TC_SplinterTwins_Spell3|NONE|100 +Keyword = Soulsy_Shout_SplinterTwins|Spell|TC_SplinterTwins_Spell3_Ab|NONE|100 +Keyword = Soulsy_Shout_Stormblast|Spell|TC_Stormblast_Spell1|NONE|100 +Keyword = Soulsy_Shout_Stormblast|Spell|TC_Stormblast_Spell1_Bolt|NONE|100 +Keyword = Soulsy_Shout_Stormblast|Spell|TC_Stormblast_Spell1_Touch|NONE|100 +Keyword = Soulsy_Shout_Stormblast|Spell|TC_Stormblast_Spell2|NONE|100 +Keyword = Soulsy_Shout_Stormblast|Spell|TC_Stormblast_Spell2_Bolt|NONE|100 +Keyword = Soulsy_Shout_Stormblast|Spell|TC_Stormblast_Spell2_Touch|NONE|100 +Keyword = Soulsy_Shout_Stormblast|Spell|TC_Stormblast_Spell3|NONE|100 +Keyword = Soulsy_Shout_Stormblast|Spell|TC_Stormblast_Spell3_Touch|NONE|100 +Keyword = Soulsy_Shout_TheConqueror|Spell|TC_TheConqueror_Spell1|NONE|100 +Keyword = Soulsy_Shout_TheConqueror|Spell|TC_TheConqueror_Spell2|NONE|100 +Keyword = Soulsy_Shout_TheConqueror|Spell|TC_TheConqueror_Spell3|NONE|100 +Keyword = Soulsy_Shout_TheConqueror|Spell|TC_TheConqueror_Spell3_Subspell|NONE|100 +Keyword = Soulsy_Shout_Trueshot|Spell|TC_Trueshot_Spell1|NONE|100 +Keyword = Soulsy_Shout_Trueshot|Spell|TC_Trueshot_Spell1_Touch|NONE|100 +Keyword = Soulsy_Shout_Trueshot|Spell|TC_Trueshot_Spell2|NONE|100 +Keyword = Soulsy_Shout_Trueshot|Spell|TC_Trueshot_Spell2_Touch|NONE|100 +Keyword = Soulsy_Shout_Trueshot|Spell|TC_Trueshot_Spell3|NONE|100 +Keyword = Soulsy_Shout_Trueshot|Spell|TC_Trueshot_Spell3_Bolt|NONE|100 +Keyword = Soulsy_Shout_Trueshot|Spell|TC_Trueshot_Spell3_Touch|NONE|100 +Keyword = Soulsy_Shout_WailOfTheBanshee|Spell|TC_WailOfTheBanshee_Spell1|NONE|100 +Keyword = Soulsy_Shout_WailOfTheBanshee|Spell|TC_WailOfTheBanshee_Spell2|NONE|100 +Keyword = Soulsy_Shout_WailOfTheBanshee|Spell|TC_WailOfTheBanshee_Spell3|NONE|100 +Keyword = Soulsy_Shout_Wanderlust|Spell|TC_Wanderlust_Spell1|NONE|100 +Keyword = Soulsy_Shout_Wanderlust|Spell|TC_Wanderlust_Spell2|NONE|100 +Keyword = Soulsy_Shout_Wanderlust|Spell|TC_Wanderlust_Spell3|NONE|100 +Keyword = Soulsy_Shout_Warcry|Spell|TC_Warcry_Spell1|NONE|100 +Keyword = Soulsy_Shout_Warcry|Spell|TC_Warcry_Spell2|NONE|100 +Keyword = Soulsy_Shout_Warcry|Spell|TC_Warcry_Spell3|NONE|100 diff --git a/src/controller/control.rs b/src/controller/control.rs index 69207bf4..ec82f063 100644 --- a/src/controller/control.rs +++ b/src/controller/control.rs @@ -772,7 +772,8 @@ impl Controller { consumePotion(&form_spec); } else if item.is_armor() { cxx::let_cxx_string!(form_spec = item.form_string()); - toggleArmor(&form_spec); + cxx::let_cxx_string!(name = item.name()); + toggleArmor(&form_spec, &name); } else if item.is_ammo() { cxx::let_cxx_string!(form_spec = item.form_string()); equipAmmo(&form_spec) @@ -924,12 +925,13 @@ impl Controller { let kind = item.kind(); cxx::let_cxx_string!(form_spec = item.form_string()); + cxx::let_cxx_string!(name = item.name()); log::info!("about to equip this item: slot={:?}; {}", which, item); if kind.is_magic() || kind.left_hand_ok() || kind.right_hand_ok() { - equipWeapon(&form_spec, which); + equipWeapon(&form_spec, which, &name); } else if kind.is_armor() { - toggleArmor(&form_spec); + toggleArmor(&form_spec, &name); } else if matches!(kind, BaseType::Ammo(_)) { equipAmmo(&form_spec); } else { @@ -981,7 +983,7 @@ impl Controller { if !self.left_hand_cached.is_empty() { let unarmed = HudItem::make_unarmed_proxy(); let prev_left = self.left_hand_cached.clone(); - log::debug!( + log::trace!( "re-requipping what we previously had in the LEFT hand; spec={};", prev_left ); @@ -992,14 +994,16 @@ impl Controller { let item = self.cache.get(&prev_left); self.update_slot(HudElement::Left, &item); cxx::let_cxx_string!(form_spec = prev_left.clone()); - reequipHand(Action::Left, &form_spec); + cxx::let_cxx_string!(name = item.name()); + reequipHand(Action::Left, &form_spec, &name); } } else if let Some(left_next) = self.cycles.get_top(&CycleSlot::Left) { let item = self.cache.get(&left_next); self.left_hand_cached = left_next.clone(); self.update_slot(HudElement::Left, &item); cxx::let_cxx_string!(form_spec = left_next); - reequipHand(Action::Left, &form_spec); + cxx::let_cxx_string!(name = item.name()); + reequipHand(Action::Left, &form_spec, &name); } } @@ -1130,7 +1134,7 @@ impl Controller { let treat_as_two_hander = self.treat_as_two_handed(&item); let switching = item.two_handed() != self.two_hander_equipped; - log::debug!("weapon grip normally={}; alt-grip={}; we are treating it like: 2-hander={treat_as_two_hander}; switching={switching};", + log::trace!("weapon grip normally={}; alt-grip={}; we are treating it like: 2-hander={treat_as_two_hander}; switching={switching};", item.two_handed(), self.cgo_alt_grip); self.two_hander_equipped = item.two_handed(); @@ -1212,13 +1216,15 @@ impl Controller { let item = self.cache.get(&prev_right); self.update_slot(HudElement::Right, &item); cxx::let_cxx_string!(form_spec = prev_right); - reequipHand(Action::Right, &form_spec); + cxx::let_cxx_string!(name = item.name()); + reequipHand(Action::Right, &form_spec, &name); } } else if let Some(right_next) = self.cycles.get_top(&CycleSlot::Right) { self.right_hand_cached = right_next.clone(); let item = self.cache.get(&right_next); cxx::let_cxx_string!(form_spec = right_next); - reequipHand(Action::Right, &form_spec); + cxx::let_cxx_string!(name = item.name()); + reequipHand(Action::Right, &form_spec, &name); self.update_slot(HudElement::Right, &item); } } @@ -1286,7 +1292,7 @@ impl Controller { } log::info!( - "HUD updated. Now showing: power='{}'; left='{}'; right='{}'; ammo='{}';", + "HUD initialized. Now showing: power='{}'; left='{}'; right='{}'; ammo='{}';", power.name(), left_entry.name(), right_entry.name(), @@ -1645,8 +1651,10 @@ impl Controller { }); } equipset.items().iter().for_each(|item| { + let cached = self.cache.get(item); let_cxx_string!(form_spec = item.identifier()); - equipArmor(&form_spec); + let_cxx_string!(name = cached.name()); + equipArmor(&form_spec, &name); }); let set = HudItem::for_equip_set(equipset.name(), equipset.id(), equipset.icon.clone()); diff --git a/src/controller/cycles.rs b/src/controller/cycles.rs index b119499c..91aaaacf 100644 --- a/src/controller/cycles.rs +++ b/src/controller/cycles.rs @@ -461,7 +461,7 @@ pub mod cosave_v2 { match bincode::decode_from_slice::(&bytes[..], config) { Ok((value, _len)) => { - log::info!("Cycles successfully read from cosave data version {VERSION}."); + log::info!("Cycles successfully read from cosave data version {VERSION}. Save data was {} bytes.", bytes.len()); Some(value.into()) } Err(e) => { diff --git a/src/controller/facade.rs b/src/controller/facade.rs index df9f8dfd..bfc0401d 100644 --- a/src/controller/facade.rs +++ b/src/controller/facade.rs @@ -229,3 +229,41 @@ pub fn look_up_equipset_by_name(name: String) -> u32 { pub fn show_ui() -> bool { control::get().cycles.hud_visible() } + +// ----------- windows character shenanigans + +use textcode::iso8859_15; + +/// C++ calls this version. +pub fn string_to_utf8(bytes_ffi: &CxxVector) -> String { + let bytes: Vec = bytes_ffi.iter().copied().collect(); + convert_to_utf8_doggedly(bytes) +} + +// To test in game: install daegon +// player.additem 4c2b15f4 1 +// Sacrÿfev Tëliimi + +/// Get a valid Rust representation of this Windows codepage string data by hook or by crook. +pub fn convert_to_utf8_doggedly(input: Vec) -> String { + let bytes = if input.ends_with(&[0]) { + let chopped = input.len() - 1; + let mut tmp = input.clone(); + tmp.truncate(chopped); + tmp + } else { + input.clone() + }; + if bytes.is_empty() { + return String::new(); + } + + // Maybe it's the easy case and we're done! + if let Ok(utf8string) = String::from_utf8(bytes.clone()) { + return utf8string; + } + + let mut dst = String::new(); + iso8859_15::decode(bytes.as_slice(), &mut dst); + return dst; +} diff --git a/src/data/huditem.rs b/src/data/huditem.rs index 16fb9648..d7fa8332 100644 --- a/src/data/huditem.rs +++ b/src/data/huditem.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::ffi::CString; use std::fmt::Display; use cxx::let_cxx_string; @@ -14,10 +13,6 @@ use crate::plugin::{chargeLevelByFormSpec, isPoisonedByFormSpec, Color, ItemCate /// that drives the HUD cached for fast access. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct HudItem { - /// Player-visible name as its underlying bytes. - name_bytes: Vec, - /// a bit saying if we had to lose data to encode the name - name_is_utf8: bool, /// Name as utf8 name: String, /// A string that can be turned back into form data; for serializing. @@ -34,20 +29,16 @@ impl HudItem { pub fn from_keywords( category: ItemCategory, keywords: Vec, - name_bytes: Vec, + name: String, form_string: String, count: u32, twohanded: bool, ) -> Self { - let (name_is_utf8, name) = name_from_bytes(&name_bytes); - - // log::debug!("calling BaseType::classify() with keywords={keywords:?};"); + // log::trace!("calling BaseType::classify() with keywords={keywords:?};"); let kind: BaseType = BaseType::classify(name.as_str(), category, keywords, twohanded); let format_vars = HudItem::make_format_vars(name.clone(), count); Self { - name_bytes, name, - name_is_utf8, form_string, count, kind, @@ -55,18 +46,10 @@ impl HudItem { } } - pub fn preclassified( - name_bytes: Vec, - form_string: String, - count: u32, - kind: BaseType, - ) -> Self { - let (name_is_utf8, name) = name_from_bytes(&name_bytes); + pub fn preclassified(name: String, form_string: String, count: u32, kind: BaseType) -> Self { let format_vars = HudItem::make_format_vars(name.clone(), count); Self { - name_bytes, name, - name_is_utf8, form_string, count, kind, @@ -77,9 +60,7 @@ impl HudItem { pub fn for_equip_set(name: String, id: u32, icon: Icon) -> Self { let format_vars = HudItem::make_format_vars(name.clone(), 1); Self { - name_bytes: name.as_bytes().to_vec(), name, - name_is_utf8: true, form_string: format!("equipset_{id}"), count: 1, kind: BaseType::Equipset(icon), @@ -89,7 +70,7 @@ impl HudItem { pub fn make_unarmed_proxy() -> Self { HudItem::preclassified( - "Unarmed".as_bytes().to_vec(), + "Unarmed".to_string(), "unarmed_proxy".to_string(), 1, BaseType::HandToHand, @@ -145,14 +126,6 @@ impl HudItem { self.name.clone() } - pub fn name_is_utf8(&self) -> bool { - self.name_is_utf8 - } - - pub fn name_bytes(&self) -> Vec { - self.name_bytes.clone() - } - pub fn count(&self) -> u32 { self.count } @@ -252,33 +225,3 @@ impl Display for HudItem { ) } } - -fn name_from_bytes(name_bytes: &[u8]) -> (bool, String) { - // let's try to get a name string out of the bytes - let mut name_is_utf8 = false; - let cstring = match CString::from_vec_with_nul(name_bytes.to_owned()) { - Ok(cstring) => cstring, - Err(e) => { - if let Ok(cstring) = CString::new(name_bytes.to_owned()) { - cstring - } else { - log::info!("This is a bug with the mod this item comes from: item name bytes were an invalid C string; error: {e:#}"); - CString::default() - } - } - }; - - let name = if let Ok(v) = cstring.clone().into_string() { - name_is_utf8 = true; - v - } else { - let lossy = cstring.to_string_lossy().to_string(); - log::debug!( - "Item name is invalid utf-8; falling back to lossy string. name='{}';", - lossy - ); - lossy - }; - - (name_is_utf8, name) -} diff --git a/src/data/item_cache.rs b/src/data/item_cache.rs index 8fb89670..f464c330 100644 --- a/src/data/item_cache.rs +++ b/src/data/item_cache.rs @@ -139,7 +139,7 @@ pub fn fetch_game_item(form_string: &str) -> HudItem { let name = petname::petname(2, " "); let item = HudItem::preclassified( - name.as_bytes().to_vec(), + name, form_string.to_owned(), 2, super::BaseType::Weapon(WeaponType::new( @@ -159,7 +159,7 @@ mod tests { fn test_constructor_works() { let spec = "test-spec".to_string(); let item = fetch_game_item(&spec); - assert!(item.name_is_utf8()); + assert!(!item.name().is_empty()); assert_eq!(item.form_string(), spec); } } diff --git a/src/data/keywords.rs b/src/data/keywords.rs index 30177483..33e55199 100644 --- a/src/data/keywords.rs +++ b/src/data/keywords.rs @@ -126,11 +126,44 @@ pub enum SpellKeywords { Shout_MarkedForDeath, Shout_Slowtime, Shout_SoulTear, - Shout_StormCall, + Shout_Stormcall, Shout_SummonDurnehviir, Shout_ThrowVoice, Shout_UnrelentingForce, Shout_WhirlwindSprint, + Shout_PhantomForm, + Shout_SoulCairnSummon, + Shout_LightningBreath, + Shout_PoisonBreath, + Shout_AlessiasLove, + Shout_Annihilate, + Shout_ArcaneHelix, + Shout_Armageddon, + Shout_Curse, + Shout_DanceOfTheDead, + Shout_Earthquake, + Shout_EssenceRip, + Shout_Evocation, + Shout_Geomagnetism, + Shout_Iceborn, + Shout_JonesShadow, + Shout_Kingsbane, + Shout_Lifestream, + Shout_LightningShield, + Shout_Oblivion, + Shout_PhantomDecoy, + Shout_Riftwalk, + Shout_Shattersphere, + Shout_ShorsWrath, + Shout_ShroudOfSnowfall, + Shout_SpeakUntoTheStars, + Shout_SplinterTwins, + Shout_Stormblast, + Shout_TheConqueror, + Shout_Trueshot, + Shout_WailOfTheBanshee, + Shout_Wanderlust, + Shout_Warcry, // From here on it's OCF keywords minus the prefix ClassArcane, diff --git a/src/data/mod.rs b/src/data/mod.rs index 1be08471..1fe0620b 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -43,22 +43,14 @@ pub fn empty_huditem() -> Box { pub fn hud_item_from_keywords( category: ItemCategory, keywords_ffi: &CxxVector, - bytes_ffi: &CxxVector, + name: String, form_string: String, count: u32, twohanded: bool, ) -> Box { // #[allow(clippy::map_clone)] - let name_bytes: Vec = bytes_ffi.iter().copied().collect(); let keywords: Vec = keywords_ffi.iter().map(|xs| xs.to_string()).collect(); - let result = HudItem::from_keywords( - category, - keywords, - name_bytes, - form_string, - count, - twohanded, - ); + let result = HudItem::from_keywords(category, keywords, name, form_string, count, twohanded); Box::new(result) } @@ -78,11 +70,10 @@ pub fn magic_from_spelldata( which: ItemCategory, #[allow(clippy::boxed_local)] spelldata: Box, // this is coming from C++ keywords_ffi: &CxxVector, - bytes_ffi: &CxxVector, + name: String, form_string: String, count: u32, ) -> Box { - let name_bytes: Vec = bytes_ffi.iter().copied().collect(); let data = *spelldata; // unbox let keywords: Vec = keywords_ffi.iter().map(|xs| xs.to_string()).collect(); @@ -92,16 +83,22 @@ pub fn magic_from_spelldata( ItemCategory::Shout => BaseType::Shout(ShoutType::new(keywords)), _ => BaseType::Spell(SpellType::new(data, keywords)), }; - let result = HudItem::preclassified(name_bytes, form_string, count, kind); + let result = HudItem::preclassified(name, form_string, count, kind); Box::new(result) } -pub fn simple_from_formdata( - kind: ItemCategory, - bytes_ffi: &CxxVector, +pub fn categorize_shout( + keywords_ffi: &CxxVector, + name: String, form_string: String, ) -> Box { - let name_bytes: Vec = bytes_ffi.iter().copied().collect(); + let keywords: Vec = keywords_ffi.iter().map(|xs| xs.to_string()).collect(); + let kind = BaseType::Shout(ShoutType::new(keywords)); + let result = HudItem::preclassified(name, form_string, 1, kind); + Box::new(result) +} + +pub fn simple_from_formdata(kind: ItemCategory, name: String, form_string: String) -> Box { let classification = match kind { ItemCategory::HandToHand => BaseType::HandToHand, ItemCategory::Lantern => BaseType::Light(base::LightType::Lantern), @@ -111,7 +108,7 @@ pub fn simple_from_formdata( ItemCategory::Shout => BaseType::Shout(ShoutType::default()), _ => BaseType::Empty, }; - let result = HudItem::preclassified(name_bytes, form_string, 1, classification); + let result = HudItem::preclassified(name, form_string, 1, classification); Box::new(result) } @@ -119,12 +116,11 @@ pub fn potion_from_formdata( is_poison: bool, effect: i32, count: u32, - bytes_ffi: &CxxVector, + name: String, form_string: String, ) -> Box { - let name_bytes: Vec = bytes_ffi.iter().copied().collect(); let kind = PotionType::from_effect(is_poison, effect.into()); - let result = HudItem::preclassified(name_bytes, form_string, count, BaseType::Potion(kind)); + let result = HudItem::preclassified(name, form_string, count, BaseType::Potion(kind)); Box::new(result) } @@ -134,7 +130,7 @@ pub fn make_magicka_proxy() -> HudItem { #[cfg(not(test))] let count = magickaPotionCount(); HudItem::preclassified( - "Best Magicka".as_bytes().to_vec(), + "Best Magicka".to_string(), "magicka_proxy".to_string(), count, BaseType::PotionProxy(Proxy::Magicka), @@ -147,7 +143,7 @@ pub fn make_health_proxy() -> HudItem { #[cfg(not(test))] let count = healthPotionCount(); HudItem::preclassified( - "Best Health".as_bytes().to_vec(), + "Best Health".to_string(), "health_proxy".to_string(), count, BaseType::PotionProxy(Proxy::Health), @@ -160,7 +156,7 @@ pub fn make_stamina_proxy() -> HudItem { #[cfg(not(test))] let count = staminaPotionCount(); HudItem::preclassified( - "Best Stamina".as_bytes().to_vec(), + "Best Stamina".to_string(), "stamina_proxy".to_string(), count, BaseType::PotionProxy(Proxy::Stamina), @@ -220,18 +216,18 @@ mod tests { #[test] fn can_classify_huditem() { - let input = vec![ + let kwds = vec![ "OCF_InvColorBlood".to_string(), "WeapTypeHalberd".to_string(), "OCF_WeapTypeHalberd2H".to_string(), ]; - let name_bytes = "Placeholder".as_bytes().to_vec(); + let name = "Placeholder".to_string(); let item = HudItem::from_keywords( ItemCategory::Weapon, - input, - name_bytes, - "placeholder".to_string(), + kwds, + name, + "placeholder|0xcafed00d".to_string(), 2, true, ); diff --git a/src/data/shout.rs b/src/data/shout.rs index 6fd3dcef..3e4d8333 100644 --- a/src/data/shout.rs +++ b/src/data/shout.rs @@ -1,3 +1,7 @@ +use std::collections::HashMap; + +use once_cell::sync::Lazy; + use super::color::InvColor; use super::keywords::*; use super::{strings_to_enumset, HasIcon}; @@ -34,62 +38,16 @@ impl HasIcon for ShoutType { impl ShoutType { pub fn new(tags: Vec) -> Self { let keywords = strings_to_enumset::(&tags); - - let variant = if keywords.contains(SpellKeywords::Shout_AnimalAllegiance) { - ShoutVariant::AnimalAllegiance - } else if keywords.contains(SpellKeywords::Shout_AuraWhisper) { - ShoutVariant::AuraWhisper - } else if keywords.contains(SpellKeywords::Shout_BattleFury) { - ShoutVariant::BattleFury - } else if keywords.contains(SpellKeywords::Shout_BecomeEthereal) { - ShoutVariant::BecomeEthereal - } else if keywords.contains(SpellKeywords::Shout_BendWill) { - ShoutVariant::BendWill - } else if keywords.contains(SpellKeywords::Shout_CallDragon) { - ShoutVariant::CallDragon - } else if keywords.contains(SpellKeywords::Shout_CallOfValor) { - ShoutVariant::CallOfValor - } else if keywords.contains(SpellKeywords::Shout_ClearSkies) { - ShoutVariant::ClearSkies - } else if keywords.contains(SpellKeywords::Shout_Disarm) { - ShoutVariant::Disarm - } else if keywords.contains(SpellKeywords::Shout_Dismay) { - ShoutVariant::Dismay - } else if keywords.contains(SpellKeywords::Shout_DragonAspect) { - ShoutVariant::DragonAspect - } else if keywords.contains(SpellKeywords::Shout_Dragonrend) { - ShoutVariant::Dragonrend - } else if keywords.contains(SpellKeywords::Shout_DrainVitality) { - ShoutVariant::DrainVitality - } else if keywords.contains(SpellKeywords::Shout_ElementalFury) { - ShoutVariant::ElementalFury - } else if keywords.contains(SpellKeywords::Shout_FireBreath) { - ShoutVariant::FireBreath - } else if keywords.contains(SpellKeywords::Shout_FrostBreath) { - ShoutVariant::FrostBreath - } else if keywords.contains(SpellKeywords::Shout_IceForm) { - ShoutVariant::IceForm - } else if keywords.contains(SpellKeywords::Shout_KynesPeace) { - ShoutVariant::KynesPeace - } else if keywords.contains(SpellKeywords::Shout_MarkedForDeath) { - ShoutVariant::MarkedForDeath - } else if keywords.contains(SpellKeywords::Shout_Slowtime) { - ShoutVariant::Slowtime - } else if keywords.contains(SpellKeywords::Shout_SoulTear) { - ShoutVariant::SoulTear - } else if keywords.contains(SpellKeywords::Shout_StormCall) { - ShoutVariant::StormCall - } else if keywords.contains(SpellKeywords::Shout_SummonDurnehviir) { - ShoutVariant::SummonDurnehviir - } else if keywords.contains(SpellKeywords::Shout_ThrowVoice) { - ShoutVariant::ThrowVoice - } else if keywords.contains(SpellKeywords::Shout_UnrelentingForce) { - ShoutVariant::UnrelentingForce - } else if keywords.contains(SpellKeywords::Shout_WhirlwindSprint) { - ShoutVariant::WhirlwindSprint - } else { - ShoutVariant::Unclassified - }; + let (variant, icon) = SHOUT_MAPPING + .iter() + .find_map(|(k, v)| { + if keywords.contains(*k) { + Some(v.clone()) + } else { + None + } + }) + .unwrap_or((ShoutVariant::Unclassified, Icon::Shout)); let color = match variant { ShoutVariant::AnimalAllegiance => InvColor::Green, @@ -102,29 +60,18 @@ impl ShoutType { ShoutVariant::IceForm => InvColor::Frost, ShoutVariant::KynesPeace => InvColor::Green, ShoutVariant::MarkedForDeath => InvColor::Poison, - ShoutVariant::StormCall => InvColor::Shock, + ShoutVariant::Stormcall => InvColor::Shock, _ => InvColor::White, }; - let icon = match variant { - ShoutVariant::AnimalAllegiance => Icon::ShoutAnimalAllegiance, - ShoutVariant::CallDragon => Icon::ShoutCallDragon, - ShoutVariant::ClearSkies => Icon::ShoutClearSkies, - ShoutVariant::Cyclone => Icon::ShoutCyclone, - ShoutVariant::Dismay => Icon::ShoutDismay, - // ShoutVariant::Dragonrend => Icon::ShoutDragonrend, - ShoutVariant::ElementalFury => Icon::ShoutElementalFury, - ShoutVariant::FireBreath => Icon::ShoutBreathAttack, - ShoutVariant::FrostBreath => Icon::ShoutBreathAttack, - ShoutVariant::IceForm => Icon::ShoutIceForm, - ShoutVariant::MarkedForDeath => Icon::ShoutMarkedForDeath, - ShoutVariant::Slowtime => Icon::SpellTime, - ShoutVariant::StormCall => Icon::ShoutStormcall, - ShoutVariant::UnrelentingForce => Icon::ShoutUnrelentingForce, - ShoutVariant::WhirlwindSprint => Icon::SpellSprint, - _ => Icon::Shout, - }; + Self { + icon, + color, + variant, + } + } + pub fn construct(icon: Icon, color: InvColor, variant: ShoutVariant) -> Self { Self { icon, color, @@ -158,13 +105,48 @@ impl ShoutType { ShoutVariant::IceForm => "Iiz-slen-nus!", ShoutVariant::KynesPeace => "Kaan-drem-ov!", ShoutVariant::MarkedForDeath => "Krii-lun-aus!", + ShoutVariant::PhantomForm => "Fiik-lo-sah!", ShoutVariant::Slowtime => "Tiid-klo-ui!", ShoutVariant::SoulTear => "Rii-vaaz-zol!", - ShoutVariant::StormCall => "Strun-bah-qo!", + ShoutVariant::Stormcall => "Strun-bah-qo!", ShoutVariant::SummonDurnehviir => "Dur-neh-viir!", ShoutVariant::ThrowVoice => "Zul-mey-gut!", ShoutVariant::UnrelentingForce => "Fus-ro-dah!", - ShoutVariant::WhirlwindSprint => "Wuld-nah-kest!!", + ShoutVariant::WhirlwindSprint => "Wuld-nah-kest!", + ShoutVariant::SoulCairnSummon => "Diil-qoth-zaam!", + // stormcrown + ShoutVariant::LightningBreath => "Strun-gaar-kest", + ShoutVariant::PoisonBreath => "Laas-slen-aus", + // thunderchild + ShoutVariant::AlessiasLove => "Juoor-drem-ov", + ShoutVariant::Annihilate => "Fii-gaar-nos", + ShoutVariant::ArcaneHelix => "Vol-nah-kest", + ShoutVariant::Armageddon => "Wuld-toor-shul", + ShoutVariant::Curse => "Fiik-zii-gron!", + ShoutVariant::DanceOfTheDead => "Raan-vaaz-sol", + ShoutVariant::Earthquake => "Fus-klo-ul", + ShoutVariant::EssenceRip => "Laaz-ro-dah", + ShoutVariant::Evocation => "Ven-lah-haas", + ShoutVariant::Geomagnetism => "Gol-yah-nir", + ShoutVariant::Iceborn => "Iiz-ah-viing", + ShoutVariant::JonesShadow => "Zul-lun-aus", + ShoutVariant::Kingsbane => "Mul-neh-viir", + ShoutVariant::Lifestream => "Gaan-vur-shaan", + ShoutVariant::LightningShield => "Strun-slen-nus", + ShoutVariant::Oblivion => "Dur-hah-dov", + ShoutVariant::PhantomDecoy => "Fiik-lo-sah", + ShoutVariant::Riftwalk => "Su-ru-maar", + ShoutVariant::Shattersphere => "Fo-mey-gut", + ShoutVariant::ShorsWrath => "Hun-haal-viik", + ShoutVariant::ShroudOfSnowfall => "Feim-krah-diin", + ShoutVariant::SpeakUntoTheStars => "Tiid-mir-tah", + ShoutVariant::SplinterTwins => "Frii-lo-sah", + ShoutVariant::Stormblast => "Lok-bah-qo", + ShoutVariant::TheConqueror => "Mid-quah-diiv", + ShoutVariant::Trueshot => "Kaan-grah-dun", + ShoutVariant::WailOfTheBanshee => "Faaz-zah-frul", + ShoutVariant::Wanderlust => "Od-vah-koor", + ShoutVariant::Warcry => "Zun-kaal-zoor", ShoutVariant::Unclassified => "This shout is new to me!", } } @@ -192,13 +174,386 @@ pub enum ShoutVariant { IceForm, KynesPeace, MarkedForDeath, + PhantomForm, Slowtime, SoulTear, - StormCall, + Stormcall, SummonDurnehviir, ThrowVoice, UnrelentingForce, WhirlwindSprint, + // unused dawnguard shout + SoulCairnSummon, + // Stormcrown + LightningBreath, + PoisonBreath, + // Thunderchild shouts + AlessiasLove, + Annihilate, + ArcaneHelix, + Armageddon, + Curse, + DanceOfTheDead, + Earthquake, + EssenceRip, + Evocation, + Geomagnetism, + Iceborn, + JonesShadow, + Kingsbane, + Lifestream, + LightningShield, + Oblivion, + PhantomDecoy, + Riftwalk, + Shattersphere, + ShorsWrath, + ShroudOfSnowfall, + SpeakUntoTheStars, + SplinterTwins, + Stormblast, + TheConqueror, + Trueshot, + WailOfTheBanshee, + Wanderlust, + Warcry, #[default] Unclassified, } + +static SHOUT_MAPPING: Lazy> = Lazy::new(|| { + HashMap::from([ + ( + SpellKeywords::Shout_Curse, + (ShoutVariant::Curse, Icon::ShoutCurse), + ), + ( + SpellKeywords::Shout_Warcry, + (ShoutVariant::Warcry, Icon::ShoutWarcry), + ), + // vanilla shouts + ( + SpellKeywords::Shout_AnimalAllegiance, + (ShoutVariant::AnimalAllegiance, Icon::ShoutAnimalAllegiance), + ), + ( + SpellKeywords::Shout_AuraWhisper, + (ShoutVariant::AuraWhisper, Icon::ShoutAuraWhisper), + ), + ( + SpellKeywords::Shout_BattleFury, + (ShoutVariant::BattleFury, Icon::ShoutBattleFury), + ), + ( + SpellKeywords::Shout_BecomeEthereal, + (ShoutVariant::BecomeEthereal, Icon::ShoutBecomeEthereal), + ), + ( + SpellKeywords::Shout_BendWill, + (ShoutVariant::BendWill, Icon::ShoutBendWill), + ), + ( + SpellKeywords::Shout_CallDragon, + (ShoutVariant::CallDragon, Icon::ShoutCallDragon), + ), + ( + SpellKeywords::Shout_CallOfValor, + (ShoutVariant::CallOfValor, Icon::ShoutCallOfValor), + ), + ( + SpellKeywords::Shout_ClearSkies, + (ShoutVariant::ClearSkies, Icon::ShoutClearSkies), + ), + ( + SpellKeywords::Shout_Disarm, + (ShoutVariant::Disarm, Icon::ShoutDisarm), + ), + ( + SpellKeywords::Shout_Dismay, + (ShoutVariant::Dismay, Icon::ShoutDismay), + ), + ( + SpellKeywords::Shout_DragonAspect, + (ShoutVariant::DragonAspect, Icon::ShoutDragonAspect), + ), + ( + SpellKeywords::Shout_Dragonrend, + (ShoutVariant::Dragonrend, Icon::ShoutDragonrend), + ), + ( + SpellKeywords::Shout_DrainVitality, + (ShoutVariant::DrainVitality, Icon::ShoutDrainVitality), + ), + ( + SpellKeywords::Shout_ElementalFury, + (ShoutVariant::ElementalFury, Icon::ShoutElementalFury), + ), + ( + SpellKeywords::Shout_FireBreath, + (ShoutVariant::FireBreath, Icon::ShoutFireBreath), + ), + ( + SpellKeywords::Shout_FrostBreath, + (ShoutVariant::FrostBreath, Icon::ShoutFrostBreath), + ), + ( + SpellKeywords::Shout_IceForm, + (ShoutVariant::IceForm, Icon::ShoutIceForm), + ), + ( + SpellKeywords::Shout_KynesPeace, + (ShoutVariant::KynesPeace, Icon::ShoutKynesPeace), + ), + ( + SpellKeywords::Shout_MarkedForDeath, + (ShoutVariant::MarkedForDeath, Icon::ShoutMarkedForDeath), + ), + ( + SpellKeywords::Shout_Slowtime, + (ShoutVariant::Slowtime, Icon::ShoutSlowtime), + ), + ( + SpellKeywords::Shout_SoulTear, + (ShoutVariant::SoulTear, Icon::ShoutSoulTear), + ), + ( + SpellKeywords::Shout_Stormcall, + (ShoutVariant::Stormcall, Icon::ShoutStormcall), + ), + ( + SpellKeywords::Shout_SummonDurnehviir, + (ShoutVariant::SummonDurnehviir, Icon::ShoutSummonDurnehviir), + ), + ( + SpellKeywords::Shout_ThrowVoice, + (ShoutVariant::ThrowVoice, Icon::ShoutThrowVoice), + ), + ( + SpellKeywords::Shout_UnrelentingForce, + (ShoutVariant::UnrelentingForce, Icon::ShoutUnrelentingForce), + ), + ( + SpellKeywords::Shout_WhirlwindSprint, + (ShoutVariant::WhirlwindSprint, Icon::ShoutWhirlwindSprint), + ), + ( + SpellKeywords::Shout_PhantomForm, + (ShoutVariant::PhantomForm, Icon::ShoutPhantomForm), + ), + ( + SpellKeywords::Shout_AlessiasLove, + (ShoutVariant::AlessiasLove, Icon::ShoutAlessiasLove), + ), + ( + SpellKeywords::Shout_Annihilate, + (ShoutVariant::Annihilate, Icon::ShoutAnnihilate), + ), + ( + SpellKeywords::Shout_ArcaneHelix, + (ShoutVariant::ArcaneHelix, Icon::ShoutArcaneHelix), + ), + ( + SpellKeywords::Shout_Armageddon, + (ShoutVariant::Armageddon, Icon::ShoutArmageddon), + ), + ( + SpellKeywords::Shout_Curse, + (ShoutVariant::Curse, Icon::ShoutCurse), + ), + ( + SpellKeywords::Shout_DanceOfTheDead, + (ShoutVariant::DanceOfTheDead, Icon::ShoutDanceOfTheDead), + ), + ( + SpellKeywords::Shout_Earthquake, + (ShoutVariant::Earthquake, Icon::ShoutEarthquake), + ), + ( + SpellKeywords::Shout_EssenceRip, + (ShoutVariant::EssenceRip, Icon::ShoutEssenceRip), + ), + ( + SpellKeywords::Shout_Evocation, + (ShoutVariant::Evocation, Icon::ShoutEvocation), + ), + ( + SpellKeywords::Shout_Geomagnetism, + (ShoutVariant::Geomagnetism, Icon::ShoutGeomagnetism), + ), + ( + SpellKeywords::Shout_Iceborn, + (ShoutVariant::Iceborn, Icon::ShoutIceborn), + ), + ( + SpellKeywords::Shout_JonesShadow, + (ShoutVariant::JonesShadow, Icon::ShoutJonesShadow), + ), + ( + SpellKeywords::Shout_Kingsbane, + (ShoutVariant::Kingsbane, Icon::ShoutKingsbane), + ), + ( + SpellKeywords::Shout_Lifestream, + (ShoutVariant::Lifestream, Icon::ShoutLifestream), + ), + ( + SpellKeywords::Shout_LightningShield, + (ShoutVariant::LightningShield, Icon::ShoutLightningShield), + ), + ( + SpellKeywords::Shout_Oblivion, + (ShoutVariant::Oblivion, Icon::ShoutOblivion), + ), + ( + SpellKeywords::Shout_PhantomDecoy, + (ShoutVariant::PhantomDecoy, Icon::ShoutPhantomDecoy), + ), + ( + SpellKeywords::Shout_Riftwalk, + (ShoutVariant::Riftwalk, Icon::ShoutRiftwalk), + ), + ( + SpellKeywords::Shout_Shattersphere, + (ShoutVariant::Shattersphere, Icon::ShoutShattersphere), + ), + ( + SpellKeywords::Shout_ShorsWrath, + (ShoutVariant::ShorsWrath, Icon::ShoutShorsWrath), + ), + ( + SpellKeywords::Shout_ShroudOfSnowfall, + (ShoutVariant::ShroudOfSnowfall, Icon::ShoutShroudOfSnowfall), + ), + ( + SpellKeywords::Shout_SpeakUntoTheStars, + ( + ShoutVariant::SpeakUntoTheStars, + Icon::ShoutSpeakUntoTheStars, + ), + ), + ( + SpellKeywords::Shout_SplinterTwins, + (ShoutVariant::SplinterTwins, Icon::ShoutSplinterTwins), + ), + ( + SpellKeywords::Shout_Stormblast, + (ShoutVariant::Stormblast, Icon::ShoutStormblast), + ), + ( + SpellKeywords::Shout_TheConqueror, + (ShoutVariant::TheConqueror, Icon::ShoutTheConqueror), + ), + ( + SpellKeywords::Shout_Trueshot, + (ShoutVariant::Trueshot, Icon::ShoutTrueshot), + ), + ( + SpellKeywords::Shout_WailOfTheBanshee, + (ShoutVariant::WailOfTheBanshee, Icon::ShoutWailOfTheBanshee), + ), + ( + SpellKeywords::Shout_Wanderlust, + (ShoutVariant::Wanderlust, Icon::ShoutWanderlust), + ), + ( + SpellKeywords::Shout_LightningBreath, + (ShoutVariant::LightningBreath, Icon::ShoutLightningBreath), + ), + ( + SpellKeywords::Shout_PoisonBreath, + (ShoutVariant::PoisonBreath, Icon::ShoutPoisonBreath), + ), + ( + SpellKeywords::Shout_SoulCairnSummon, + (ShoutVariant::SoulCairnSummon, Icon::ShoutSoulCairnSummon), + ), + ]) +}); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn all_keywords_used() { + let shoutwords: Vec = vec![ + SpellKeywords::Shout_AnimalAllegiance, + SpellKeywords::Shout_AuraWhisper, + SpellKeywords::Shout_BattleFury, + SpellKeywords::Shout_BecomeEthereal, + SpellKeywords::Shout_BendWill, + SpellKeywords::Shout_CallDragon, + SpellKeywords::Shout_CallOfValor, + SpellKeywords::Shout_ClearSkies, + SpellKeywords::Shout_Disarm, + SpellKeywords::Shout_Dismay, + SpellKeywords::Shout_DragonAspect, + SpellKeywords::Shout_Dragonrend, + SpellKeywords::Shout_DrainVitality, + SpellKeywords::Shout_ElementalFury, + SpellKeywords::Shout_FireBreath, + SpellKeywords::Shout_FrostBreath, + SpellKeywords::Shout_IceForm, + SpellKeywords::Shout_KynesPeace, + SpellKeywords::Shout_MarkedForDeath, + SpellKeywords::Shout_Slowtime, + SpellKeywords::Shout_SoulTear, + SpellKeywords::Shout_Stormcall, + SpellKeywords::Shout_SummonDurnehviir, + SpellKeywords::Shout_ThrowVoice, + SpellKeywords::Shout_UnrelentingForce, + SpellKeywords::Shout_WhirlwindSprint, + // Dawnguard unused spell + SpellKeywords::Shout_SoulCairnSummon, + // ForcefulTongue + SpellKeywords::Shout_PhantomForm, + // Stormcrown + SpellKeywords::Shout_LightningBreath, + SpellKeywords::Shout_PoisonBreath, + // Thunderchild + SpellKeywords::Shout_AlessiasLove, + SpellKeywords::Shout_Annihilate, + SpellKeywords::Shout_ArcaneHelix, + SpellKeywords::Shout_Armageddon, + SpellKeywords::Shout_Curse, + SpellKeywords::Shout_DanceOfTheDead, + SpellKeywords::Shout_Earthquake, + SpellKeywords::Shout_EssenceRip, + SpellKeywords::Shout_Evocation, + SpellKeywords::Shout_Geomagnetism, + SpellKeywords::Shout_Iceborn, + SpellKeywords::Shout_JonesShadow, + SpellKeywords::Shout_Kingsbane, + SpellKeywords::Shout_Lifestream, + SpellKeywords::Shout_LightningShield, + SpellKeywords::Shout_Oblivion, + SpellKeywords::Shout_PhantomDecoy, + SpellKeywords::Shout_Riftwalk, + SpellKeywords::Shout_Shattersphere, + SpellKeywords::Shout_ShorsWrath, + SpellKeywords::Shout_ShroudOfSnowfall, + SpellKeywords::Shout_SpeakUntoTheStars, + SpellKeywords::Shout_SplinterTwins, + SpellKeywords::Shout_Stormblast, + SpellKeywords::Shout_TheConqueror, + SpellKeywords::Shout_Trueshot, + SpellKeywords::Shout_WailOfTheBanshee, + SpellKeywords::Shout_Wanderlust, + SpellKeywords::Shout_Warcry, + ]; + + let unused: Vec<&SpellKeywords> = shoutwords + .iter() + .filter(|xs| { + let shout = ShoutType::new(vec![xs.to_string()]); + if matches!(shout.variant, ShoutVariant::Unclassified) { + eprintln!("{xs} turned into unclassified shout"); + true + } else { + false + } + }) + .collect(); + assert!(unused.is_empty()); + } +} diff --git a/src/game/equippable.cpp b/src/game/equippable.cpp index deb1e583..6c8f426b 100644 --- a/src/game/equippable.cpp +++ b/src/game/equippable.cpp @@ -56,7 +56,7 @@ namespace equippable return RE::ActorValue::kNone; } - rust::Box fillOutSpellData(bool two_handed, int32_t skill_level, const RE::EffectSetting* effect) + rust::Box fillOutSpellData(bool twoHanded, int32_t skill_level, const RE::EffectSetting* effect) { auto isHostile = effect->IsHostile(); auto archetype = effect->data.archetype; @@ -65,113 +65,53 @@ namespace equippable rust::Box data = fill_out_spell_data(isHostile, static_cast>(resist), - two_handed, + twoHanded, static_cast>(school), skill_level, static_cast>(archetype)); return data; } - rust::Box hudItemFromForm(RE::TESForm* item_form) + rust::Box hudItemFromForm(RE::TESForm* form) { - if (!item_form) { return empty_huditem(); } + if (!form) { return empty_huditem(); } - KeywordAccumulator::clear(); - auto loggerName = game::displayName(item_form); - auto chonker = helpers::chars_to_vec(loggerName); - std::string form_string = helpers::makeFormSpecString(item_form); - bool two_handed = requiresTwoHands(item_form); + RE::TESBoundObject* boundObject = nullptr; + RE::ExtraDataList* extraData = nullptr; + const auto count = game::boundObjectForForm(form, boundObject, extraData); - if (item_form->Is(RE::FormType::Ammo)) - { - rlog::debug("making HudItem for ammo: '{}'"sv, loggerName); - const auto* ammo = item_form->As()->AsKeywordForm(); - ammo->ForEachKeyword(KeywordAccumulator::collect); - auto& keywords = KeywordAccumulator::mKeywords; - auto count = player::getInventoryCountByForm(item_form); + rlog::info("entering hudItemFromForm() for {}", rlog::formatAsHex(form->GetFormID())); - rust::Box item = - hud_item_from_keywords(ItemCategory::Ammo, *keywords, std::move(chonker), form_string, count, false); - return item; - } + auto safename = boundObject ? helpers::displayNameAsUtf8(boundObject) : helpers::displayNameAsUtf8(form); + std::string formSpec = + boundObject ? helpers::makeFormSpecString(boundObject) : helpers::makeFormSpecString(form); + bool twoHanded = requiresTwoHands(form); - if (item_form->IsWeapon()) - { - const auto* weapon = item_form->As(); - if (weapon) - { - rlog::debug("making HudItem for weapon: '{}'"sv, loggerName); - weapon->ForEachKeyword(KeywordAccumulator::collect); - auto& keywords = KeywordAccumulator::mKeywords; - if (weapon->IsBound()) { keywords->push_back(std::string("OCF_InvColorBound")); } - auto count = player::getInventoryCountByForm(item_form); - rust::Box item = hud_item_from_keywords( - ItemCategory::Weapon, *keywords, std::move(chonker), form_string, count, two_handed); - - return item; - } - } - - if (item_form->IsArmor()) - { - rlog::debug("making HudItem for armor: '{}'"sv, loggerName); - const auto* armor = item_form->As(); - armor->ForEachKeyword(KeywordAccumulator::collect); - auto& keywords = KeywordAccumulator::mKeywords; - auto count = player::getInventoryCountByForm(item_form); - rust::Box item = - hud_item_from_keywords(ItemCategory::Armor, *keywords, std::move(chonker), form_string, count, false); - - return item; - } - - // There are two kinds of lights: lights held in the hand like torches, - // and wearable lights (usually lanterns). The wearable ones are armor, and - // have just been taken care of in the previous block. This block handles - // the other types. These go into the left hand! - if (item_form->Is(RE::FormType::Light)) - { - // This form type does not have keywords. This presents a problem. Cough. - rlog::debug("making HudItem for light: '{}';"sv, loggerName); - const auto name = std::string(item_form->GetName()); - if (name.find("Lantern") != std::string::npos) // yes, very limited in effectiveness - { - rust::Box item = simple_from_formdata(ItemCategory::Lantern, std::move(chonker), form_string); - return item; - } - rust::Box item = simple_from_formdata(ItemCategory::Torch, std::move(chonker), form_string); - return item; - } + KeywordAccumulator::clear(); - if (item_form->Is(RE::FormType::Shout)) + if (form->Is(RE::FormType::Shout)) { - rlog::debug("making HudItem for shout: '{}'"sv, loggerName); - auto* shout = item_form->As(); + rlog::trace("making HudItem for shout: '{}'"sv, safename); + auto* shout = form->As(); - if (!shout) return simple_from_formdata(ItemCategory::Shout, std::move(chonker), form_string); + if (!shout) return simple_from_formdata(ItemCategory::Shout, std::move(safename), formSpec); auto* spell = shout->variations[RE::TESShout::VariationIDs::kOne].spell; // always the first to ID - if (!spell) return simple_from_formdata(ItemCategory::Shout, std::move(chonker), form_string); + if (!spell) return simple_from_formdata(ItemCategory::Shout, std::move(safename), formSpec); - const auto* effect = spell->GetCostliestEffectItem()->baseEffect; - if (!effect) return simple_from_formdata(ItemCategory::Shout, std::move(chonker), form_string); - effect->ForEachKeyword(KeywordAccumulator::collect); + spell->ForEachKeyword(KeywordAccumulator::collect); auto& keywords = KeywordAccumulator::mKeywords; - - auto data = fillOutSpellData(false, 1, effect); - rust::Box item = magic_from_spelldata( - ItemCategory::Shout, std::move(data), *keywords, std::move(chonker), form_string, 1); - return item; + return categorize_shout(*keywords, std::move(safename), formSpec); } - if (item_form->Is(RE::FormType::Spell)) + if (form->Is(RE::FormType::Spell)) { - auto* spell = item_form->As(); + auto* spell = form->As(); const auto spell_type = spell->GetSpellType(); if (spell_type == RE::MagicSystem::SpellType::kLesserPower || spell_type == RE::MagicSystem::SpellType::kPower) { - rlog::debug("making HudItem for power: '{}'"sv, loggerName); + rlog::trace("making HudItem for power: '{}'"sv, safename); const auto* costliest = spell->GetCostliestEffectItem(); if (costliest) { @@ -181,14 +121,14 @@ namespace equippable effect->ForEachKeyword(KeywordAccumulator::collect); auto& keywords = KeywordAccumulator::mKeywords; rust::Box item = hud_item_from_keywords( - ItemCategory::Power, *keywords, std::move(chonker), form_string, 1, false); + ItemCategory::Power, *keywords, std::move(safename), formSpec, 1, false); return item; } } } // Regular spells. - rlog::debug("making HudItem for spell: '{}'"sv, loggerName); + rlog::trace("making HudItem for spell: '{}'"sv, safename); const auto* costliest = spell->GetCostliestEffectItem(); if (costliest) { @@ -198,66 +138,124 @@ namespace equippable effect->ForEachKeyword(KeywordAccumulator::collect); auto& keywords = KeywordAccumulator::mKeywords; auto skill_level = effect->GetMinimumSkillLevel(); - auto data = fillOutSpellData(two_handed, skill_level, effect); + auto data = fillOutSpellData(twoHanded, skill_level, effect); rust::Box item = magic_from_spelldata( - ItemCategory::Spell, std::move(data), *keywords, std::move(chonker), form_string, 1); + ItemCategory::Spell, std::move(data), *keywords, std::move(safename), formSpec, 1); return item; } } } - if (item_form->Is(RE::FormType::Scroll)) + if (form->Is(RE::FormType::Ammo)) { - rlog::debug("making HudItem for scroll: '{}'"sv, loggerName); - auto* scroll = item_form->As(); + rlog::trace("making HudItem for ammo: '{}'"sv, safename); + const auto* ammo = form->As()->AsKeywordForm(); + ammo->ForEachKeyword(KeywordAccumulator::collect); + auto& keywords = KeywordAccumulator::mKeywords; + + rust::Box item = + hud_item_from_keywords(ItemCategory::Ammo, *keywords, std::move(safename), formSpec, count, false); + return item; + } + + if (form->IsWeapon()) + { + const auto* weapon = form->As(); + if (weapon) + { + rlog::trace("making HudItem for weapon: '{}'"sv, safename); + weapon->ForEachKeyword(KeywordAccumulator::collect); + auto& keywords = KeywordAccumulator::mKeywords; + if (weapon->IsBound()) { keywords->push_back(std::string("OCF_InvColorBound")); } + rust::Box item = hud_item_from_keywords( + ItemCategory::Weapon, *keywords, std::move(safename), formSpec, count, twoHanded); + + return item; + } + } + + if (form->IsArmor()) + { + rlog::trace("making HudItem for armor: '{}'"sv, safename); + const auto* armor = form->As(); + armor->ForEachKeyword(KeywordAccumulator::collect); + auto& keywords = KeywordAccumulator::mKeywords; + rust::Box item = + hud_item_from_keywords(ItemCategory::Armor, *keywords, std::move(safename), formSpec, count, false); + + return item; + } + + // There are two kinds of lights: lights held in the hand like torches, + // and wearable lights (usually lanterns). The wearable ones are armor, and + // have just been taken care of in the previous block. This block handles + // the other types. These go into the left hand! + if (form->Is(RE::FormType::Light)) + { + // This form type does not have keywords. This presents a problem. Cough. + rlog::trace("making HudItem for light: '{}';"sv, safename); + const auto name = std::string(form->GetName()); // this use of GetName() is okay + if (name.find("Lantern") != std::string::npos) // yes, very limited in effectiveness; TODO + { + rust::Box item = simple_from_formdata(ItemCategory::Lantern, std::move(safename), formSpec); + return item; + } + rust::Box item = simple_from_formdata(ItemCategory::Torch, std::move(safename), formSpec); + return item; + } + + + if (form->Is(RE::FormType::Scroll)) + { + rlog::trace("making HudItem for scroll: '{}'"sv, safename); + auto* scroll = form->As(); if (scroll->GetCostliestEffectItem() && scroll->GetCostliestEffectItem()->baseEffect) { const auto effect = scroll->GetCostliestEffectItem()->baseEffect; effect->ForEachKeyword(KeywordAccumulator::collect); - auto& keywords = KeywordAccumulator::mKeywords; - auto skill_level = effect->GetMinimumSkillLevel(); + auto& keywords = KeywordAccumulator::mKeywords; + auto skillLevel = effect->GetMinimumSkillLevel(); - auto data = fillOutSpellData(two_handed, skill_level, effect); + auto data = fillOutSpellData(twoHanded, skillLevel, effect); rust::Box item = magic_from_spelldata( - ItemCategory::Scroll, std::move(data), *keywords, std::move(chonker), form_string, 1); + ItemCategory::Scroll, std::move(data), *keywords, std::move(safename), formSpec, count); return item; } } - if (item_form->Is(RE::FormType::AlchemyItem)) + if (form->Is(RE::FormType::AlchemyItem)) { - auto count = player::getInventoryCountByForm(item_form); - auto* alchemy_potion = item_form->As(); - const auto* effect = alchemy_potion->GetCostliestEffectItem()->baseEffect; - auto actor_value = effect->data.primaryAV; + auto* alchemy_potion = form->As(); if (alchemy_potion->IsFood()) { - rlog::debug("making HudItem for food: '{}'"sv, loggerName); + rlog::trace("making HudItem for food: '{}'"sv, safename); 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); + auto& keywords = KeywordAccumulator::mKeywords; + rust::Box item = + hud_item_from_keywords(ItemCategory::Food, *keywords, std::move(safename), formSpec, count, false); return item; } else { - rlog::debug("making HudItem for potion: '{}'"sv, loggerName); + rlog::trace("making HudItem for potion: '{}'"sv, safename); + const auto* effect = alchemy_potion->GetCostliestEffectItem()->baseEffect; + auto actor_value = effect->data.primaryAV; rust::Box item = potion_from_formdata(alchemy_potion->IsPoison(), static_cast(actor_value), count, - std::move(chonker), - form_string); + std::move(safename), + formSpec); return item; } } - const auto formtype = item_form->GetFormType(); + const auto formtype = form->GetFormType(); const auto formtypestr = RE::FormTypeToString(formtype); rlog::debug("hudItemFromForm() fell all the way through; type={}; name='{}'; formspec='{}';", formtypestr, - item_form->GetName(), - form_string); + safename, + formSpec); return empty_huditem(); } diff --git a/src/game/equippable.h b/src/game/equippable.h index 36403673..90885cbc 100644 --- a/src/game/equippable.h +++ b/src/game/equippable.h @@ -1,13 +1,11 @@ #pragma once #include "rust/cxx.h" +#include "soulsy.h" // Builds the rust HudItem struct from game data, inspecting forms, // keywords, and inventory data as needed. -struct HudItem; -struct SpellData; - namespace equippable { rust::Box hudItemFromForm(RE::TESForm* form); diff --git a/src/game/gear.cpp b/src/game/gear.cpp index 70debd3d..2f348b36 100644 --- a/src/game/gear.cpp +++ b/src/game/gear.cpp @@ -1,14 +1,26 @@ #include "gear.h" +#include "RE/E/ExtraDataTypes.h" #include "constant.h" +#include "helpers.h" #include "offset.h" #include "player.h" -#include "string_util.h" #include "lib.rs.h" namespace game { + EquippableItemData::EquippableItemData() + : count(0) + , itemExtraList(nullptr) + , wornExtraList(nullptr) + , wornLeftExtraList(nullptr) + , isWorn(false) + , isWornLeft(false) + , isFavorite(false) + { + } + RE::BGSEquipSlot* right_hand_equip_slot() { using func_t = decltype(&right_hand_equip_slot); @@ -32,9 +44,9 @@ namespace game bool inventoryEntryDataFor(const RE::TESForm* form, RE::InventoryEntryData*& outentry) { - auto* the_player = RE::PlayerCharacter::GetSingleton(); + auto* thePlayer = RE::PlayerCharacter::GetSingleton(); std::map>> candidates = - player::getInventoryForType(the_player, form->GetFormType()); + player::getInventoryForType(thePlayer, form->GetFormType()); RE::InventoryEntryData entryData; bool found = false; @@ -56,55 +68,177 @@ namespace game return found; } - int boundObjectForForm(const RE::TESForm* form, - RE::PlayerCharacter*& the_player, + // The next three functions have some refactoring opportunities, as they + // say. However, I am first getting them working properly before cleaning up. + + // Returns only matches that are currently equipped, so 0, 1, or 2 are your + // only possible return values. Heh. + int boundObjectForWornItem(const RE::TESForm* form, + WornWhere constraint, RE::TESBoundObject*& outobj, - RE::ExtraDataList*& outextra) + RE::ExtraDataList* outextra) { - RE::TESBoundObject* bound_obj = nullptr; - RE::ExtraDataList* extra = nullptr; - std::vector extra_vector; + auto* thePlayer = RE::PlayerCharacter::GetSingleton(); + RE::TESBoundObject* foundObject = nullptr; + EquippableItemData equipData = EquippableItemData(); + std::vector extraDataCopy; std::map>> candidates = - player::getInventoryForType(the_player, form->GetFormType()); + player::getInventoryForType(thePlayer, form->GetFormType()); - auto item_count = 0; - for (const auto& [item, inv_data] : candidates) + bool matchFound = false; + for (const auto& [item, inventoryData] : candidates) { - if (const auto& [num_items, entry] = inv_data; entry->object->formID == form->formID) - { - bound_obj = item; - item_count = num_items; - auto simple_extra_data_list = entry->extraLists; + const auto& [countHeld, entry] = inventoryData; - if (simple_extra_data_list) + if (entry->object->formID == form->formID) + { + EquippableItemData tmpData = EquippableItemData(); + std::vector tmpExtra; + // We walk extra data and wait until we have a worn item + // before we decide we have a match. + auto simpleList = entry->extraLists; + if (simpleList) { - for (auto* extra_data : *simple_extra_data_list) + for (auto* extraData : *simpleList) { - extra = extra_data; - extra_vector.push_back(extra_data); + tmpExtra.push_back(extraData); + bool isWorn = extraData->HasType(RE::ExtraDataType::kWorn); + bool isWornLeft = extraData->HasType(RE::ExtraDataType::kWornLeft); + + if (isWornLeft) + { + matchFound = constraint == WornWhere::kLeftOnly || constraint == WornWhere::kAnywhere; + } + else if (isWorn) + { + matchFound = constraint == WornWhere::kRightOnly || constraint == WornWhere::kAnywhere; + } + + tmpData.isFavorite |= extraData->HasType(RE::ExtraDataType::kHotkey); + tmpData.isPoisoned |= extraData->HasType(RE::ExtraDataType::kPoison); + + if (isWorn) { tmpData.isWorn = true; } + else if (isWornLeft) { tmpData.isWornLeft = true; } } } + if (matchFound) + { + extraDataCopy = tmpExtra; + equipData = tmpData; + break; + } + } // end of if block + } // end of candidates loop + + if (!foundObject) { return 0; } + + rlog::debug("boundObjectForWornItem(constraint={}) found formid='{}';", + static_cast>(constraint), + rlog::formatAsHex(foundObject->formID)); + + if (extraDataCopy.size() > 0) { outextra = extraDataCopy.back(); } + outobj = foundObject; + return equipData.count; + } + + // Returns only exact name matches. + int boundObjectMatchName(const RE::TESForm* form, + const std::string& nameToMatch, + RE::TESBoundObject*& outobj, + RE::ExtraDataList* outextra) + { + const auto* baseName = form->GetName(); // this use of GetName() is okay + // If we don't need to match the name, we don't do that work. + if (std::string(baseName) == nameToMatch) { return boundObjectForForm(form, outobj, outextra); } + + auto* thePlayer = RE::PlayerCharacter::GetSingleton(); + RE::TESBoundObject* foundObject = nullptr; + EquippableItemData equipData = EquippableItemData(); + std::vector extraDataCopy; + + std::map>> candidates = + player::getInventoryForType(thePlayer, form->GetFormType()); + + for (const auto& [item, inventoryData] : candidates) + { + const auto& [countHeld, entry] = inventoryData; + if (entry->object->formID == form->formID) + { + // there are two cases where we know we have a match: + // first, when countHeld == 1 + // second, when the name matches + if (countHeld > 1) + { + const auto candidateName = std::string(entry->GetDisplayName()); + if (candidateName != nameToMatch) { continue; } + } + // we have a match. This next part is probably extractable into a function. + foundObject = item; + equipData.count = 1; + + auto simpleList = entry->extraLists; + if (simpleList) + { + for (auto* extraData : *simpleList) { extraDataCopy.push_back(extraData); } + } + break; + } // end of if block + } // end of candidates loop + + if (!foundObject) { return 0; } + + rlog::debug( + "boundObjectMatchName '{}'; found formID={};"sv, nameToMatch, rlog::formatAsHex(foundObject->formID)); + if (extraDataCopy.size() > 0) { outextra = extraDataCopy.back(); } + outobj = foundObject; + return equipData.count; + } + + // Returns first found. + int boundObjectForForm(const RE::TESForm* form, RE::TESBoundObject*& outobj, RE::ExtraDataList* outextra) + { + auto* thePlayer = RE::PlayerCharacter::GetSingleton(); + RE::TESBoundObject* foundObject = nullptr; + std::vector extraDataCopy; + + std::map>> candidates = + player::getInventoryForType(thePlayer, form->GetFormType()); + + auto count = 0; + for (const auto& [item, inventoryData] : candidates) + { + const auto& [num_items, entry] = inventoryData; + if (entry->object->formID == form->formID) + { + foundObject = item; + count = num_items; + auto simpleList = entry->extraLists; + + if (simpleList) + { + for (auto* extraData : *simpleList) { extraDataCopy.push_back(extraData); } + } break; } } - if (!bound_obj) { return 0; } + if (!foundObject) { return 0; } - rlog::trace("found {} instance for bound object; name='{}'; formID={};"sv, - item_count, - form->GetName(), - util::string_util::int_to_hex(form->formID)); + rlog::trace("found {} instance(s) for bound object; name='{}'; formID={};"sv, + count, + helpers::nameAsUtf8(form), + rlog::formatAsHex(form->formID)); - if (!extra_vector.empty()) { outextra = extra_vector.back(); } - outobj = bound_obj; - return item_count; + if (extraDataCopy.size() > 0) { outextra = extraDataCopy.back(); } + outobj = foundObject; + return count; } - bool isItemWorn(RE::TESBoundObject*& bound_obj, RE::PlayerCharacter*& the_player) + bool isItemWorn(RE::TESBoundObject*& bound_obj, RE::PlayerCharacter*& thePlayer) { auto worn = false; - for (const auto& [item, inv_data] : player::getInventoryForType(the_player, RE::FormType::Armor)) + for (const auto& [item, inv_data] : player::getInventoryForType(thePlayer, RE::FormType::Armor)) { const auto& [count, entry] = inv_data; if (entry && entry->object && (entry->object->formID == bound_obj->formID) && entry->IsWorn()) @@ -120,27 +254,24 @@ namespace game { // TODO I don't think this handles spells RE::TESBoundObject* bound_obj = nullptr; - RE::ExtraDataList* extra = nullptr; - auto* the_player = RE::PlayerCharacter::GetSingleton(); - game::boundObjectForForm(form, the_player, bound_obj, extra); - if (extra) { return extra->HasType(RE::ExtraDataType::kHotkey); } + RE::ExtraDataList* extraData = nullptr; + game::boundObjectForForm(form, bound_obj, extraData); + if (extraData) { return extraData->HasType(RE::ExtraDataType::kHotkey); } return false; } bool isItemPoisoned(const RE::TESForm* form) { - auto* the_player = RE::PlayerCharacter::GetSingleton(); - RE::TESBoundObject* obj = nullptr; - RE::ExtraDataList* extra = nullptr; - [[maybe_unused]] auto count = boundObjectForForm(form, the_player, obj, extra); - if (extra) { return extra->HasType(RE::ExtraDataType::kPoison); } + RE::TESBoundObject* obj = nullptr; + RE::ExtraDataList* extraData = nullptr; + [[maybe_unused]] auto count = boundObjectForForm(form, obj, extraData); + if (extraData) { return extraData->HasType(RE::ExtraDataType::kPoison); } return false; } float itemChargeLevel(const RE::TESForm* form) { RE::InventoryEntryData* inventoryEntry = nullptr; - if (!inventoryEntryDataFor(form, inventoryEntry)) { return 0.0f; } std::optional charge = inventoryEntry->GetEnchantmentCharge(); return static_cast(charge.value_or(0.0)); @@ -148,9 +279,9 @@ namespace game const char* displayName(const RE::TESForm* form) { - auto* the_player = RE::PlayerCharacter::GetSingleton(); + auto* thePlayer = RE::PlayerCharacter::GetSingleton(); std::map>> candidates = - player::getInventoryForType(the_player, form->GetFormType()); + player::getInventoryForType(thePlayer, form->GetFormType()); for (const auto& [item, inv_data] : candidates) { @@ -175,92 +306,89 @@ namespace game return form->GetName(); } - void equipItemByFormAndSlot(RE::TESForm* form, RE::BGSEquipSlot*& slot, RE::PlayerCharacter*& player) + void equipItemByFormAndSlot(RE::TESForm* form, + RE::BGSEquipSlot*& slot, + RE::PlayerCharacter*& thePlayer, + const std::string& nameToMatch) { auto slot_is_left = slot == left_hand_equip_slot(); - rlog::debug("attempting to equip item in slot; name='{}'; is-left='{}'; type={};"sv, - form->GetName(), + rlog::trace("attempting to equip item in slot; name='{}'; is-left='{}'; type={};"sv, + helpers::nameAsUtf8(form), slot_is_left, form->GetFormType()); if (form->formID == util::unarmed) { rlog::debug("unequipping this slot by request!"sv); - unequipLeftOrRightSlot(player, slot); + unequipLeftOrRightSlot(thePlayer, slot); return; } else if (form->Is(RE::FormType::Spell)) { - // We do not want to look for a bound object for spells. - equipSpellByFormAndSlot(form, slot, player); + // We do not want to look for a bound object for spells. Q: why not? + equipSpellByFormAndSlot(form, slot, thePlayer); return; } - RE::TESBoundObject* bound_obj = nullptr; - RE::ExtraDataList* extra = nullptr; - auto item_count = boundObjectForForm(form, player, bound_obj, extra); - if (!bound_obj) + RE::TESBoundObject* equipObject = nullptr; + RE::ExtraDataList* extraData = nullptr; + auto foundCount = boundObjectMatchName(form, nameToMatch, equipObject, extraData); + if (foundCount == 0) { - rlog::debug("unable to find bound object for name='{}'"sv, form->GetName()); + rlog::debug("unable to find bound object for name='{}'"sv, nameToMatch); return; } - const auto* obj_right = player->GetActorRuntimeData().currentProcess->GetEquippedRightHand(); - const auto* obj_left = player->GetActorRuntimeData().currentProcess->GetEquippedLeftHand(); + const auto* obj_right = thePlayer->GetActorRuntimeData().currentProcess->GetEquippedRightHand(); + const auto* obj_left = thePlayer->GetActorRuntimeData().currentProcess->GetEquippedLeftHand(); - const auto obj_equipped_left = obj_left && obj_left->formID == bound_obj->formID; - const auto obj_equipped_right = obj_right && obj_right->formID == bound_obj->formID; + const auto obj_equipped_left = obj_left && obj_left->formID == equipObject->formID; + const auto obj_equipped_right = obj_right && obj_right->formID == equipObject->formID; if (slot_is_left && obj_equipped_left) { - rlog::debug("item already equipped in left hand. name='{}'"sv, bound_obj->GetName()); + rlog::debug("item already equipped in left hand. name='{}'"sv, helpers::nameAsUtf8(equipObject)); return; } if (!slot_is_left && obj_equipped_right) { - rlog::debug("item already equipped in right hand. name='{}'"sv, bound_obj->GetName()); + rlog::debug("item already equipped in right hand. name='{}'"sv, helpers::nameAsUtf8(equipObject)); return; } auto equipped_count = 0; if (obj_equipped_left) { equipped_count++; } if (obj_equipped_right) { equipped_count++; } - rlog::debug("checking how many '{}' we have available; count={}; equipped_count={}"sv, - bound_obj->GetName(), - item_count, + rlog::trace("checking how many '{}' we have available; count={}; equipped_count={}"sv, + helpers::nameAsUtf8(equipObject), + foundCount, equipped_count); - if (item_count == equipped_count) + if (foundCount == equipped_count) { // The game might try to equip something else, according to mlthelama. - unequipLeftOrRightSlot(player, slot); + unequipLeftOrRightSlot(thePlayer, slot); return; } rlog::debug("queuing task to equip '{}'; left={}; formID={};"sv, - form->GetName(), + helpers::nameAsUtf8(form), slot_is_left, - util::string_util::int_to_hex(bound_obj->formID)); + rlog::formatAsHex(equipObject->formID)); auto* task = SKSE::GetTaskInterface(); if (task) { - task->AddTask( - [=]() { RE::ActorEquipManager::GetSingleton()->EquipObject(player, bound_obj, nullptr, 1, slot); }); + task->AddTask([=]() + { RE::ActorEquipManager::GetSingleton()->EquipObject(thePlayer, equipObject, extraData, 1, slot); }); } } void equipSpellByFormAndSlot(RE::TESForm* form, RE::BGSEquipSlot*& slot, RE::PlayerCharacter*& player) { - if (form->Is(RE::FormType::Scroll)) - { - equipItemByFormAndSlot(form, slot, player); - return; - } - auto slot_is_left = slot == left_hand_equip_slot(); - rlog::debug("attempting to equip spell in slot; name='{}'; is-left='{}'; type={};"sv, - form->GetName(), + rlog::trace("attempting to equip spell in slot; name='{}'; is-left='{}'; type={};"sv, + helpers::nameAsUtf8(form), slot_is_left, form->GetFormType()); @@ -272,13 +400,13 @@ namespace game if (slot_is_left && obj_equipped_left) { - rlog::debug("spell already equipped in left hand. name='{}'"sv, form->GetName()); + rlog::debug("spell already equipped in left hand. name='{}'"sv, helpers::nameAsUtf8(form)); return; } if (!slot_is_left && obj_equipped_right) { - rlog::debug("spell already equipped in right hand. name='{}'"sv, form->GetName()); + rlog::debug("spell already equipped in right hand. name='{}'"sv, helpers::nameAsUtf8(form)); return; } @@ -297,9 +425,9 @@ namespace game } rlog::debug("queued task to equip '{}'; left={}; formID={};"sv, - form->GetName(), + helpers::nameAsUtf8(form), slot_is_left, - util::string_util::int_to_hex(form->formID)); + rlog::formatAsHex(form->formID)); } void unequipHand(RE::PlayerCharacter*& player, Action which) diff --git a/src/game/gear.h b/src/game/gear.h index 5390601b..c65cfb84 100644 --- a/src/game/gear.h +++ b/src/game/gear.h @@ -3,10 +3,39 @@ // Equipping and unequipping armor and weapons, as well as answering questions // about equipped gear. -enum class Action : ::std::uint8_t; +#include "soulsy.h" +#include namespace game { + using namespace soulsy; + + enum class WornWhere + { + kAnywhere, + kRightOnly, + kLeftOnly, + }; + + // This struct holds useful information gleaned from item extra data, + // for convenience when building hud items, equipping an item, or + // unequipping it. If you make one, you are responsible for deleting it. + struct EquippableItemData + { + int count = 0; + bool isWorn = false; + bool isWornLeft = false; + bool isFavorite = false; + bool isPoisoned = false; + // enchantment charge? + + RE::ExtraDataList* itemExtraList = nullptr; + RE::ExtraDataList* wornExtraList = nullptr; + RE::ExtraDataList* wornLeftExtraList = nullptr; + + EquippableItemData(); + }; + // Ask the game for the right hand slot. RE::BGSEquipSlot* right_hand_equip_slot(); // Ask the game for the left hand slot. @@ -14,13 +43,27 @@ namespace game // Ask the game for the shouts/powers slot. RE::BGSEquipSlot* power_equip_slot(); - // Find a bound object matching this form in the player's inventory. Caller must provide - // pointers to bound object and extra data list references to receive found data. Returns - // the number of such items the player has in their inventory. - int boundObjectForForm(const RE::TESForm* form, - RE::PlayerCharacter*& the_player, - RE::TESBoundObject*& outval, - RE::ExtraDataList*& outextra); + // The next functions find a bound object matching this form in the player's + // inventory. Caller must provide pointers to bound object and extra data list + // references to receive found data. + // All return the number of such items the player has in their inventory. + + // Finds only items worn in the specified hand. Pass anywhere for armor or if you + // don't care which hand. + int boundObjectForWornItem(const RE::TESForm* form, + WornWhere constraint, + RE::TESBoundObject*& outobj, + RE::ExtraDataList* outextra); + + // Returns only exact name matches. + int boundObjectMatchName(const RE::TESForm* form, + const std::string& nameToMatch, + RE::TESBoundObject*& outobj, + RE::ExtraDataList* outextra); + + // Returns first found. + int boundObjectForForm(const RE::TESForm* form, RE::TESBoundObject*& outobj, RE::ExtraDataList* outextra); + // Similar to boundObjectForForm(), but fills out an inventory entry instead of extra data lists. bool inventoryEntryDataFor(const RE::TESForm* form, RE::TESBoundObject*& outobj, RE::InventoryEntryData*& outentry); @@ -36,7 +79,10 @@ namespace game const char* displayName(const RE::TESForm* form); // Equip a form in either the left or right hand. Handles weapons/shields directly, but delegates spells. - void equipItemByFormAndSlot(RE::TESForm* form, RE::BGSEquipSlot*& slot, RE::PlayerCharacter*& the_player); + void equipItemByFormAndSlot(RE::TESForm* form, + RE::BGSEquipSlot*& slot, + RE::PlayerCharacter*& the_player, + const std::string& nameToMatch); // Equip a spell in either the left or right hand. void equipSpellByFormAndSlot(RE::TESForm* form, RE::BGSEquipSlot*& slot, RE::PlayerCharacter*& the_player); @@ -46,4 +92,5 @@ namespace game // then immediately unequips the dummy dagger item (if found) to make sure the item shown // in the hand is updated properly. void unequipLeftOrRightSlot(RE::PlayerCharacter*& the_player, RE::BGSEquipSlot*& slot); + } diff --git a/src/game/magic.cpp b/src/game/magic.cpp index c9a974e7..4b72e223 100644 --- a/src/game/magic.cpp +++ b/src/game/magic.cpp @@ -1,9 +1,9 @@ #include "magic.h" +#include "RE/A/Actor.h" #include "gear.h" #include "offset.h" #include "player.h" -#include "string_util.h" namespace game { @@ -13,12 +13,14 @@ namespace game RE::PlayerCharacter*& player) { auto left = a_slot == game::left_hand_equip_slot(); - rlog::trace( - "try to work spell {}, action {}, left {}"sv, a_form->GetName(), static_cast(a_action), left); + rlog::trace("try to work spell {}, action {}, left {}"sv, + helpers::nameAsUtf8(a_form), + static_cast(a_action), + left); if (!a_form->Is(RE::FormType::Spell)) { - rlog::warn("object {} is not a spell. return."sv, a_form->GetName()); + rlog::warn("object {} is not a spell. return."sv, helpers::nameAsUtf8(a_form)); return; } @@ -26,13 +28,13 @@ namespace game if (!player->HasSpell(spell)) { - rlog::warn("player does not have spell {}. return."sv, spell->GetName()); + rlog::warn("player does not have spell {}. return."sv, helpers::nameAsUtf8(spell)); return; } //maybe check if the spell is already equipped auto casting_type = spell->GetCastingType(); - rlog::trace("spell {} is type {}"sv, spell->GetName(), static_cast(casting_type)); + rlog::trace("spell {} is type {}"sv, helpers::nameAsUtf8(spell), static_cast(casting_type)); if (a_action == action_type::instant && casting_type != RE::MagicSystem::CastingType::kConcentration) { if (true) @@ -42,7 +44,7 @@ namespace game { rlog::warn( "power/shout {} is equipped, will only cast spell in elden mode if shout slot is empty. return."sv, - selected_power->GetName()); + helpers::nameAsUtf8(selected_power)); RE::DebugNotification("Shout Slot not Empty, Skipping Spellcast"); return; } @@ -52,7 +54,8 @@ namespace game //might cost nothing if nothing has been equipped into tha hands after start, so it seems auto cost = spell->CalculateMagickaCost(actor); - rlog::trace("spell cost for {} is {}"sv, spell->GetName(), fmt::format(FMT_STRING("{:.2f}"), cost)); + rlog::trace( + "spell cost for {} is {}"sv, helpers::nameAsUtf8(spell), fmt::format(FMT_STRING("{:.2f}"), cost)); auto current_magicka = actor->AsActorValueOwner()->GetActorValue(RE::ActorValue::kMagicka); auto dual_cast = false; @@ -78,7 +81,7 @@ namespace game } else { flash_hud_meter(RE::ActorValue::kMagicka); } rlog::warn("not enough magicka for spell {}, magicka {}, cost {} return."sv, - a_form->GetName(), + helpers::nameAsUtf8(a_form), current_magicka, cost); return; @@ -97,7 +100,7 @@ namespace game auto effectiveness = 1.f; if (auto* effect = spell->GetCostliestEffectItem()) { magnitude = effect->GetMagnitude(); } rlog::trace("casting spell {}, magnitude {}, effectiveness {}"sv, - spell->GetName(), + helpers::nameAsUtf8(spell), fmt::format(FMT_STRING("{:.2f}"), magnitude), fmt::format(FMT_STRING("{:.2f}"), effectiveness)); caster->CastSpellImmediate( @@ -111,18 +114,18 @@ namespace game const auto* obj_left = player->GetActorRuntimeData().currentProcess->GetEquippedLeftHand(); if (left && obj_left && obj_left->formID == spell->formID) { - rlog::debug( - "Object Left {} is already where it should be already equipped. return."sv, spell->GetName()); + rlog::debug("Object Left {} is already where it should be already equipped. return."sv, + helpers::nameAsUtf8(spell)); return; } if (!left && obj_right && obj_right->formID == spell->formID) { - rlog::debug( - "Object Right {} is already where it should be already equipped. return."sv, spell->GetName()); + rlog::debug("Object Right {} is already where it should be already equipped. return."sv, + helpers::nameAsUtf8(spell)); return; } - rlog::trace("calling equip spell {}, left {}"sv, spell->GetName(), left); + rlog::trace("calling equip spell {}, left {}"sv, helpers::nameAsUtf8(spell), left); auto* task = SKSE::GetTaskInterface(); if (task) { @@ -130,22 +133,24 @@ namespace game } } - rlog::trace("worked spell {}, action {}. return."sv, a_form->GetName(), static_cast(a_action)); + rlog::trace( + "worked spell {}, action {}. return."sv, helpers::nameAsUtf8(a_form), static_cast(a_action)); } void cast_scroll(const RE::TESForm* form, action_type a_action, RE::PlayerCharacter*& player) { - rlog::trace("start casting scroll; name='{}'; action {}"sv, form->GetName(), static_cast(a_action)); + rlog::trace( + "start casting scroll; name='{}'; action {}"sv, helpers::nameAsUtf8(form), static_cast(a_action)); if (!form->Is(RE::FormType::Scroll)) { - rlog::warn("object {} is not a scroll. return."sv, form->GetName()); + rlog::warn("object {} is not a scroll. return."sv, helpers::nameAsUtf8(form)); return; } - RE::TESBoundObject* obj = nullptr; - RE::ExtraDataList* extra = nullptr; - auto item_count = boundObjectForForm(form, player, obj, extra); + RE::TESBoundObject* obj = nullptr; + RE::ExtraDataList* extraData = nullptr; + auto item_count = boundObjectForForm(form, obj, extraData); if (!obj || item_count == 0) { @@ -166,20 +171,20 @@ namespace game auto* task = SKSE::GetTaskInterface(); if (task) { - task->AddTask([=]() { RE::ActorEquipManager::GetSingleton()->EquipObject(player, obj); }); + task->AddTask([=]() { RE::ActorEquipManager::GetSingleton()->EquipObject(player, obj, extraData); }); } } - rlog::trace("used scroll {}, action {}. return."sv, form->GetName(), static_cast(a_action)); + rlog::trace("used scroll {}, action {}. return."sv, helpers::nameAsUtf8(form), static_cast(a_action)); } void equip_or_cast_power(RE::TESForm* a_form, action_type a_action, RE::PlayerCharacter*& player) { - rlog::trace("try to work power {}, action {}"sv, a_form->GetName(), static_cast(a_action)); + rlog::trace("try to work power {}, action {}"sv, helpers::nameAsUtf8(a_form), static_cast(a_action)); if (!a_form->Is(RE::FormType::Spell)) { - rlog::warn("object {} is not a spell. return."sv, a_form->GetName()); + rlog::warn("object {} is not a spell. return."sv, helpers::nameAsUtf8(a_form)); return; } @@ -187,12 +192,13 @@ namespace game selected_power && a_action != action_type::instant) { rlog::trace("current selected power is {}, is shout {}, is spell {}"sv, - selected_power->GetName(), + helpers::nameAsUtf8(selected_power), selected_power->Is(RE::FormType::Shout), selected_power->Is(RE::FormType::Spell)); if (selected_power->formID == a_form->formID) { - rlog::debug("no need to equip power {}, it is already equipped. return."sv, a_form->GetName()); + rlog::debug( + "no need to equip power {}, it is already equipped. return."sv, helpers::nameAsUtf8(a_form)); return; } } @@ -200,7 +206,7 @@ namespace game auto* spell = a_form->As(); if (!player->HasSpell(spell)) { - rlog::warn("player does not have spell {}. return."sv, spell->GetName()); + rlog::warn("player does not have spell {}. return."sv, helpers::nameAsUtf8(spell)); return; } @@ -210,7 +216,8 @@ namespace game task->AddTask([=]() { RE::ActorEquipManager::GetSingleton()->EquipSpell(player, spell); }); } - rlog::trace("worked power {} action {}. return."sv, a_form->GetName(), static_cast(a_action)); + rlog::trace( + "worked power {} action {}. return."sv, helpers::nameAsUtf8(a_form), static_cast(a_action)); } RE::MagicSystem::CastingSource get_casting_source(const RE::BGSEquipSlot* a_slot) diff --git a/src/game/player.cpp b/src/game/player.cpp index f4212201..1249e40c 100644 --- a/src/game/player.cpp +++ b/src/game/player.cpp @@ -7,25 +7,11 @@ #include "helpers.h" #include "offset.h" -#include "string_util.h" #include "lib.rs.h" namespace player { - using string_util = util::string_util; - - rust::Vec playerName() - { - auto* name = RE::PlayerCharacter::GetSingleton()->GetName(); - auto cbytes = helpers::chars_to_vec(name); - rust::Vec bytes; - bytes.reserve(cbytes.size() + 1); - for (auto iter = cbytes.cbegin(); iter != cbytes.cend(); iter++) { bytes.push_back(*iter); } - - return std::move(bytes); - } - bool isInCombat() { return RE::PlayerCharacter::GetSingleton()->IsInCombat(); } bool weaponsAreDrawn() { return RE::PlayerCharacter::GetSingleton()->AsActorState()->IsWeaponDrawn(); } @@ -39,19 +25,20 @@ namespace player rust::String specEquippedLeft() { - auto* player = RE::PlayerCharacter::GetSingleton(); + auto* player = RE::PlayerCharacter::GetSingleton(); + // I think this is a form already???? const auto obj = player->GetActorRuntimeData().currentProcess->GetEquippedLeftHand(); if (!obj) return std::string("unarmed_proxy"); - auto* item_form = RE::TESForm::LookupByID(obj->formID); - if (!item_form) return std::string("unarmed_proxy"); + auto* form = RE::TESForm::LookupByID(obj->formID); + if (!form) return std::string("unarmed_proxy"); - RE::TESBoundObject* bound_obj = nullptr; - RE::ExtraDataList* extra = nullptr; - game::boundObjectForForm(item_form, player, bound_obj, extra); + RE::TESBoundObject* bound = nullptr; + RE::ExtraDataList* extraData = nullptr; + game::boundObjectForWornItem(form, game::WornWhere::kLeftOnly, bound, extraData); - if (bound_obj) { return helpers::makeFormSpecString(bound_obj); } - else { return helpers::makeFormSpecString(item_form); } + if (bound) { return helpers::makeFormSpecString(bound); } + else { return helpers::makeFormSpecString(form); } } rust::String specEquippedRight() @@ -60,15 +47,15 @@ namespace player const auto obj = player->GetActorRuntimeData().currentProcess->GetEquippedRightHand(); if (!obj) return std::string("unarmed_proxy"); - auto* item_form = RE::TESForm::LookupByID(obj->formID); - if (!item_form) return std::string("unarmed_proxy"); + auto* form = RE::TESForm::LookupByID(obj->formID); + if (!form) return std::string("unarmed_proxy"); - RE::TESBoundObject* bound_obj = nullptr; - RE::ExtraDataList* extra = nullptr; - game::boundObjectForForm(item_form, player, bound_obj, extra); + RE::TESBoundObject* bound = nullptr; + RE::ExtraDataList* extraData = nullptr; + game::boundObjectForWornItem(form, game::WornWhere::kRightOnly, bound, extraData); - if (bound_obj) { return helpers::makeFormSpecString(bound_obj); } - else { return helpers::makeFormSpecString(item_form); } + if (bound) { return helpers::makeFormSpecString(bound); } + else { return helpers::makeFormSpecString(form); } } rust::String specEquippedPower() @@ -175,29 +162,29 @@ namespace player game::equipSpellByFormAndSlot(form, equip_slot, player); } - void equipWeapon(const std::string& form_spec, Action slot) + void equipWeapon(const std::string& form_spec, Action slot, const std::string& nameToMatch) { auto* form = helpers::formSpecToFormItem(form_spec); if (!form) { return; } auto* player = RE::PlayerCharacter::GetSingleton(); auto* equip_slot = (slot == Action::Left ? game::left_hand_equip_slot() : game::right_hand_equip_slot()); - game::equipItemByFormAndSlot(form, equip_slot, player); + game::equipItemByFormAndSlot(form, equip_slot, player, nameToMatch); } - void toggleArmor(const std::string& form_spec) + void toggleArmor(const std::string& form_spec, const std::string& nameToMatch) { auto* form = helpers::formSpecToFormItem(form_spec); if (!form) { return; } auto* player = RE::PlayerCharacter::GetSingleton(); - game::toggleArmorByForm(form, player); + game::toggleArmorByForm(form, player, nameToMatch); } - void equipArmor(const std::string& form_spec) + void equipArmor(const std::string& form_spec, const std::string& nameToMatch) { auto* form = helpers::formSpecToFormItem(form_spec); if (!form) { return; } auto* player = RE::PlayerCharacter::GetSingleton(); - game::equipArmorByForm(form, player); + game::equipArmorByForm(form, player, nameToMatch); } void equipAmmo(const std::string& form_spec) @@ -236,7 +223,7 @@ namespace player auto* player = RE::PlayerCharacter::GetSingleton(); count = inventoryCount(form, form->GetFormType(), player); - // rlog::trace("item='{}'; count={};"sv, form->GetName(), count); + // rlog::trace("item='{}'; count={};"sv, helpers::nameAsUtf8(form), count); return count; } @@ -272,38 +259,19 @@ namespace player has_it = has_shout(player, shout); } - rlog::debug("player has: {}; name='{}'; formID={};"sv, has_it, form->GetName(), form_spec); + rlog::debug("player has: {}; name='{}'; formID={};"sv, has_it, helpers::nameAsUtf8(form), form_spec); return has_it; } - void reequipHand(Action which, const std::string& form_spec) + void reequipHand(Action which, const std::string& form_spec, const std::string& nameToMatch) { auto* form = helpers::formSpecToFormItem(form_spec); if (!form) { return; } - auto* player = RE::PlayerCharacter::GetSingleton(); - - RE::TESBoundObject* bound_obj = nullptr; - RE::ExtraDataList* extra = nullptr; - game::boundObjectForForm(form, player, bound_obj, extra); - if (!bound_obj) { return; } - - rlog::info("Re-equipping item in left hand; name='{}'; formID={}"sv, - form->GetName(), - util::string_util::int_to_hex(form->formID)); - RE::BGSEquipSlot* slot; - - if (which == Action::Left) { slot = game::left_hand_equip_slot(); } - else { slot = game::right_hand_equip_slot(); } - - - auto* task = SKSE::GetTaskInterface(); - if (task) - { - task->AddTask( - [=]() { RE::ActorEquipManager::GetSingleton()->EquipObject(player, bound_obj, extra, 1, slot); }); - } + auto* thePlayer = RE::PlayerCharacter::GetSingleton(); + auto* slot = which == Action::Left ? game::left_hand_equip_slot() : game::right_hand_equip_slot(); + game::equipItemByFormAndSlot(form, slot, thePlayer, nameToMatch); } uint32_t inventoryCount(const RE::TESForm* a_form, RE::FormType a_type, RE::PlayerCharacter*& a_player) diff --git a/src/game/player.h b/src/game/player.h index 0d8daa47..c0d7eee8 100644 --- a/src/game/player.h +++ b/src/game/player.h @@ -3,10 +3,7 @@ #include "helpers.h" #include "rust/cxx.h" - -struct HudItem; -struct EquippedData; -enum class Action : ::std::uint8_t; +#include "soulsy.h" namespace player { @@ -28,8 +25,6 @@ namespace player rust::Box getEquippedItems(); - rust::Vec playerName(); - bool isInCombat(); bool weaponsAreDrawn(); bool hasRangedEquipped(); @@ -38,13 +33,13 @@ namespace player void unequipShout(); void equipShout(const std::string& form_spec); bool has_shout(RE::Actor* a_actor, RE::TESShout* a_shout); - void reequipHand(Action which, const std::string& form_spec); - void toggleArmor(const std::string& form_spec); - void equipArmor(const std::string& form_spec); - void unequipSlotByShift(uint8_t shift); + void reequipHand(Action which, const std::string& form_spec, const std::string& nameToMatch); + void equipWeapon(const std::string& form_spec, Action slot, const std::string& nameToMatch); void equipMagic(const std::string& form_spec, Action slot); - void equipWeapon(const std::string& form_spec, Action slot); void equipAmmo(const std::string& form_spec); + void toggleArmor(const std::string& form_spec, const std::string& nameToMatch); + void equipArmor(const std::string& form_spec, const std::string& nameToMatch); + void unequipSlotByShift(uint8_t shift); void consumePotion(const std::string& form_spec); diff --git a/src/game/shouts.cpp b/src/game/shouts.cpp index 8728413b..c9292603 100644 --- a/src/game/shouts.cpp +++ b/src/game/shouts.cpp @@ -2,7 +2,6 @@ #include "offset.h" #include "player.h" -#include "string_util.h" // For game implementation reasons, this also includes spells. // Lesser powers are spells that go into the shout slot, IIUC. @@ -37,7 +36,7 @@ namespace game { auto* task = SKSE::GetTaskInterface(); if (!task) return; - rlog::trace("unequipping shout/power formID={};"sv, util::string_util::int_to_hex(selected_power->formID)); + rlog::trace("unequipping shout/power formID={};"sv, rlog::formatAsHex(selected_power->formID)); if (selected_power->Is(RE::FormType::Shout)) { task->AddTask( @@ -58,16 +57,16 @@ namespace game void equipShoutByForm(RE::TESForm* form, RE::PlayerCharacter*& player) { - // rlog::trace("tring to equip shout; name='{}';"sv, form->GetName()); + // rlog::trace("tring to equip shout; name='{}';"sv, helpers::nameAsUtf8(form)); if (const auto selected_power = player->GetActorRuntimeData().selectedPower; selected_power) { rlog::trace("current power: name='{}'; is-shout={}; is-spell={};"sv, - selected_power->GetName(), + helpers::nameAsUtf8(selected_power), selected_power->Is(RE::FormType::Shout), selected_power->Is(RE::FormType::Spell)); if (selected_power->formID == form->formID) { - rlog::trace("shout already equipped; moving on."sv, form->GetName()); + rlog::trace("shout already equipped; moving on."sv, helpers::nameAsUtf8(form)); return; } } @@ -77,11 +76,11 @@ namespace game if (form->Is(RE::FormType::Spell)) { - rlog::debug("equipping lesser power name='{}';"sv, form->GetName()); + rlog::debug("equipping lesser power name='{}';"sv, helpers::nameAsUtf8(form)); auto* spell = form->As(); if (!player->HasSpell(spell)) { - rlog::warn("player does not know lesser power; name='{}';"sv, spell->GetName()); + rlog::warn("player does not know lesser power; name='{}';"sv, helpers::nameAsUtf8(spell)); return; } @@ -92,11 +91,11 @@ namespace game auto* shout = form->As(); if (!player::has_shout(player, shout)) { - rlog::warn("player does not know shout; name='{}';"sv, shout->GetName()); + rlog::warn("player does not know shout; name='{}';"sv, helpers::nameAsUtf8(shout)); return; } task->AddTask([=]() { RE::ActorEquipManager::GetSingleton()->EquipShout(player, shout); }); - rlog::debug("shout equipped! name='{}'"sv, form->GetName()); + rlog::debug("shout equipped! name='{}'"sv, helpers::nameAsUtf8(form)); } } diff --git a/src/game/utility.cpp b/src/game/utility.cpp index f6d3d49b..ce7c3d4e 100644 --- a/src/game/utility.cpp +++ b/src/game/utility.cpp @@ -1,41 +1,41 @@ #include "utility.h" +#include "RE/A/Actor.h" #include "constant.h" #include "equippable.h" #include "gear.h" #include "helpers.h" #include "player.h" -#include "string_util.h" #include "lib.rs.h" +using namespace soulsy; + namespace game { - using string_util = util::string_util; - // ---------- ammo void equipAmmoByForm(const RE::TESForm* form, RE::PlayerCharacter*& thePlayer) { - RE::TESBoundObject* obj = nullptr; - RE::ExtraDataList* extra = nullptr; - auto remaining = boundObjectForForm(form, thePlayer, obj, extra); + RE::TESBoundObject* obj = nullptr; + RE::ExtraDataList* extraData = nullptr; + auto remaining = boundObjectForForm(form, obj, extraData); if (!obj || remaining == 0) { - rlog::warn("Ammo not found in inventory! name='{}';"sv, form->GetName()); + rlog::warn("Ammo not found in inventory! name='{}';"sv, helpers::nameAsUtf8(form)); return; } if (const auto* current_ammo = thePlayer->GetCurrentAmmo(); current_ammo && current_ammo->formID == obj->formID) { - // rlog::trace("ammo is already equipped; bound formID={}"sv, string_util::int_to_hex(obj->formID)); + // rlog::trace("ammo is already equipped; bound formID={}"sv, rlog::formatAsHex(obj->formID)); return; } rlog::debug("queuing task to equip ammo; name='{}'; bound formID={}"sv, - obj->GetName(), - string_util::int_to_hex(obj->formID)); + helpers::nameAsUtf8(obj), + rlog::formatAsHex(obj->formID)); auto* task = SKSE::GetTaskInterface(); if (task) { @@ -59,9 +59,8 @@ namespace game { task->AddTask([=]() { RE::ActorEquipManager::GetSingleton()->UnequipObject(thePlayer, ammo); }); } - rlog::debug("ammo unequipped; name='{}'; formID={}"sv, - ammo->GetName(), - util::string_util::int_to_hex(ammo->formID)); + rlog::debug( + "ammo unequipped; name='{}'; formID={}"sv, helpers::nameAsUtf8(ammo), rlog::formatAsHex(ammo->formID)); } } @@ -77,63 +76,56 @@ namespace game { task->AddTask([=]() { equipManager->UnequipObject(thePlayer, item); }); } - // rlog::trace("unequipped armor; name='{}';"sv, item->GetName()); + // rlog::trace("unequipped armor; name='{}';"sv, helpers::nameAsUtf8(item)); } return isWorn; } - void toggleArmorByForm(const RE::TESForm* form, RE::PlayerCharacter*& thePlayer) + void toggleArmorByForm(const RE::TESForm* form, RE::PlayerCharacter*& thePlayer, const std::string& nameToMatch) { // This is a toggle in reality. Also, use this as a model for other equip funcs. - // rlog::trace("attempting to toggle armor; name='{}';"sv, form->GetName()); - RE::TESBoundObject* obj = nullptr; - RE::ExtraDataList* extra = nullptr; - auto remaining = boundObjectForForm(form, thePlayer, obj, extra); + // rlog::trace("attempting to toggle armor; name='{}';"sv, helpers::nameAsUtf8(form)); + RE::TESBoundObject* obj = nullptr; + RE::ExtraDataList* extraData = nullptr; + auto remaining = boundObjectMatchName(form, nameToMatch, obj, extraData); if (!obj || remaining == 0) { - rlog::warn("could not find armor in player inventory; name='{}';"sv, form->GetName()); - return; - } - - auto* task = SKSE::GetTaskInterface(); - if (!task) - { - rlog::warn("could not find SKSE task interface! Cannot act."sv); + rlog::warn("could not find armor in player inventory; name='{}';"sv, nameToMatch); return; } - const auto is_worn = isItemWorn(obj, thePlayer); - auto* equip_manager = RE::ActorEquipManager::GetSingleton(); - if (is_worn) + auto* task = SKSE::GetTaskInterface(); + auto* equipManager = RE::ActorEquipManager::GetSingleton(); + const auto isWorn = isItemWorn(obj, thePlayer); + if (isWorn) { - task->AddTask([=]() { equip_manager->UnequipObject(thePlayer, obj); }); + task->AddTask([=]() { equipManager->UnequipObject(thePlayer, obj, extraData); }); } else { - task->AddTask([=]() { equip_manager->EquipObject(thePlayer, obj); }); + task->AddTask([=]() { equipManager->EquipObject(thePlayer, obj, extraData); }); } } - void equipArmorByForm(const RE::TESForm* form, RE::PlayerCharacter*& thePlayer) + void equipArmorByForm(const RE::TESForm* form, RE::PlayerCharacter*& thePlayer, const std::string& nameToMatch) { - // rlog::trace("attempting to equip armor; name='{}';"sv, form->GetName()); - RE::TESBoundObject* obj = nullptr; - RE::ExtraDataList* extra = nullptr; - auto remaining = boundObjectForForm(form, thePlayer, obj, extra); + // rlog::trace("attempting to equip armor; name='{}';"sv, helpers::nameAsUtf8(form)); + RE::TESBoundObject* obj = nullptr; + RE::ExtraDataList* extraData = nullptr; + auto remaining = boundObjectMatchName(form, nameToMatch, obj, extraData); if (!obj || remaining == 0) { - rlog::warn("could not find armor in player inventory; name='{}';"sv, form->GetName()); + rlog::warn("could not find armor in player inventory; name='{}';"sv, nameToMatch); return; } - const auto is_worn = isItemWorn(obj, thePlayer); - if (!is_worn) + if (!isItemWorn(obj, thePlayer)) { auto* task = SKSE::GetTaskInterface(); auto* equipManager = RE::ActorEquipManager::GetSingleton(); - task->AddTask([=]() { equipManager->EquipObject(thePlayer, obj); }); + task->AddTask([=]() { equipManager->EquipObject(thePlayer, obj, extraData); }); } } @@ -142,12 +134,12 @@ namespace game void consumePotion(const RE::TESForm* potionForm, RE::PlayerCharacter*& thePlayer) { rlog::trace("consumePotion called; form_id={}; potion='{}';"sv, - util::string_util::int_to_hex(potionForm->formID), - potionForm->GetName()); + rlog::formatAsHex(potionForm->formID), + helpers::nameAsUtf8(potionForm)); - RE::TESBoundObject* obj = nullptr; - RE::ExtraDataList* extra = nullptr; - auto remaining = boundObjectForForm(potionForm, thePlayer, obj, extra); + RE::TESBoundObject* obj = nullptr; + RE::ExtraDataList* extraData = nullptr; + auto remaining = boundObjectForForm(potionForm, obj, extraData); if (!obj || remaining == 0) { @@ -160,26 +152,27 @@ namespace game { helpers::honk(); rlog::warn("bound object is not an alchemy item? name='{}'; formID={};"sv, - obj->GetName(), - string_util::int_to_hex(obj->formID)); + helpers::nameAsUtf8(obj), + rlog::formatAsHex(obj->formID)); return; } auto* alchemyItem = obj->As(); if (alchemyItem->IsPoison()) { - poisonWeapon(thePlayer, alchemyItem, remaining); + poisonWeapon(thePlayer, alchemyItem, remaining, extraData); return; } auto* task = SKSE::GetTaskInterface(); if (!task) { return; } - task->AddTask([=]() { RE::ActorEquipManager::GetSingleton()->EquipObject(thePlayer, alchemyItem); }); + task->AddTask([=]() { RE::ActorEquipManager::GetSingleton()->EquipObject(thePlayer, alchemyItem, extraData); }); } void poisonWeapon(RE::PlayerCharacter*& thePlayer, RE::AlchemyItem*& poison, - uint32_t remaining) + uint32_t remaining, + RE::ExtraDataList* extraData) { auto* task = SKSE::GetTaskInterface(); if (!task) { return; } @@ -188,9 +181,10 @@ namespace game if (right_eq && right_eq->IsWeapon()) { task->AddTask( - [=]() { + [=]() + { RE::ActorEquipManager::GetSingleton()->EquipObject( - thePlayer, poison, nullptr, 1, game::right_hand_equip_slot()); + thePlayer, poison, extraData, 1, game::right_hand_equip_slot()); }); remaining--; } @@ -200,7 +194,7 @@ namespace game task->AddTask( [=]() { RE::ActorEquipManager::GetSingleton()->EquipObject( - thePlayer, poison, nullptr, 1, game::left_hand_equip_slot()); + thePlayer, poison, extraData, 1, game::left_hand_equip_slot()); }); } } @@ -307,7 +301,7 @@ namespace game rlog::debug("after considering {} candidates, found a potion: rating={}; name='{}';"sv, vitalStat, prevRating, - obj->GetName()); + helpers::nameAsUtf8(obj)); auto* task = SKSE::GetTaskInterface(); if (task) { @@ -324,14 +318,14 @@ namespace game // ---------- perk visitor, used only by the actor value potion selection using PerkFuncType = RE::BGSEntryPointPerkEntry::EntryData::Function; - using PerkFuncDataType = RE::BGSEntryPointFunctionData::FunctionType; + using PerkFuncDataType = RE::BGSEntryPointFunctionData::ENTRY_POINT_FUNCTION_DATA; RE::BSContainer::ForEachResult perk_visitor::Visit(RE::BGSPerkEntry* perk_entry) { const auto* entry_point = static_cast(perk_entry); const auto* perk = entry_point->perk; - rlog::trace("perk formID={}; name='{}';"sv, string_util::int_to_hex(perk->formID), perk->GetName()); + rlog::trace("perk formID={}; name='{}';"sv, rlog::formatAsHex(perk->formID), helpers::nameAsUtf8(perk)); // This was originally intended to handle many variations of the poison // dose perk-- it should calculate the correct value from vanilla, diff --git a/src/game/utility.h b/src/game/utility.h index bc6f84df..4b1b62d6 100644 --- a/src/game/utility.h +++ b/src/game/utility.h @@ -8,9 +8,9 @@ namespace game void unequipCurrentAmmo(); // Equip this armor. Does nothing if it's already equipped. - void equipArmorByForm(const RE::TESForm* form, RE::PlayerCharacter*& thePlayer); + void equipArmorByForm(const RE::TESForm* form, RE::PlayerCharacter*& thePlayer, const std::string& nameToMatch); // Equip if unequipped, un-equip if equipped already. - void toggleArmorByForm(const RE::TESForm* form, RE::PlayerCharacter*& thePlayer); + void toggleArmorByForm(const RE::TESForm* form, RE::PlayerCharacter*& thePlayer, const std::string& nameToMatch); // reurns true if anything was unequipped. bool unequipArmor(RE::TESBoundObject*& a_obj, RE::PlayerCharacter*& thePlayer, @@ -18,7 +18,10 @@ namespace game void consumePotion(const RE::TESForm* a_form, RE::PlayerCharacter*& thePlayer); void consumeBestOption(RE::ActorValue a_actor_value); - void poisonWeapon(RE::PlayerCharacter*& thePlayer, RE::AlchemyItem*& a_poison, uint32_t remaining); + void poisonWeapon(RE::PlayerCharacter*& thePlayer, + RE::AlchemyItem*& a_poison, + uint32_t remaining, + RE::ExtraDataList* extraData); void playSound(RE::BGSSoundDescriptor* a_sound_descriptor_form, RE::PlayerCharacter*& thePlayer); diff --git a/src/images/icons.rs b/src/images/icons.rs index fba42234..69d31872 100644 --- a/src/images/icons.rs +++ b/src/images/icons.rs @@ -2,7 +2,7 @@ //! //! To add a new icon, you must add a variant name here AND add the variant to //! `fallback()`, so the HUD renders something if the file is missing (e.g., an -//! icon pack dose not include it). If the icon is added to the core set in the +//! icon pack does not include it). If the icon is added to the core set in the //! main mod, it is its own fallback and can be a fallback target. (A test //! validates that all fallbacks are valid.) //! @@ -109,16 +109,69 @@ pub enum Icon { Scroll, Shout, ShoutAnimalAllegiance, + ShoutAuraWhisper, + ShoutBattleFury, + ShoutBecomeEthereal, + ShoutBendWill, ShoutBreathAttack, ShoutCallDragon, + ShoutCallOfValor, ShoutClearSkies, ShoutCyclone, + ShoutDisarm, ShoutDismay, + ShoutDragonAspect, + ShoutDragonrend, + ShoutDrainVitality, ShoutElementalFury, + ShoutFireBreath, + ShoutFrostBreath, ShoutIceForm, + ShoutKynesPeace, ShoutMarkedForDeath, + ShoutPhantomForm, + ShoutSlowtime, + ShoutSoulTear, ShoutStormcall, + ShoutSummonDurnehviir, + ShoutThrowVoice, ShoutUnrelentingForce, + ShoutWhirlwindSprint, + ShoutSoulCairnSummon, + // Stormcrown's modest additions. + ShoutLightningBreath, + ShoutPoisonBreath, + // These are Thunderchild shouts. + ShoutAlessiasLove, + ShoutAnnihilate, + ShoutArcaneHelix, + ShoutArmageddon, + ShoutCurse, + ShoutDanceOfTheDead, + ShoutEarthquake, + ShoutEssenceRip, + ShoutEvocation, + ShoutGeomagnetism, + ShoutIceborn, + ShoutJonesShadow, + ShoutKingsbane, + ShoutLifestream, + ShoutLightningShield, + ShoutOblivion, + ShoutPhantomDecoy, + ShoutRiftwalk, + ShoutShattersphere, + ShoutShorsWrath, + ShoutShroudOfSnowfall, + ShoutSpeakUntoTheStars, + ShoutSplinterTwins, + ShoutStormblast, + ShoutTheConqueror, + ShoutTrueshot, + ShoutWailOfTheBanshee, + ShoutWanderlust, + ShoutWarcry, + // SpellArcane, SpellArclight, // SpellBlast, // not yet used @@ -322,16 +375,68 @@ impl Icon { // Shout. Shout. Let it all out. Icon::Shout => Icon::Shout, Icon::ShoutAnimalAllegiance => Icon::Shout, + Icon::ShoutAuraWhisper => Icon::Shout, + Icon::ShoutBattleFury => Icon::Shout, + Icon::ShoutBecomeEthereal => Icon::Shout, + Icon::ShoutBendWill => Icon::Shout, Icon::ShoutBreathAttack => Icon::Shout, + Icon::ShoutCallDragon => Icon::Shout, + Icon::ShoutCallOfValor => Icon::Shout, Icon::ShoutClearSkies => Icon::Shout, Icon::ShoutCyclone => Icon::Shout, - Icon::ShoutCallDragon => Icon::Shout, + Icon::ShoutDisarm => Icon::Shout, Icon::ShoutDismay => Icon::Shout, + Icon::ShoutDragonAspect => Icon::Shout, + Icon::ShoutDragonrend => Icon::Shout, + Icon::ShoutDrainVitality => Icon::Shout, Icon::ShoutElementalFury => Icon::Shout, - Icon::ShoutIceForm => Icon::Destruction, + Icon::ShoutFireBreath => Icon::Shout, + Icon::ShoutFrostBreath => Icon::Shout, + Icon::ShoutIceForm => Icon::Shout, + Icon::ShoutKynesPeace => Icon::Shout, Icon::ShoutMarkedForDeath => Icon::Shout, + Icon::ShoutPhantomForm => Icon::Shout, + Icon::ShoutSlowtime => Icon::Shout, + Icon::ShoutSoulTear => Icon::Shout, Icon::ShoutStormcall => Icon::Shout, + Icon::ShoutSummonDurnehviir => Icon::Shout, + Icon::ShoutThrowVoice => Icon::Shout, Icon::ShoutUnrelentingForce => Icon::Shout, + Icon::ShoutWhirlwindSprint => Icon::Shout, + Icon::ShoutSoulCairnSummon => Icon::Shout, + // stormcrown + Icon::ShoutLightningBreath => Icon::Shout, + Icon::ShoutPoisonBreath => Icon::Shout, + // thunderchild's massive shout list + Icon::ShoutAlessiasLove => Icon::Shout, + Icon::ShoutAnnihilate => Icon::Shout, + Icon::ShoutArcaneHelix => Icon::Shout, + Icon::ShoutArmageddon => Icon::Shout, + Icon::ShoutCurse => Icon::Shout, + Icon::ShoutDanceOfTheDead => Icon::Shout, + Icon::ShoutEarthquake => Icon::Shout, + Icon::ShoutEssenceRip => Icon::Shout, + Icon::ShoutEvocation => Icon::Shout, + Icon::ShoutGeomagnetism => Icon::Shout, + Icon::ShoutIceborn => Icon::Shout, + Icon::ShoutJonesShadow => Icon::Shout, + Icon::ShoutKingsbane => Icon::Shout, + Icon::ShoutLifestream => Icon::Shout, + Icon::ShoutLightningShield => Icon::Shout, + Icon::ShoutOblivion => Icon::Shout, + Icon::ShoutPhantomDecoy => Icon::Shout, + Icon::ShoutRiftwalk => Icon::Shout, + Icon::ShoutShattersphere => Icon::Shout, + Icon::ShoutShorsWrath => Icon::Shout, + Icon::ShoutShroudOfSnowfall => Icon::Shout, + Icon::ShoutSpeakUntoTheStars => Icon::Shout, + Icon::ShoutSplinterTwins => Icon::Shout, + Icon::ShoutStormblast => Icon::Shout, + Icon::ShoutTheConqueror => Icon::Shout, + Icon::ShoutTrueshot => Icon::Shout, + Icon::ShoutWailOfTheBanshee => Icon::Shout, + Icon::ShoutWanderlust => Icon::Shout, + Icon::ShoutWarcry => Icon::Shout, // Most spells won't ever reach this because they'll fall back to their // schools, but just in case. @@ -502,6 +607,7 @@ mod tests { use std::path::PathBuf; use std::str::FromStr; + use once_cell::sync::Lazy; use strum::VariantNames; use super::*; @@ -601,6 +707,120 @@ mod tests { assert!(missing.is_empty(), "{missing:#?}"); } + // sanity-checking the rune icon packs before I hand them over + + static VANILLA_SHOUTS: Lazy> = Lazy::new(|| { + vec![ + Icon::ShoutAnimalAllegiance, + Icon::ShoutAuraWhisper, + Icon::ShoutBattleFury, + Icon::ShoutBecomeEthereal, + Icon::ShoutBendWill, + Icon::ShoutBreathAttack, + Icon::ShoutCallDragon, + Icon::ShoutCallOfValor, + Icon::ShoutClearSkies, + Icon::ShoutCyclone, + Icon::ShoutDisarm, + Icon::ShoutDismay, + Icon::ShoutDragonAspect, + Icon::ShoutDragonrend, + Icon::ShoutDrainVitality, + Icon::ShoutElementalFury, + Icon::ShoutFireBreath, + Icon::ShoutFrostBreath, + Icon::ShoutIceForm, + Icon::ShoutKynesPeace, + Icon::ShoutMarkedForDeath, + Icon::ShoutPhantomForm, + Icon::ShoutSlowtime, + Icon::ShoutSoulTear, + Icon::ShoutStormcall, + Icon::ShoutSummonDurnehviir, + Icon::ShoutThrowVoice, + Icon::ShoutUnrelentingForce, + Icon::ShoutWhirlwindSprint, + ] + }); + + #[test] + #[ignore] + fn validate_rune_icons() { + let missing: Vec<&Icon> = VANILLA_SHOUTS + .iter() + .filter(|icon| { + let fpath: PathBuf = ["layouts/unused/soulsy_vanilla", icon.icon_file().as_str()] + .iter() + .collect(); + + if !fpath.exists() { + eprintln!("{icon:?} missing: vanilla rune shout"); + true + } else { + false + } + }) + .collect(); + assert!(missing.is_empty()); + } + + #[test] + #[ignore] + fn validate_thunderchild_icons() { + let mut thunderchild_shouts: Vec = vec![ + Icon::ShoutAlessiasLove, + Icon::ShoutAnnihilate, + Icon::ShoutArcaneHelix, + Icon::ShoutArmageddon, + Icon::ShoutCurse, + Icon::ShoutDanceOfTheDead, + Icon::ShoutEarthquake, + Icon::ShoutEssenceRip, + Icon::ShoutEvocation, + Icon::ShoutGeomagnetism, + Icon::ShoutIceborn, + Icon::ShoutJonesShadow, + Icon::ShoutKingsbane, + Icon::ShoutLifestream, + Icon::ShoutLightningShield, + Icon::ShoutOblivion, + Icon::ShoutPhantomDecoy, + Icon::ShoutRiftwalk, + Icon::ShoutShattersphere, + Icon::ShoutShorsWrath, + Icon::ShoutShroudOfSnowfall, + Icon::ShoutSpeakUntoTheStars, + Icon::ShoutSplinterTwins, + Icon::ShoutStormblast, + Icon::ShoutTheConqueror, + Icon::ShoutTrueshot, + Icon::ShoutWailOfTheBanshee, + Icon::ShoutWanderlust, + Icon::ShoutWarcry, + ]; + thunderchild_shouts.extend_from_slice(VANILLA_SHOUTS.as_slice()); + + let missing: Vec<&Icon> = thunderchild_shouts + .iter() + .filter(|icon| { + let fpath: PathBuf = [ + "layouts/unused/soulsy_thunderchild", + icon.icon_file().as_str(), + ] + .iter() + .collect(); + + if !fpath.exists() { + eprintln!("{icon:?} missing: thunderchild shout"); + true + } else { + false + } + }) + .collect(); + assert!(missing.is_empty()); + } + #[test] #[ignore] fn emit_icon_files() { diff --git a/src/lib.rs b/src/lib.rs index 5ec9ca14..f3237e8e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,7 @@ use layouts::hud_layout; /// affordances of the `cxx` crate. At build time `cxx_build` generates the /// header files required by the C++ side. The macros expand in-line to generate /// the matching Rust code. -#[cxx::bridge] +#[cxx::bridge(namespace = "soulsy")] pub mod plugin { // ceejbot says: organize into namespaces; getting pretty cluttered @@ -250,6 +250,9 @@ pub mod plugin { /// Log at trace level. Use this level for debugging programming problems. fn log_trace(message: String); + /// Decode a vector of bytes from a windows codepage string to a utf-8 string. + fn string_to_utf8(bytes: &CxxVector) -> String; + /// Trigger rust to read config, figure out what the player has equipped, /// and figure out what it should draw. fn initialize_hud(); @@ -306,10 +309,6 @@ pub mod plugin { fn color(self: &HudItem) -> Color; /// Get the item name as a possibly-lossy utf8 string. fn name(self: &HudItem) -> String; - /// Check if the item name is representable in utf8. - fn name_is_utf8(self: &HudItem) -> bool; - /// Get the underlying bytes of a possibly non-utf8 name for this item. - fn name_bytes(self: &HudItem) -> Vec; /// Get the form spec string for this item; format is `Plugin.esp|0xdeadbeef` fn form_string(self: &HudItem) -> String; /// Get how many of this item the player has. Updated on inventory changes. @@ -335,16 +334,21 @@ pub mod plugin { which: ItemCategory, spelldata: Box, keywords: &CxxVector, - bytes_ffi: &CxxVector, + name: String, form_string: String, count: u32, ) -> Box; + fn categorize_shout( + keywords: &CxxVector, + name: String, + form_string: String, + ) -> Box; /// Build a HUD item from a rough category and a list of keywords from OCF and other mods. fn hud_item_from_keywords( category: ItemCategory, keywords: &CxxVector, - bytes_ffi: &CxxVector, + name: String, form_string: String, count: u32, twohanded: bool, @@ -354,14 +358,14 @@ pub mod plugin { is_poison: bool, effect: i32, count: u32, - bytes_ffi: &CxxVector, + name: String, form_string: String, ) -> Box; /// Build a very simple item, one where the rough category can specify everything. Only used /// now for lights & shouts as a fallback. fn simple_from_formdata( kind: ItemCategory, - bytes_ffi: &CxxVector, + name: String, form_string: String, ) -> Box; /// Build an empty HUD item. @@ -500,9 +504,6 @@ pub mod plugin { unsafe extern "C++" { include!("player.h"); - /// Get the player's name as a vec of wide bytes. Might not be valid utf8. - fn playerName() -> Vec; - /// Is the player in combat? fn isInCombat() -> bool; /// Are the player's weapons drawn? @@ -539,13 +540,13 @@ pub mod plugin { /// Equip the spell matching the form spec. fn equipMagic(form_spec: &CxxString, which: Action); /// Equip the weapon matching the form spec. - fn equipWeapon(form_spec: &CxxString, which: Action); + fn equipWeapon(form_spec: &CxxString, which: Action, name: &CxxString); /// Re-equip an item in the left hand. This forces an un-equip first. - fn reequipHand(which: Action, form_spec: &CxxString); + fn reequipHand(which: Action, form_spec: &CxxString, name: &CxxString); /// Toggle the armor matching the form spec. - fn toggleArmor(form_spec: &CxxString); + fn toggleArmor(form_spec: &CxxString, name: &CxxString); /// Equip the armor; do not toggle. - fn equipArmor(form_spec: &CxxString); + fn equipArmor(form_spec: &CxxString, name: &CxxString); /// Equip the ammo matching the form spec. fn equipAmmo(form_spec: &CxxString); /// Potions great and small. diff --git a/src/plugin/inventory.cpp b/src/plugin/inventory.cpp index bd44c715..789163f2 100644 --- a/src/plugin/inventory.cpp +++ b/src/plugin/inventory.cpp @@ -2,7 +2,6 @@ #include "equippable.h" #include "gear.h" -#include "string_util.h" #include "lib.rs.h" diff --git a/src/plugin/log.h b/src/plugin/log.h index 0a17c928..8726c7d4 100644 --- a/src/plugin/log.h +++ b/src/plugin/log.h @@ -4,6 +4,24 @@ namespace rlog { + static void leftTrim(std::string& s) + { + s.erase(s.begin(), std::ranges::find_if(s, [](const unsigned char ch) { return !std::isspace(ch); })); + } + + static std::string leftTrimCopy(std::string s) + { + leftTrim(s); + return s; + } + + template + std::string formatAsHex(T xs) + { + std::stringstream stream; + stream << "0x" << std::hex << std::setw(8) << std::setfill('0') << xs; + return leftTrimCopy(stream.str()); + } template struct [[maybe_unused]] critical diff --git a/src/plugin/menus.cpp b/src/plugin/menus.cpp index cf86ee36..0079bb4f 100644 --- a/src/plugin/menus.cpp +++ b/src/plugin/menus.cpp @@ -3,7 +3,7 @@ #include "equippable.h" #include "gear.h" #include "keycodes.h" -#include "util/string_util.h" +#include "log.h" #include "lib.rs.h" @@ -22,7 +22,7 @@ inline const std::set RELEVANT_FORMTYPES_ALL{ void MenuHook::install() { - rlog::info("Hooking menus to get keystrokes..."sv); + rlog::info("Hooking menus to get keystrokes..."); REL::Relocation menu_controls_vtbl{ RE::VTABLE_MenuControls[0] }; process_event_ = menu_controls_vtbl.write_vfunc(0x1, &MenuHook::process_event); @@ -87,7 +87,7 @@ RE::BSEventNotifyControl MenuHook::process_event(RE::InputEvent** eventPtr, if (!menu_form) continue; rlog::debug("Got toggled favorite: form_id={}; form_type={}; is-favorited={};"sv, - util::string_util::int_to_hex(selection->form_id), + rlog::formatAsHex(selection->form_id), selection->formType, selection->favorite); @@ -136,10 +136,9 @@ MenuSelection::MenuSelection(RE::FormID formid) : form_id(formid) this->form = item_form; - auto* player = RE::PlayerCharacter::GetSingleton(); RE::TESBoundObject* boundObject = nullptr; - RE::ExtraDataList* extra = nullptr; - game::boundObjectForForm(item_form, player, boundObject, extra); + RE::ExtraDataList* extraData = nullptr; + game::boundObjectForForm(item_form, boundObject, extraData); if (boundObject) { @@ -168,7 +167,7 @@ uint32_t MenuSelection::makeFromFavoritesMenu(RE::FavoritesMenu* menu, MenuSelec if (result.GetType() == RE::GFxValue::ValueType::kNumber) { form_id = static_cast(result.GetNumber()); - // rlog::debug("favorites menu selection has formid {}"sv, util::string_util::int_to_hex(form_id)); + // rlog::debug("favorites menu selection has formid {}"sv, rlog::formatAsHex(form_id)); } if (form_id == 0) { return 0; } @@ -230,21 +229,21 @@ void MenuSelection::makeFromInventoryMenu(RE::InventoryMenu* menu, MenuSelection if (result.GetType() == RE::GFxValue::ValueType::kNumber) { RE::FormID form_id = static_cast(result.GetNumber()); - rlog::trace("formid {}"sv, util::string_util::int_to_hex(form_id)); + rlog::trace("formid {}"sv, rlog::formatAsHex(form_id)); auto* item_form = RE::TESForm::LookupByID(form_id); if (!item_form) return; - auto* player = RE::PlayerCharacter::GetSingleton(); RE::TESBoundObject* bound_obj = nullptr; - RE::ExtraDataList* extra = nullptr; - game::boundObjectForForm(item_form, player, bound_obj, extra); - - auto* selection = new MenuSelection(form_id); - selection->count = 0; - selection->poisoned = extra ? extra->HasType(RE::ExtraDataType::kPoison) : false; - selection->favorite = !(extra ? extra->HasType(RE::ExtraDataType::kHotkey) : false); - selection->equipped = - extra ? extra->HasType(RE::ExtraDataType::kWorn) || extra->HasType(RE::ExtraDataType::kWornLeft) : false; + RE::ExtraDataList* extraData = nullptr; + game::boundObjectForForm(item_form, bound_obj, extraData); + + auto* selection = new MenuSelection(form_id); + selection->count = 0; + selection->poisoned = extraData ? extraData->HasType(RE::ExtraDataType::kPoison) : false; + selection->favorite = extraData ? extraData->HasType(RE::ExtraDataType::kHotkey) : false; + selection->equipped = extraData ? extraData->HasType(RE::ExtraDataType::kWorn) || + extraData->HasType(RE::ExtraDataType::kWornLeft) : + false; selection->bound_obj = bound_obj; selection->form = item_form; outSelection = selection; @@ -273,8 +272,7 @@ void MenuSelection::makeFromMagicMenu(RE::MagicMenu* menu, MenuSelection*& outSe for (auto* form : mfaves->spells) { - rlog::debug( - "mfave form: id={}; name='{}'"sv, util::string_util::int_to_hex(form->GetFormID()), form->GetName()); + rlog::debug("mfave form: id={}; name='{}'"sv, rlog::formatAsHex(form->GetFormID()), helpers::nameAsUtf8(form)); if (form->GetFormID() == form_id) { // match time diff --git a/src/plugin/sinks.cpp b/src/plugin/sinks.cpp index d935e5b2..7ee11301 100644 --- a/src/plugin/sinks.cpp +++ b/src/plugin/sinks.cpp @@ -49,6 +49,11 @@ EquipEventListener::event_result EquipEventListener::ProcessEvent(const RE::TESE if (current_ammo && current_ammo->GetFormID() == form->GetFormID()) { return event_result::kContinue; } } + const auto formtype = form->GetFormType(); + const auto name = helpers::displayNameAsUtf8(form); + if (event->equipped) { rlog::debug("equip event: {} '{}' equipped", RE::FormTypeToString(formtype), name); } + else { rlog::debug("equip event: {} '{}' removed", RE::FormTypeToString(formtype), name); } + std::string worn_right = helpers::makeFormSpecString(right_eq); std::string worn_left = helpers::makeFormSpecString(left_eq); std::string form_spec = helpers::makeFormSpecString(form); @@ -82,8 +87,7 @@ event_result KeyEventListener::ProcessEvent(RE::InputEvent* const* event_list, { if (event->eventType != RE::INPUT_EVENT_TYPE::kButton) { continue; } - auto* button = - static_cast(event); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) + auto* button = static_cast(event); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) // This offsets the button by an amount that varies based on what originated the // event. This appears to be so that we can directly compare it to the hotkey numbers diff --git a/src/renderer/ui_renderer.cpp b/src/renderer/ui_renderer.cpp index db7f1c6b..194e1a64 100644 --- a/src/renderer/ui_renderer.cpp +++ b/src/renderer/ui_renderer.cpp @@ -8,6 +8,8 @@ #include "lib.rs.h" +using namespace soulsy; + namespace ui { static std::map> animation_frame_map = {}; @@ -66,10 +68,11 @@ namespace ui rlog::error("Cannot find game renderer. Initialization failed."); return; } + const auto rendererData = renderer->GetRendererDataSingleton(); - const auto context = renderer->data.context; - const auto swapChain = renderer->data.renderWindows->swapChain; - const auto forwarder = renderer->data.forwarder; + const auto context = rendererData->context; + const auto swapChain = rendererData->renderWindows->swapChain; + const auto forwarder = rendererData->forwarder; rlog::info("Getting DXGI swapchain..."sv); auto* swapchain = swapChain; @@ -184,10 +187,11 @@ namespace ui const auto renderer = RE::BSGraphics::Renderer::GetSingleton(); if (!renderer) { - rlog::error("Cannot find render manager. Initialization failed."sv); + rlog::error("Cannot find render manager. Unable to build new texture."sv); return false; } - const auto forwarder = renderer->data.forwarder; + const auto rendererData = renderer->GetRendererDataSingleton(); + const auto forwarder = rendererData->forwarder; // Create texture D3D11_TEXTURE2D_DESC desc; @@ -404,19 +408,7 @@ namespace ui continue; } - auto entry_name = std::string(""); - // We use the data cached in the entry if at all possible - if (entry->name_is_utf8()) { entry_name = std::string(entry->name()); } - else - { - // use the bytes from the cstring, which are identical to the data the form gave us - // note that imgui cannot draw non-utf8-valid characters, so we'll get the ?? subs. - // I am *guessing* that the Flash menus are old enough that they handle UCS-16 BE - // data, which is why people do it. OMFG this explains the translation files too. - auto bytes = entry->name_bytes(); - entry_name = helpers::vec_to_stdstring(bytes); - } - + auto entry_name = std::string(entry->name()); const auto hotkey = settings->hotkey_for(slotLayout.element); const auto slot_center = ImVec2(slotLayout.center.x, slotLayout.center.y); @@ -707,7 +699,7 @@ namespace ui gGoalAlpha = std::clamp(goal, 0.0f, gMaxAlpha); if (becomeVisible && gHudAlpha >= gMaxAlpha) { return; } if (!becomeVisible && gHudAlpha == 0.0f) { return; } - rlog::debug("startAlphaTransition() called with in={} and goal={}; gHudAlpha={};"sv, + rlog::trace("startAlphaTransition() called with in={} and goal={}; gHudAlpha={};"sv, becomeVisible, gGoalAlpha, gHudAlpha); diff --git a/src/renderer/ui_renderer.h b/src/renderer/ui_renderer.h index d62950be..2c1d200e 100644 --- a/src/renderer/ui_renderer.h +++ b/src/renderer/ui_renderer.h @@ -2,15 +2,7 @@ #include "animation_handler.h" #include "image_path.h" - -// Forward declarations of the types we're getting from Rust. -enum class Action : ::std::uint8_t; -enum class Align : ::std::uint8_t; -struct HudLayout; -struct SlotLayout; -struct Point; -struct Color; -struct LoadedImage; +#include "soulsy.h" namespace ui { @@ -42,6 +34,8 @@ namespace ui // TODO either make this use the fact that it's a class or make it not a class. class ui_renderer { + using Color = soulsy::Color; + struct wnd_proc_hook { static LRESULT thunk(HWND h_wnd, UINT u_msg, WPARAM w_param, LPARAM l_param); diff --git a/src/soulsy.h b/src/soulsy.h new file mode 100644 index 00000000..fb99a94c --- /dev/null +++ b/src/soulsy.h @@ -0,0 +1,19 @@ +#pragma once + +// Forward declarations of Rust types used by C++. + +namespace soulsy +{ + enum class Action : ::std::uint8_t; + enum class Align : ::std::uint8_t; + struct Color; + struct EquippedData; + struct HudItem; + struct HudLayout; + struct LoadedImage; + struct Point; + struct SlotLayout; + struct SpellData; +} + +using namespace soulsy; diff --git a/src/util/helpers.cpp b/src/util/helpers.cpp index 001d2609..2bc3efdf 100644 --- a/src/util/helpers.cpp +++ b/src/util/helpers.cpp @@ -4,7 +4,6 @@ #include "equippable.h" #include "gear.h" #include "player.h" -#include "string_util.h" #include "ui_renderer.h" #include "utility.h" @@ -12,7 +11,8 @@ namespace helpers { - using string_util = util::string_util; + using UEFLAG = RE::UserEvents::USER_EVENT_FLAG; + // play a denied/failure/no sound void honk() @@ -32,6 +32,25 @@ namespace helpers // How you know I've been replaced by a pod person: if I ever declare that // I love dealing with strings in systems programming languages. + std::string nameAsUtf8(const RE::TESForm* form) + { + // absolutely must never look for a bound object for this puppy. + // It is called by bound object finder functions. + auto name = form->GetName(); // this use is required + auto chonker = helpers::chars_to_vec(name); + auto safename = std::string(string_to_utf8(chonker)); + return safename; + } + + std::string displayNameAsUtf8(const RE::TESForm* form) + { + // Do not call this from bound object finder functions. + auto name = game::displayName(form); + auto chonker = helpers::chars_to_vec(name); + auto safename = std::string(string_to_utf8(chonker)); + return safename; + } + std::vector chars_to_vec(const char* input) { if (!input) { return std::move(std::vector()); } @@ -61,10 +80,20 @@ namespace helpers // Handles photo mode and possibly others. static constexpr auto requiredControlFlags = static_cast(1036); + // Returns true if the player can use movement and gameplay controls. + // Returns false during the intro, for examples. + bool playerInControl() + { + const auto* controlMap = RE::ControlMap::GetSingleton(); + if (!controlMap) { return false; } + const auto canMove = controlMap->IsMovementControlsEnabled(); + return canMove; + } + bool ignoreKeyEvents() { // We pay attention to keypress events when: - // - we are in normal gameplace mode + // - we are in normal gameplay mode // - the item, magic, or favorites menus are visible // We ignore them when other menus are up or when controls are disabled for quest reasons. @@ -76,7 +105,10 @@ namespace helpers if (ui->GameIsPaused() || ui->IsMenuOpen("LootMenu")) return true; if (!ui->IsCursorHiddenWhenTopmost() || !ui->IsShowingMenus() || !ui->GetMenu()) { return true; } + // If we're not in control of the player character or otherwise not in gameplay, move on. + if (!playerInControl()) { return true; } + /* const auto* control_map = RE::ControlMap::GetSingleton(); if (!control_map || !control_map->IsMovementControlsEnabled() || !control_map->AreControlsEnabled(requiredControlFlags) || !control_map->IsActivateControlsEnabled() || @@ -84,8 +116,9 @@ namespace helpers { return true; } + */ - return false; + return false; // FOR NOW } bool gamepadInUse() @@ -110,11 +143,7 @@ namespace helpers ui->IsMenuOpen(RE::LoadingMenu::MENU_NAME); if (hudInappropriate) { return false; } - const auto* control_map = RE::ControlMap::GetSingleton(); - bool playerNotInControl = - !control_map || !control_map->IsMovementControlsEnabled() || - control_map->contextPriorityStack.back() != RE::UserEvents::INPUT_CONTEXT_ID::kGameplay; - if (playerNotInControl) { return false; } + if (!playerInControl()) { return false; } return true; } @@ -154,14 +183,14 @@ namespace helpers { // rlog::trace("it is dynamic"sv); form_string = - fmt::format("{}{}{}", util::dynamic_name, util::delimiter, string_util::int_to_hex(form->GetFormID())); + fmt::format("{}{}{}", util::dynamic_name, util::delimiter, rlog::formatAsHex(form->GetFormID())); } else { auto* source_file = form->sourceFiles.array->front()->fileName; auto local_form = form->GetLocalFormID(); - const auto hexified = string_util::int_to_hex(local_form); + const auto hexified = rlog::formatAsHex(local_form); // rlog::trace("source file='{}'; local id={}'; hex={};"sv, source_file, local_form, hexified); form_string = fmt::format("{}{}{}", source_file, util::delimiter, hexified); } @@ -207,8 +236,8 @@ namespace helpers // { // rlog::trace("found form id for form spec='{}'; name='{}'; formID={}", // a_str, - // form->GetName(), - // string_util::int_to_hex(form->GetFormID())); + // helpers::nameAsUtf8(form), + // rlog::formatAsHex(form->GetFormID())); // } return form; diff --git a/src/util/helpers.h b/src/util/helpers.h index 21212e20..956daf18 100644 --- a/src/util/helpers.h +++ b/src/util/helpers.h @@ -1,13 +1,12 @@ #pragma once #include "rust/cxx.h" +#include "soulsy.h" // This namespace is for rust/C++ bridge helpers as well as any // decision-making that needs a single source of truth. It's // badly-named. -struct HudItem; - namespace helpers { RE::TESForm* formSpecToFormItem(const std::string& spec); @@ -37,6 +36,8 @@ namespace helpers bool isPoisonedByFormSpec(const std::string& form_spec); float chargeLevelByFormSpec(const std::string& form_spec); + std::string nameAsUtf8(const RE::TESForm* form); + std::string displayNameAsUtf8(const RE::TESForm* form); std::string vec_to_stdstring(rust::Vec input); std::vector chars_to_vec(const char* input); diff --git a/src/util/string_util.h b/src/util/string_util.h deleted file mode 100644 index 5a22055e..00000000 --- a/src/util/string_util.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -namespace util -{ - class string_util - { - public: - template - static std::string int_to_hex(T xs) - { - std::stringstream stream; - stream << "0x" << std::hex << std::setw(8) << std::setfill('0') << xs; - return ltrim_copy(stream.str()); - } - - private: - static void ltrim(std::string& s) - { - s.erase(s.begin(), std::ranges::find_if(s, [](const unsigned char ch) { return !std::isspace(ch); })); - } - - static std::string ltrim_copy(std::string s) - { - ltrim(s); - return s; - } - }; -} diff --git a/vcpkg.json b/vcpkg.json index 28b6f8b3..fb64898e 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,22 +1,24 @@ { - "name": "soulsyhud", - "version-string": "0.13.7", - "description": "hotkey and hud for Skyrim AE", - "homepage": "https://github.com/ceejbot/soulsy", - "license": "GPL-2.0-or-later", - "dependencies": [ - { - "name": "imgui", - "features": ["dx11-binding", "win32-binding"] - }, - "fmt", - "spdlog", - "rapidcsv" - ], - "overrides": [ - { - "name": "imgui", - "version": "1.88" - } - ] + "name": "soulsyhud", + "version-string": "0.15.0", + "description": "hotkey and hud for Skyrim AE", + "homepage": "https://github.com/ceejbot/soulsy", + "license": "GPL-2.0-or-later", + "dependencies": [ + { + "name": "imgui", + "features": ["dx11-binding", "win32-binding"] + }, + "fmt", + "spdlog", + "rapidcsv", + "directxtk", + "rsm-binary-io" + ], + "overrides": [ + { + "name": "imgui", + "version": "1.88" + } + ] }