Skip to content

Commit 983cc3b

Browse files
committed
unit: add UnitDistanceSquared
Backport retail's UnitDistanceSquared(unit) -> (distanceSquared, checkedPosition): the squared world distance from the player to the unit, plus a flag for whether both positions were readable. Squared because almost all distance logic is a threshold compare or nearest-unit sort, neither of which needs the sqrt — which is why retail never shipped a plain UnitDistance. Added to the existing Unit::Range module alongside UnitInRange (both now share one PlayerToUnitDistSq compute off the CGObject::GetPosition vtable virtual). Center-to-center, self-contained (reads the position in C — no SuperWoW dependency, and leaves SuperWoW's own UnitPosition untouched). Verified in-game to match SuperWoW UnitPosition math to float precision. On a position miss returns (0, false); no self-quirk, so UnitDistanceSquared("player") is a legitimate (0, true).
1 parent 18ac033 commit 983cc3b

3 files changed

Lines changed: 102 additions & 15 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
6262
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime` |
6363
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes` |
6464
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
65-
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitGUID`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
65+
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitDistanceSquared`, `UnitGUID`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
6666
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterComboDuration` |
6767
| [VoiceChat](docs/API.md#voicechat) | `C_VoiceChat.GetTtsVoices`, `C_VoiceChat.GetRemoteTtsVoices`, `C_VoiceChat.SpeakText`, `C_VoiceChat.StopSpeakingText`, `C_TTSSettings.GetSpeechRate`, `C_TTSSettings.GetSpeechVolume`, `C_TTSSettings.GetSpeechVoiceID`, `C_TTSSettings.GetVoiceOptionName`, `C_TTSSettings.SetSpeechRate`, `C_TTSSettings.SetSpeechVolume`, `C_TTSSettings.SetVoiceOption`, `C_TTSSettings.SetVoiceOptionByName`, `C_TTSSettings.SetDefaultSettings`, `C_TTSSettings.RefreshVoices` |
6868
| [XMLUtil](docs/API.md#xmlutil) | `C_XMLUtil.DoesTemplateExist`, `C_XMLUtil.GetTemplateInfo`, `C_XMLUtil.GetTemplates` |

docs/API.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,7 @@ build instructions.
428428
- [`UnitIsOtherPlayersPet(unit)`](#unitisotherplayerspetunit)
429429
- [`UnitStandState(unit)`](#unitstandstateunit)
430430
- [`UnitInRange(unit)`](#unitinrangeunit)
431+
- [`UnitDistanceSquared(unit)`](#unitdistancesquaredunit)
431432
- [`UnitPower(unit [, powerType])` / `UnitPowerMax(unit [, powerType])`](#unitpowerunit--powertype--unitpowermaxunit--powertype)
432433
- [`UnitPowerType(unit)`](#unitpowertypeunit)
433434

@@ -10211,6 +10212,44 @@ mouseover, etc.). For raid members in a different zone or otherwise
1021110212
not in the engine's sync window, the position virtual returns null
1021210213
and the function reports `(false, false)`.
1021310214

10215+
### `UnitDistanceSquared(unit)`
10216+
10217+
Returns `(distanceSquared, checkedPosition)` — the **squared** world
10218+
distance (yards²) from the player to `unit`, and a flag indicating
10219+
whether both positions were available.
10220+
10221+
```lua
10222+
local distSq, checked = UnitDistanceSquared("target")
10223+
if checked and distSq <= 30 * 30 then
10224+
-- target is within 30 yards
10225+
end
10226+
```
10227+
10228+
| Return | Meaning |
10229+
|--------|---------|
10230+
| `distanceSquared` | Squared distance player→unit. `0` when `checkedPosition` is `false` (a placeholder — matches retail's "always a number" shape). |
10231+
| `checkedPosition` | `true` when both positions were read. `false` when `unit`'s position isn't available (empty `partyN` slot, no target, raid member outside the sync window, etc.). |
10232+
10233+
The value is **squared** on purpose: nearly all distance logic is a
10234+
threshold compare (`distSq <= range * range`) or a nearest-unit sort,
10235+
neither of which needs the square root — so retail exposes only the
10236+
squared form (added 5.0.4) and never a plain `UnitDistance`. Take
10237+
`math.sqrt(distanceSquared)` only when you need a yard number to show a
10238+
human.
10239+
10240+
Reads world positions via the `CGObject::GetPosition` vtable virtual
10241+
(slot 5, offset `+0x14`) — the same path `UnitInRange` /
10242+
`CheckInteractDistance` use. **Center-to-center**, not reach-aware (that
10243+
edge-to-edge nicety is UnitXP_SP3's niche); the value equals what you'd
10244+
compute from SuperWoW's `UnitPosition`, but it's self-contained (no
10245+
sibling-DLL dependency, and no clash with SuperWoW's own `UnitPosition`,
10246+
which is left untouched).
10247+
10248+
> No self-quirk (unlike `UnitInRange`): `UnitDistanceSquared("player")`
10249+
> returns a legitimate `(0, true)`. Because a real `0` (self, or two
10250+
> exactly co-located units) is indistinguishable by value from the miss
10251+
> placeholder, always branch on `checkedPosition`.
10252+
1021410253
### `UnitClassBase(unit)`
1021510254

1021610255
Returns `(classFile, classID)` — the locale-independent class

src/unit/Range.cpp

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,21 @@
88
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
99
// PURPOSE. See the GNU General Public License for more details.
1010

11-
// `UnitInRange(unit) → (inRange, checkedRange)` — 40-yard healing-
12-
// range check, modern's `UnitInRange` shape. Vanilla 1.12 doesn't
13-
// ship the function but has all the underlying machinery — the
14-
// same `GetPosition` virtual (vtable slot 5) `CheckInteractDistance`
15-
// uses, applied with a fixed 40-yard threshold.
11+
// Player↔unit proximity, modern's shape. Vanilla 1.12 ships neither
12+
// function but has all the underlying machinery — the object's
13+
// `GetPosition` virtual (vtable slot 5) `CheckInteractDistance` uses:
14+
//
15+
// `UnitInRange(unit) → (inRange, checkedRange)` — 40-yard healing-
16+
// range check (fixed threshold on the squared distance).
17+
// `UnitDistanceSquared(unit) → (distanceSquared, checkedPosition)` —
18+
// the raw squared world distance player→unit. Squared because
19+
// nearly every consumer compares against a threshold (`distSq <=
20+
// range*range`) or ranks by nearest, neither of which needs the
21+
// sqrt; that's why retail exposes only the squared form (added
22+
// 5.0.4). Center-to-center — NOT reach-aware (that's UnitXP_SP3's
23+
// niche); equivalent to computing it from SuperWoW `UnitPosition`,
24+
// but self-contained (reads the position in C, no sibling-DLL
25+
// dependency, no clash with SuperWoW's own `UnitPosition`).
1626

