creature_anim: the loot sparkle's switch starts where its CVar does

Reported from play: no sparkle on a mining vein, while lootable mobs sparkled
fine. That split is the diagnosis — only the GameObject arm reads the switch.

`LootSparkleMode` derived `Default`, so it booted `false` while its row
registered `"1"`. A boot fires `CvarChanged` only for rows `config.toml`
actually carries, and writing `"1"` over a value already `"1"` is a no-op that
fires nothing either, so nothing could ever tell the resource its real default —
not the boot, and not the player toggling the CVar. The feature was inert for
everyone who had never written that row.

`vplates.rs`'s `VPlateMode::default` carries this warning in as many words,
from `NameplateOcclusion` shipping inert the same way, and this is the same
mistake one feature later. So: a hand-written `Default`, and the twin of that
module's test, reading the registered row rather than restating it. Swept the
rest — of the 32 resources a `CvarChanged` observer writes, 6 derive `Default`
and every one of their rows registers `"0"`, so this was the only one.

The doc row kept two stale claims and both are corrected: the test has had two
arms (the activate bit *or* `GAMEOBJECT_TYPE_CHEST`) since it was widened, and
`FxStage` is immaterial here — `Particles\LootFX.m2`, the `"HARDCODED Loot Art"`
row's own model, holds one 2400 ms sequence with `flags = 0`, and bit 0 clear
loops, so `FxAnimLife::arm` repeats it whatever the stage says.

Still unverified in world: whether veins, herbs and chests sparkle, and whether
anything sparkles that should not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-17 13:41:47 -06:00
co-authored by Claude Opus 5
parent 0807144f38
commit 1287c328e0
3 changed files with 40 additions and 5 deletions
@@ -1086,11 +1086,24 @@ pub(crate) fn arm_aura_state_fx(
/// just a corpse. The mod's own spelling; registered in `cvars.rs`.
pub(crate) const CVAR_LOOT_SPARKLE: &str = "LootSparkle";
/// [`CVAR_LOOT_SPARKLE`]'s applied value, seeded by the boot-time `CvarChanged` observer as
/// `vplates.rs`'s `VPlateMode` is.
#[derive(Resource, Default)]
/// [`CVAR_LOOT_SPARKLE`]'s applied value.
///
/// **The default must equal the registered row's**, because a boot emits `CvarChanged` only for
/// rows `config.toml` actually carries, and a write of `"1"` over a `"1"` is a no-op that emits
/// nothing either — so nothing would ever tell a derived `false` that the CVar reads on. That is
/// exactly how this feature shipped inert: the row registered `"1"`, the resource derived
/// `false`, and no vein, herb or chest sparkled for anyone who had never touched the CVar.
/// `vplates.rs`'s `VPlateMode::default` carries the same warning for the same reason;
/// [`tests::the_loot_sparkle_default_agrees_with_its_registered_cvar`] is the check.
#[derive(Resource)]
pub(crate) struct LootSparkleMode(pub(crate) bool);
impl Default for LootSparkleMode {
fn default() -> Self {
Self(true)
}
}
/// Apply a change of the switch.
pub(crate) fn on_loot_sparkle_cvar(
ev: On<crate::cvars::CvarChanged>,
@@ -1653,6 +1653,26 @@ fn an_anim_only_state_kit_still_arms_on_the_aura_add_edge() {
);
}
/// The switch's applied default must equal the row it reads, or the feature ships inert.
///
/// A boot fires `CvarChanged` only for rows `config.toml` carries, and re-writing a row with the
/// value it already holds is a no-op that fires nothing either — so a derived `false` beside a
/// registered `"1"` is never corrected, by the boot or by the player. That is how the sparkle
/// shipped off: the decision below was right, the switch feeding it was never turned on. Twin of
/// `vplates.rs`'s `the_occlusion_default_agrees_with_its_registered_cvar`.
#[test]
fn the_loot_sparkle_default_agrees_with_its_registered_cvar() {
let registered = crate::cvars::registered_pairs()
.find(|(n, _)| n.eq_ignore_ascii_case(super::CVAR_LOOT_SPARKLE))
.map(|(_, d)| d)
.expect("LootSparkle is registered");
assert_eq!(
super::LootSparkleMode::default().0,
registered != "0",
"the applied default must equal the registered CVar's ({registered:?})"
);
}
/// SuperWoW's `LootSparkle`: 1.12 sparkles a lootable corpse and nothing else, so the GameObject
/// arm is a deviation that has to be switchable, and the switch off has to answer exactly 1.12.
///
+4 -2
View File
File diff suppressed because one or more lines are too long