feat(unit): softenemy and softfriend fire unit events

Unit::TokenEvents already redirects the 38 broadcast and 4 combat
feedback call sites so a unit holding focus / nameplateN / markN gets
every engine unit event under that token. softenemy and softfriend join
that token list, read through SoftUnit::Current (the last pick, no scan)
because TokensOf runs on every unit event.

The registration bit (UNIT_SCRIPT_REG_TOKENS) is what makes the engine
watch a unit, so SoftUnit::Refresh re-runs FUN_UNIT_SCRIPT_RECOMPUTE
through TokenEvents::Refresh for the old and new guid before the
PLAYER_SOFT_*_CHANGED event fires. The frame tick now picks every frame
rather than only while that event has a listener: a UNIT_HEALTH listener
never registers for it, and the pick is cached per scene drain anyway.
This commit is contained in:
Brues
2026-10-31 22:25:18 -05:00
parent f9ec92014c
commit 44f9bcd9af
3 changed files with 27 additions and 15 deletions
+14 -12
View File
@@ -39,8 +39,9 @@
// SoftTargetWithLocked 1. The arcs are not: Classic Era's 2 picks the
// nearest unit wherever the camera looks, so both default to 0, the
// interact arc's (the unit in front of the camera).
// Picked when something asks (a read of a token) and on the UI frame tick
// only while a frame is registered for the side's event.
// Picked on the UI frame tick and when something asks (a read of a token).
// The softenemy / softfriend unit events (UNIT_HEALTH, …) follow the pick via
// Unit::TokenEvents, as they do for focus.
#include "target/SoftUnit.h"
@@ -53,6 +54,7 @@
#include "target/Nearest.h"
#include "target/SoftTarget.h"
#include "tick/FrameTick.h"
#include "unit/TokenEvents.h"
namespace Target::SoftUnit {
@@ -124,8 +126,6 @@ uint64_t Pick(State &st) {
return SoftTarget::Nearest(area, Offsets::TYPEMASK_UNIT, &Accept, &st);
}
bool Watched(const State &st) { return Event::Custom::HasListeners(st.changed->Slot()); }
// One pick per side per scene drain. The scan reads what the last drain drew,
// which changes only when the next one does, so every read until then — the
// circles, the icons, Lua's token reads — shares the first one's answer. The
@@ -142,7 +142,12 @@ void Refresh(State &st) {
const uint64_t guid = Pick(st);
if (guid == st.soft)
return;
const uint64_t old = st.soft;
st.soft = guid;
// The token's unit events follow the pick (the engine watches a unit
// that holds a token), so the registration moves before the event fires.
Unit::TokenEvents::Refresh(old);
Unit::TokenEvents::Refresh(guid);
Event::Custom::Fire(st.changed->Slot(), "");
}
@@ -152,10 +157,8 @@ void OnFrameTick() {
g_friend.soft = 0;
return;
}
if (Watched(g_enemy))
Refresh(g_enemy);
if (Watched(g_friend))
Refresh(g_friend);
Refresh(g_enemy);
Refresh(g_friend);
}
const Tick::FrameTick::AutoSubscribe _tick{&OnFrameTick};
@@ -193,17 +196,16 @@ const Game::ModuleAutoRegister _autoreg{&Register};
} // namespace
// Fresh when nothing watches; the frame tick's pick (this frame's) when a
// listener does.
uint64_t Get(Side side) {
if (!Object::RegistryReady())
return 0;
State &st = Of(side);
if (!Watched(st))
Refresh(st);
Refresh(st);
return st.soft;
}
uint64_t Current(Side side) { return Of(side).soft; }
// Classic Era 1.15 probes, `anyfriend`: a target that qualifies, else
// `softfriend`, else the target whatever its reaction (a hostile one, or a
// friendly NPC the friend test rejects), else `softenemy` (no target, no
+4
View File
@@ -23,6 +23,10 @@ enum class Side { Enemy, Friend };
// `unit/TokenExtensions.cpp`.
uint64_t Get(Side side);
// The last pick, with no scan. For the unit-event hot path
// (`unit/TokenEvents.cpp`); the frame tick keeps it current.
uint64_t Current(Side side);
// SoftTargetEnemy / SoftTargetFriend turns the side on.
bool Enabled(Side side);
+9 -3
View File
@@ -12,7 +12,8 @@
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Every unit event the engine fires reaches ClassicAPI's unit tokens —
// `focus`, `nameplateN`, `markN` — as it reaches `target`.
// `focus`, `nameplateN`, `markN`, `softenemy`, `softfriend` — as it reaches
// `target`.
//
// This is how 4.3.4 makes its focus a full unit token (decomp: GameUI.cpp
// SetFocusTarget, ScriptEvents.cpp token maps), in two steps: the unit gets a
@@ -56,6 +57,7 @@
#include "debug/Log.h"
#include "nameplate/Walk.h"
#include "object/Resolve.h"
#include "target/SoftUnit.h"
#include "unit/Focus.h"
#include "unit/Identity.h"
#include "unit/RaidTarget.h"
@@ -76,8 +78,8 @@ using Broadcast_t = void(__fastcall *)(const uint64_t *guid, uint32_t eventId);
using CombatFeedback_t = void(__fastcall *)(const uint64_t *guid, const char *action, int amount,
int school, uint32_t hitInfo);
// At most one focus, one nameplate and one mark per unit.
constexpr int kMaxTokens = 3;
// At most one focus, nameplate, mark, softenemy and softfriend per unit.
constexpr int kMaxTokens = 5;
using TokenList = char[kMaxTokens][24];
// The ClassicAPI tokens `guid` has right now, in UnitTokenFromGUID's order.
@@ -97,6 +99,10 @@ int TokensOf(uint64_t guid, TokenList &out) {
std::snprintf(out[n++], sizeof out[0], "mark%d", m);
break;
}
if (guid == Target::SoftUnit::Current(Target::SoftUnit::Side::Enemy))
std::snprintf(out[n++], sizeof out[0], "softenemy");
if (guid == Target::SoftUnit::Current(Target::SoftUnit::Side::Friend))
std::snprintf(out[n++], sizeof out[0], "softfriend");
return n;
}