1727
#include "Game.h"
1828
#include "unit/Position.h"
@@ -26,6 +36,24 @@ namespace {
2636
constexpr float kRangeYards = 40.0f;
2737
constexpr float kRangeYardsSq = kRangeYards * kRangeYards;
2838

39+
// Squared world distance from the player to `token`'s unit. Returns true
40+
// and writes *outSq on success; false when either position is unavailable
41+
// (unresolved / absent token, or an object with no known position yet —
42+
// e.g. a party member outside the client's sync range). This false is
43+
// exactly the `checkedPosition = false` case retail flags.
44+
bool PlayerToUnitDistSq(const char *token, float *outSq) {
45+
float unitPos[3] = {};
46+
float playerPos[3] = {};
47+
if (!Unit::Position::ReadToken(token, unitPos) ||
48+
!Unit::Position::ReadToken("player", playerPos))
49+
return false;
50+
const float dx = unitPos[0] - playerPos[0];
51+
const float dy = unitPos[1] - playerPos[1];
52+
const float dz = unitPos[2] - playerPos[2];
53+
*outSq = dx * dx + dy * dy + dz * dz;
54+
return true;
55+
}
56+
2957
int __fastcall Script_UnitInRange(void *L) {
3058
if (!Game::Lua::IsString(L, 1)) {
3159
Game::Lua::Error(L, "Usage: UnitInRange(\"unit\")");
@@ -44,27 +72,47 @@ int __fastcall Script_UnitInRange(void *L) {
4472
return 2;
4573
}
4674

47-
float unitPos[3] = {};
48-
float playerPos[3] = {};
49-
if (!Unit::Position::ReadToken(token, unitPos) ||
50-
!Unit::Position::ReadToken("player", playerPos)) {
75+
float distSq = 0.0f;
76+
if (!PlayerToUnitDistSq(token, &distSq)) {
5177
Game::Lua::PushBool(L, false);
5278
Game::Lua::PushBool(L, false);
5379
return 2;
5480
}
5581

56-
const float dx = unitPos[0] - playerPos[0];
57-
const float dy = unitPos[1] - playerPos[1];
58-
const float dz = unitPos[2] - playerPos[2];
59-
const float distSq = dx * dx + dy * dy + dz * dz;
60-
6182
Game::Lua::PushBool(L, distSq <= kRangeYardsSq);
6283
Game::Lua::PushBool(L, true);
6384
return 2;
6485
}
6586

87+
// `UnitDistanceSquared("unit") → (distanceSquared, checkedPosition)`.
88+
// Unlike UnitInRange there's no self-quirk: `UnitDistanceSquared("player")`
89+
// is a legitimate `(0, true)`. On a position miss returns `(0, false)` —
90+
// matching retail's "always a number" shape; consumers must branch on
91+
// `checkedPosition`, since a real `0` (self, or exactly co-located units)
92+
// is indistinguishable from the miss placeholder by value alone.
93+
int __fastcall Script_UnitDistanceSquared(void *L) {
94+
if (!Game::Lua::IsString(L, 1)) {
95+
Game::Lua::Error(L, "Usage: UnitDistanceSquared(\"unit\")");
96+
return 0;
97+
}
98+
const char *token = Game::Lua::ToString(L, 1);
99+
100+
float distSq = 0.0f;
101+
if (!PlayerToUnitDistSq(token, &distSq)) {
102+
Game::Lua::PushNumber(L, 0.0);
103+
Game::Lua::PushBool(L, false);
104+
return 2;
105+
}
106+
107+
Game::Lua::PushNumber(L, static_cast<double>(distSq));
108+
Game::Lua::PushBool(L, true);
109+
return 2;
110+
}
111+
66112
void RegisterLuaFunctions() {
67113
Game::Lua::RegisterGlobalFunction("UnitInRange", &Script_UnitInRange);
114+
Game::Lua::RegisterGlobalFunction("UnitDistanceSquared",
115+
&Script_UnitDistanceSquared);
68116
}
69117

70118
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};

0 commit comments

Comments
 (0)