mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 20:37:37 +00:00
Compare commits
109
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4477749bd6 | ||
|
|
99fde6e1bb | ||
|
|
ea7e215a0c | ||
|
|
f5b240de77 | ||
|
|
bb5acbbdfb | ||
|
|
0825491af2 | ||
|
|
2a4731f9e5 | ||
|
|
0d639a0a35 | ||
|
|
8dc7cf8ace | ||
|
|
1e0871b639 | ||
|
|
70a6b99bfe | ||
|
|
edd7fb8577 | ||
|
|
34b061e3da | ||
|
|
ddffa12f2f | ||
|
|
d7ff5d8a5c | ||
|
|
3df06c056d | ||
|
|
67e833557a | ||
|
|
5a78289124 | ||
|
|
9f166b6770 | ||
|
|
6b4224d281 | ||
|
|
79d4571f8b | ||
|
|
8982ec757b | ||
|
|
b44b0d2f59 | ||
|
|
76eb5d9135 | ||
|
|
aa88e776e9 | ||
|
|
d74952c9ac | ||
|
|
2567ac1bd8 | ||
|
|
d5f93a1e7f | ||
|
|
3132eb80d8 | ||
|
|
cf6593aa7b | ||
|
|
0cb101ef97 | ||
|
|
545cc70a9d | ||
|
|
2fda1cdbcd | ||
|
|
13ccfb0865 | ||
|
|
7fa9ad9787 | ||
|
|
2cb0e4b5b7 | ||
|
|
9f77a40235 | ||
|
|
76c3cd3a37 | ||
|
|
7f05e88eaf | ||
|
|
229a5d51ae | ||
|
|
12eeac5050 | ||
|
|
f5f9485375 | ||
|
|
aaa033bb4f | ||
|
|
39a774930b | ||
|
|
5ee83e5427 | ||
|
|
3791981873 | ||
|
|
8f537226e9 | ||
|
|
6532eb76eb | ||
|
|
098432bee1 | ||
|
|
220fbff57a | ||
|
|
bddd6d91fa | ||
|
|
4a72ca1138 | ||
|
|
75653b9485 | ||
|
|
dfa29376f2 | ||
|
|
7be139fe32 | ||
|
|
c61e3b6c52 | ||
|
|
5783814dc5 | ||
|
|
afd5127f50 | ||
|
|
cfa4645cad | ||
|
|
8640cc02da | ||
|
|
bc32b9b645 | ||
|
|
5bb14c036e | ||
|
|
e5227157b9 | ||
|
|
be6a3c5bef | ||
|
|
4e8dc39e65 | ||
|
|
d53e15ff45 | ||
|
|
5c5bedece6 | ||
|
|
a258c856d1 | ||
|
|
e9340786c9 | ||
|
|
9101de7f78 | ||
|
|
8b6b6ce4ce | ||
|
|
ea6b3714ed | ||
|
|
2e87d286ff | ||
|
|
03afa419f2 | ||
|
|
5d5682cbdd | ||
|
|
69665cdadf | ||
|
|
81d63bc11c | ||
|
|
ce87b64ab2 | ||
|
|
af01cda9b6 | ||
|
|
a878407ed6 | ||
|
|
8ed9c0f707 | ||
|
|
06c9594e2a | ||
|
|
83298e03d7 | ||
|
|
849b9b2c92 | ||
|
|
e27d1e5849 | ||
|
|
2ba1818a6e | ||
|
|
8ec08a43b1 | ||
|
|
82756a9d25 | ||
|
|
38771147d0 | ||
|
|
ce1ac37964 | ||
|
|
1c70be0e3d | ||
|
|
56d618b1d4 | ||
|
|
ac09c59d90 | ||
|
|
b6fa913732 | ||
|
|
cf76b699a2 | ||
|
|
dc394ab046 | ||
|
|
12b3784333 | ||
|
|
f6b37d5fa1 | ||
|
|
754997ea51 | ||
|
|
a16b99073a | ||
|
|
d7103de564 | ||
|
|
c00eb24a8d | ||
|
|
b65c9e0319 | ||
|
|
de033896b1 | ||
|
|
dc8d4caf12 | ||
|
|
20069f3815 | ||
|
|
f2fa112f0a | ||
|
|
fca967c827 | ||
|
|
24404b5a23 |
@@ -0,0 +1,373 @@
|
||||
-- Broodwatch brood: spawning, hit feedback, damage and death.
|
||||
|
||||
local _, BW = ...
|
||||
local CENTER = BW.CENTER
|
||||
local ROOM_RADIUS = BW.ROOM_RADIUS
|
||||
local SPAWN_MIN_DISTANCE = BW.SPAWN_MIN_DISTANCE
|
||||
local MAX_ALIVE = BW.MAX_ALIVE
|
||||
local DEATH_SECONDS = BW.DEATH_SECONDS
|
||||
local ANIM_STAND = BW.ANIM_STAND
|
||||
local ANIM_DEATH = BW.ANIM_DEATH
|
||||
local KINDS = BW.KINDS
|
||||
local ELITE = BW.ELITE
|
||||
local TUNING = BW.TUNING
|
||||
local VAMPIRIC_SHARE = BW.VAMPIRIC_SHARE
|
||||
local pools = BW.pools
|
||||
local enemies = BW.enemies
|
||||
local FloorAt = BW.FloorAt
|
||||
local Dist2 = BW.Dist2
|
||||
local DropGem = BW.DropGem
|
||||
local Mastered = BW.Mastered
|
||||
local SPELLS = BW.SPELLS
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The brood
|
||||
|
||||
-- Health grows faster as the run goes on (x3 at 5:00, x9 at 10:00), so late
|
||||
-- upgrades keep pace instead of running away; bites grow x4 by 10:00.
|
||||
local function KindStats(kind)
|
||||
local m = BW.run.time / 300;
|
||||
local grow = 1 + 2 * m + m * m;
|
||||
return math.floor(kind.hp * grow + 0.5), kind.dps * grow;
|
||||
end
|
||||
|
||||
local function NewEnemy(kind)
|
||||
local e = { kind = kind, actor = BW.scene:AcquireActor() };
|
||||
e.actor:SetModelByCreatureDisplayID(kind.display);
|
||||
-- A whelp casts the next leap of Chain Lightning: its impact comes here.
|
||||
e.actor:HookScript("OnSpellVisualImpact", function(_, _, _, castID) BW.OnImpact(nil, nil, nil, castID); end);
|
||||
return e;
|
||||
end
|
||||
|
||||
local function Spawn(kind)
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local x, y;
|
||||
for _ = 1, 10 do
|
||||
local a = math.random() * 2 * math.pi;
|
||||
local r = ROOM_RADIUS * math.sqrt(math.random());
|
||||
x, y = CENTER[1] + r * math.cos(a), CENTER[2] + r * math.sin(a);
|
||||
if Dist2(x, y, hx, hy) >= SPAWN_MIN_DISTANCE * SPAWN_MIN_DISTANCE then
|
||||
break;
|
||||
end
|
||||
end
|
||||
local pool = pools[kind.key];
|
||||
local e = pool and table.remove(pool) or NewEnemy(kind);
|
||||
e.hp, e.dps = KindStats(kind);
|
||||
e.maxHp = e.hp;
|
||||
e.alive = true;
|
||||
e.dying = nil;
|
||||
e.fading = nil;
|
||||
e.dotLeft = nil;
|
||||
e.slowUntil, e.slowFactor = nil, nil;
|
||||
e.stunUntil = nil;
|
||||
e.fearUntil, e.fearTick = nil, nil;
|
||||
e.frozenUntil = nil;
|
||||
e.weakUntil, e.weakShare = nil, nil;
|
||||
e.taunter, e.tauntUntil = nil, nil;
|
||||
if e.kit then
|
||||
e.kit, e.kitUntil = nil, nil;
|
||||
e.actor:SetSpellVisualKit();
|
||||
end
|
||||
e.angle = math.random() * 2 * math.pi;
|
||||
local actor = e.actor;
|
||||
actor:SetPosition(x, y, FloorAt(x, y, CENTER[3]) + 0.5);
|
||||
actor:SetAnimation(ANIM_STAND);
|
||||
actor:Show();
|
||||
actor:SetMovementEnabled(true);
|
||||
actor:SetMovementSpeed("run", kind.speed);
|
||||
tinsert(enemies, e);
|
||||
BW.run.alive = BW.run.alive + 1;
|
||||
return e;
|
||||
end
|
||||
|
||||
local function Kill(e)
|
||||
e.alive = false;
|
||||
e.dying = DEATH_SECONDS;
|
||||
BW.run.alive = BW.run.alive - 1;
|
||||
BW.run.kills = BW.run.kills + 1;
|
||||
local x, y, z = e.actor:GetPosition();
|
||||
-- The corpse keeps its movement until it is released: it stops where
|
||||
-- it is, plays Death, then lies Dead, as a dead unit does.
|
||||
e.actor:StopMoving();
|
||||
e.actor:SetAnimation(ANIM_DEATH);
|
||||
if e.kit then
|
||||
e.kit, e.kitUntil = nil, nil;
|
||||
e.actor:SetSpellVisualKit();
|
||||
end
|
||||
e.fearUntil = nil;
|
||||
DropGem(x, y, z, e.kind.xp, e.kind.gemScale);
|
||||
BW.DropItem(e, x, y, z);
|
||||
-- A corrupted enemy passes its Corruption on (in the step, after the loop).
|
||||
if e.dotLeft and e.dotSpread > 0 then
|
||||
tinsert(BW.run.spreads, e);
|
||||
end
|
||||
e.dotLeft = nil;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Hit feedback: a short flash on the model, and a number that rises from the
|
||||
-- hit point and fades.
|
||||
|
||||
local FLASH_SECONDS = 0.12
|
||||
local NUMBER_SECONDS = 0.8
|
||||
local NUMBER_RISE = 34 -- pixels over the number's life
|
||||
local NUMBER_FONT = "Fonts\\ARIALN.TTF"
|
||||
local BIG_HIT = 40 -- hits this large use the large size
|
||||
local MAX_NUMBERS = 80
|
||||
|
||||
local numbers = {} -- live numbers, oldest first
|
||||
local numberPool = {}
|
||||
|
||||
local function TakeNumber()
|
||||
local n = table.remove(numberPool);
|
||||
if n then
|
||||
return n;
|
||||
end
|
||||
if table.getn(numbers) >= MAX_NUMBERS then
|
||||
-- Too many at once: the oldest gives up its string.
|
||||
return table.remove(numbers, 1);
|
||||
end
|
||||
n = { text = BW.feedback:CreateFontString(nil, "OVERLAY") };
|
||||
n.text:SetFontObject(NumberFontNormal);
|
||||
return n;
|
||||
end
|
||||
|
||||
local function ShowNumber(e, amount, color, text)
|
||||
local x, y, z = e.actor:GetNamePosition();
|
||||
if not x then
|
||||
x, y, z = e.actor:GetPosition();
|
||||
z = z + 1.5;
|
||||
end
|
||||
local n = TakeNumber();
|
||||
n.x, n.y, n.z = x, y, z;
|
||||
n.age = 0;
|
||||
n.dx = math.random(-12, 12);
|
||||
local size = amount >= BIG_HIT and 22 or 16;
|
||||
if n.size ~= size then
|
||||
n.size = size;
|
||||
n.text:SetFont(NUMBER_FONT, size, "OUTLINE");
|
||||
end
|
||||
n.text:SetTextColor(color[1], color[2], color[3]);
|
||||
n.text:SetText(text or tostring(amount));
|
||||
n.text:SetAlpha(1);
|
||||
n.text:Show();
|
||||
tinsert(numbers, n);
|
||||
end
|
||||
|
||||
local function UpdateNumbers(dt)
|
||||
local count = table.getn(numbers);
|
||||
if count == 0 then
|
||||
return;
|
||||
end
|
||||
local scale = BW.scene:GetEffectiveScale();
|
||||
for i = count, 1, -1 do
|
||||
local n = numbers[i];
|
||||
n.age = n.age + dt;
|
||||
local sx, sy = BW.scene:Project3DPointTo2D(n.x, n.y, n.z);
|
||||
if n.age >= NUMBER_SECONDS or not sx then
|
||||
n.text:Hide();
|
||||
tinsert(numberPool, n);
|
||||
table.remove(numbers, i);
|
||||
else
|
||||
local t = n.age / NUMBER_SECONDS;
|
||||
n.text:ClearAllPoints();
|
||||
n.text:SetPoint("BOTTOM", BW.scene, "BOTTOMLEFT", sx / scale + n.dx, sy / scale + NUMBER_RISE * t);
|
||||
if t > 0.5 then
|
||||
n.text:SetAlpha(2 - 2 * t);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearNumbers()
|
||||
for i = table.getn(numbers), 1, -1 do
|
||||
numbers[i].text:Hide();
|
||||
tinsert(numberPool, numbers[i]);
|
||||
numbers[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
local function Flash(e)
|
||||
e.flash = FLASH_SECONDS;
|
||||
if not e.lit then
|
||||
e.lit = true;
|
||||
e.actor:SetHighlight("mouseover", true);
|
||||
end
|
||||
end
|
||||
|
||||
local function Unflash(e)
|
||||
if e.lit then
|
||||
e.lit = false;
|
||||
e.actor:SetHighlight("mouseover", false);
|
||||
end
|
||||
end
|
||||
|
||||
local function Damage(e, amount, color)
|
||||
if not e.alive then
|
||||
return;
|
||||
end
|
||||
-- Masteries: Corruption and Frost Nova's Shatter make their marks take more.
|
||||
if e.dotLeft and Mastered("corruption") then
|
||||
amount = amount * SPELLS.corruption.mastery.amplify;
|
||||
end
|
||||
if e.frozenUntil and BW.run.time < e.frozenUntil and Mastered("frostnova") then
|
||||
amount = amount * SPELLS.frostnova.mastery.amplify;
|
||||
end
|
||||
local dealt = math.floor(amount * BW.run.damageScale + 0.5);
|
||||
e.hp = e.hp - dealt;
|
||||
if BW.run.passives.vampiric then
|
||||
BW.run.leech = BW.run.leech + dealt * VAMPIRIC_SHARE * BW.run.passives.vampiric;
|
||||
end
|
||||
Flash(e);
|
||||
ShowNumber(e, dealt, color);
|
||||
if e.hp <= 0 then
|
||||
Kill(e);
|
||||
end
|
||||
end
|
||||
|
||||
-- Throws an enemy away from (x, y) at `knock` (out, up). A corpse flies too,
|
||||
-- in its death pose.
|
||||
local function Blast(e, x, y, knock)
|
||||
local share = e.kind == ELITE and TUNING.ELITE_KNOCK_SHARE or 1;
|
||||
e.actor:KnockBack(x, y, knock[1] * share, knock[2] * share);
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Lasting effects: a hold (a root or a stun) keeps an enemy where it is, a
|
||||
-- fear sends it away from you, a weakness softens its bites. A hold or a fear
|
||||
-- can show a SpellVisualKit on the enemy while it lasts. Elites shrug off
|
||||
-- part of a hold or a fear.
|
||||
|
||||
local function ShowKit(e, kit, untilTime)
|
||||
if not kit then
|
||||
return;
|
||||
end
|
||||
if e.kit ~= kit then
|
||||
e.kit = kit;
|
||||
e.actor:SetSpellVisualKit(kit);
|
||||
end
|
||||
e.kitUntil = math.max(e.kitUntil or 0, untilTime);
|
||||
end
|
||||
|
||||
local function HoldShare(e)
|
||||
return e.kind == ELITE and TUNING.ELITE_HOLD_SHARE or 1;
|
||||
end
|
||||
|
||||
-- `frozen`: the hold is ice (Frost Nova's Shatter counts it).
|
||||
local function Hold(e, seconds, kit, frozen)
|
||||
e.stunUntil = math.max(e.stunUntil or 0, BW.run.time + seconds * HoldShare(e));
|
||||
if frozen then
|
||||
e.frozenUntil = e.stunUntil;
|
||||
end
|
||||
e.fearUntil = nil;
|
||||
e.actor:StopMoving();
|
||||
ShowKit(e, kit, e.stunUntil);
|
||||
end
|
||||
|
||||
local function Fear(e, seconds, kit)
|
||||
e.fearUntil = math.max(e.fearUntil or 0, BW.run.time + seconds * HoldShare(e));
|
||||
ShowKit(e, kit, e.fearUntil);
|
||||
end
|
||||
|
||||
local function Weaken(e, share, seconds)
|
||||
e.weakShare = math.max(e.weakShare or 0, share);
|
||||
e.weakUntil = math.max(e.weakUntil or 0, BW.run.time + seconds);
|
||||
end
|
||||
|
||||
-- Ends the effects whose time has run out (the step, for each live enemy),
|
||||
-- and burns the fleeing with Psychic Scream's Terror.
|
||||
local function UpdateStates(e, dt)
|
||||
local t = BW.run.time;
|
||||
if e.fearUntil and Mastered("scream") then
|
||||
e.fearTick = (e.fearTick or 1) - dt;
|
||||
if e.fearTick <= 0 then
|
||||
e.fearTick = e.fearTick + 1;
|
||||
Damage(e, SPELLS.scream.mastery.damage, SPELLS.scream.color);
|
||||
if not e.alive then
|
||||
return;
|
||||
end
|
||||
end
|
||||
end
|
||||
if e.kitUntil and t >= e.kitUntil then
|
||||
e.kit, e.kitUntil = nil, nil;
|
||||
e.actor:SetSpellVisualKit();
|
||||
end
|
||||
if e.fearUntil and t >= e.fearUntil then
|
||||
e.fearUntil, e.fearTick = nil, nil;
|
||||
end
|
||||
if e.weakUntil and t >= e.weakUntil then
|
||||
e.weakUntil, e.weakShare = nil, nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- The share of an enemy's bite that lands.
|
||||
local function BiteShare(e)
|
||||
return e.weakShare and 1 - e.weakShare or 1;
|
||||
end
|
||||
|
||||
local function Release(i)
|
||||
local e = enemies[i];
|
||||
Unflash(e);
|
||||
e.actor:SetMovementEnabled(false);
|
||||
e.actor:Hide();
|
||||
local pool = pools[e.kind.key];
|
||||
if not pool then
|
||||
pool = {};
|
||||
pools[e.kind.key] = pool;
|
||||
end
|
||||
tinsert(pool, e);
|
||||
table.remove(enemies, i);
|
||||
end
|
||||
|
||||
local function ClearEnemies()
|
||||
for i = table.getn(enemies), 1, -1 do
|
||||
if enemies[i].alive then
|
||||
BW.run.alive = BW.run.alive - 1;
|
||||
end
|
||||
Release(i);
|
||||
end
|
||||
end
|
||||
|
||||
-- How many should be alive at this second of the run.
|
||||
local function TargetAlive(t)
|
||||
return math.min(MAX_ALIVE, 8 + math.floor(t * 0.38));
|
||||
end
|
||||
|
||||
local function PickKind()
|
||||
local open = 0;
|
||||
for _, kind in ipairs(KINDS) do
|
||||
if BW.run.time >= kind.from then
|
||||
open = open + 1;
|
||||
end
|
||||
end
|
||||
-- Newer kinds are rarer: the first kind open gets the most weight.
|
||||
local roll = math.random() * (open * (open + 1) / 2);
|
||||
for i = 1, open do
|
||||
local w = open - i + 1;
|
||||
if roll < w then
|
||||
return KINDS[i];
|
||||
end
|
||||
roll = roll - w;
|
||||
end
|
||||
return KINDS[1];
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.Blast = Blast
|
||||
BW.Hold = Hold
|
||||
BW.Fear = Fear
|
||||
BW.Weaken = Weaken
|
||||
BW.UpdateStates = UpdateStates
|
||||
BW.BiteShare = BiteShare
|
||||
BW.Spawn = Spawn
|
||||
BW.Kill = Kill
|
||||
BW.ShowNumber = ShowNumber
|
||||
BW.UpdateNumbers = UpdateNumbers
|
||||
BW.ClearNumbers = ClearNumbers
|
||||
BW.Flash = Flash
|
||||
BW.Unflash = Unflash
|
||||
BW.Damage = Damage
|
||||
BW.Release = Release
|
||||
BW.ClearEnemies = ClearEnemies
|
||||
BW.TargetAlive = TargetAlive
|
||||
BW.PickKind = PickKind
|
||||
@@ -0,0 +1,614 @@
|
||||
-- Broodwatch: hold Onyxia's Lair against an endless brood.
|
||||
--
|
||||
-- You run with W A S D (in screen directions). Your spells cast themselves.
|
||||
-- Whelps pour into the lair and chase you; each kill drops a gem, and gems
|
||||
-- level you up. At each level you pick one of three upgrades (click, or
|
||||
-- press 1 2 3). An elite dragonspawn joins every 90 seconds. Survive ten
|
||||
-- minutes to win.
|
||||
--
|
||||
-- /broodwatch starts a run. P pauses. Escape leaves.
|
||||
--
|
||||
-- Prototype: one ClassicAPI ModelScene, the lair as its WMO, every unit an
|
||||
-- actor. The frame time and the Lua time per frame show in the corner.
|
||||
|
||||
local _, BW = ...
|
||||
local Blast = BW.Blast
|
||||
local TUNING = BW.TUNING
|
||||
local LAIR_MAP = BW.LAIR_MAP
|
||||
local CENTER = BW.CENTER
|
||||
local FAR_CLIP = BW.FAR_CLIP
|
||||
local CAMERA_YAW = BW.CAMERA_YAW
|
||||
local CAMERA_PITCH = BW.CAMERA_PITCH
|
||||
local CAMERA_ZOOM = BW.CAMERA_ZOOM
|
||||
local RUN_SECONDS = BW.RUN_SECONDS
|
||||
local ELITE_EVERY = BW.ELITE_EVERY
|
||||
local RETARGET_SECONDS = BW.RETARGET_SECONDS
|
||||
local SURROUND_RADIUS = BW.SURROUND_RADIUS
|
||||
local HERO_HP = BW.HERO_HP
|
||||
local MAGNET = BW.MAGNET
|
||||
local GEM_SPEED = BW.GEM_SPEED
|
||||
local GEM_TAKE = BW.GEM_TAKE
|
||||
local ELITE = BW.ELITE
|
||||
local SPELLS = BW.SPELLS
|
||||
local PASSIVES = BW.PASSIVES
|
||||
local SOULSTONE_SPELL = BW.SOULSTONE_SPELL
|
||||
local SOULSTONE_BLAST = BW.SOULSTONE_BLAST
|
||||
local AT_ONCE = BW.AT_ONCE
|
||||
local enemies = BW.enemies
|
||||
local gemPool = BW.gemPool
|
||||
local gems = BW.gems
|
||||
local Dist2 = BW.Dist2
|
||||
local Clock = BW.Clock
|
||||
local ClearGems = BW.ClearGems
|
||||
local Spawn = BW.Spawn
|
||||
local Kill = BW.Kill
|
||||
local ShowNumber = BW.ShowNumber
|
||||
local UpdateNumbers = BW.UpdateNumbers
|
||||
local ClearNumbers = BW.ClearNumbers
|
||||
local Unflash = BW.Unflash
|
||||
local Damage = BW.Damage
|
||||
local Release = BW.Release
|
||||
local ClearEnemies = BW.ClearEnemies
|
||||
local TargetAlive = BW.TargetAlive
|
||||
local PickKind = BW.PickKind
|
||||
local Stat = BW.Stat
|
||||
local Nearest = BW.Nearest
|
||||
local UpdateTotems = BW.UpdateTotems
|
||||
local ClearTotems = BW.ClearTotems
|
||||
local Unslow = BW.Unslow
|
||||
local UpdateAreas = BW.UpdateAreas
|
||||
local UpdateLandings = BW.UpdateLandings
|
||||
local ClearLandings = BW.ClearLandings
|
||||
local ClearAreas = BW.ClearAreas
|
||||
local Fire = BW.Fire
|
||||
local CastNext = BW.CastNext
|
||||
local Corrupt = BW.Corrupt
|
||||
local UpdateBuild = BW.UpdateBuild
|
||||
local SetPaused = BW.SetPaused
|
||||
local MaxHp = BW.MaxHp
|
||||
local UpdateHealth = BW.UpdateHealth
|
||||
local XpNeeded = BW.XpNeeded
|
||||
local GainXp = BW.GainXp
|
||||
local Taunted = BW.Taunted
|
||||
local UpdateItems = BW.UpdateItems
|
||||
local ClearItems = BW.ClearItems
|
||||
local UpdateStates = BW.UpdateStates
|
||||
local BiteShare = BW.BiteShare
|
||||
local UpdateWards = BW.UpdateWards
|
||||
local ClearWards = BW.ClearWards
|
||||
local UpdatePet = BW.UpdatePet
|
||||
local ClearPet = BW.ClearPet
|
||||
local EndShield = BW.EndShield
|
||||
local Mastered = BW.Mastered
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Movement: W A S D in screen directions. The hero faces the way it runs.
|
||||
|
||||
local function Held(key)
|
||||
return BW.held[key] and 1 or 0;
|
||||
end
|
||||
|
||||
local function Steer()
|
||||
if not BW.run or BW.paused or BW.run.over then
|
||||
return;
|
||||
end
|
||||
local forward = Held("W") - Held("S");
|
||||
local left = Held("A") - Held("D");
|
||||
if forward == 0 and left == 0 then
|
||||
if BW.moving then
|
||||
BW.hero:StopMoving();
|
||||
BW.moving = false;
|
||||
end
|
||||
return;
|
||||
end
|
||||
local yaw = BW.camera:GetYaw() + math.atan2(left, forward);
|
||||
if not BW.moving or math.abs(yaw - BW.run.heroYaw) > 0.001 then
|
||||
BW.run.heroYaw = yaw;
|
||||
BW.hero:SetYaw(yaw);
|
||||
end
|
||||
if not BW.moving then
|
||||
BW.hero:StartMovingForward();
|
||||
BW.moving = true;
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The run
|
||||
|
||||
local function EndRun(won)
|
||||
BW.run.over = true;
|
||||
SetPaused(true);
|
||||
BW.cards:Hide();
|
||||
ClearNumbers();
|
||||
BW.run.hurtGlow = 0;
|
||||
BW.hurtTexture:SetAlpha(0);
|
||||
if won then
|
||||
BW.bannerText:SetText("The lair holds!");
|
||||
else
|
||||
BW.bannerText:SetText("You fell at " .. Clock(BW.run.time));
|
||||
end
|
||||
BW.hintText:SetText(string.format("Level %d, %d kills. R: try again. Escape: leave.", BW.run.level, BW.run.kills));
|
||||
end
|
||||
|
||||
BW.heroNumber = {} -- ShowNumber's stand-in record for the hero
|
||||
local HEAL_COLOR = { 0.3, 1, 0.3 }
|
||||
|
||||
local function StartRun()
|
||||
if BW.run then
|
||||
ClearEnemies();
|
||||
ClearGems();
|
||||
ClearNumbers();
|
||||
ClearTotems();
|
||||
ClearAreas();
|
||||
ClearLandings();
|
||||
ClearItems();
|
||||
ClearWards();
|
||||
ClearPet();
|
||||
end
|
||||
BW.hero:SetSpellVisualKit();
|
||||
BW.hurtTexture:SetAlpha(0);
|
||||
BW.run = {
|
||||
time = 0, kills = 0, alive = 0, level = 1, xp = 0, pendingLevels = 0,
|
||||
hp = HERO_HP, damageScale = 1, heroYaw = 0,
|
||||
spells = { frostbolt = { level = 1, cooldown = 0, pending = 0 } },
|
||||
passives = {},
|
||||
castQueue = {}, castGap = 0, inFlight = {},
|
||||
spawnGap = 0, nextElite = ELITE_EVERY, retargetCarry = 0, retargetCursor = 1,
|
||||
shownSecond = -1, perfFrames = 0, perfLua = 0, perfClock = 0, hurtGlow = 0,
|
||||
spreads = {}, pendingHeal = 0, leech = 0, landings = {},
|
||||
};
|
||||
SetPaused(false);
|
||||
BW.held = {};
|
||||
BW.moving = false;
|
||||
BW.hero:SetMovementEnabled(false);
|
||||
BW.hero:SetPosition(CENTER[1], CENTER[2], CENTER[3] + 0.5);
|
||||
BW.hero:SetYaw(CAMERA_YAW);
|
||||
BW.hero:Show();
|
||||
BW.hero:SetMovementEnabled(true);
|
||||
BW.run.heroRun = BW.hero:GetMovementSpeed("run");
|
||||
BW.cards:Hide();
|
||||
BW.bannerText:SetText("");
|
||||
BW.hintText:SetText("");
|
||||
BW.xpBar:SetMinMaxValues(0, XpNeeded(1), AT_ONCE);
|
||||
BW.xpBar:SetValue(0, AT_ONCE);
|
||||
UpdateHealth(AT_ONCE);
|
||||
UpdateBuild();
|
||||
BW.camera:SetZoomDistance(CAMERA_ZOOM);
|
||||
BW.camera:SnapAllInterpolatedValues();
|
||||
end
|
||||
|
||||
local function IsCorrupted(e)
|
||||
return e.dotLeft ~= nil;
|
||||
end
|
||||
|
||||
-- Sends a slice of the brood after the hero, so each one gets a new MoveTo
|
||||
-- every RETARGET_SECONDS, at its own point of a ring around the hero.
|
||||
local function Chase(dt, hx, hy, hz)
|
||||
local n = table.getn(enemies);
|
||||
if n == 0 then
|
||||
return;
|
||||
end
|
||||
BW.run.retargetCarry = BW.run.retargetCarry + n * dt / RETARGET_SECONDS;
|
||||
while BW.run.retargetCarry >= 1 do
|
||||
BW.run.retargetCarry = BW.run.retargetCarry - 1;
|
||||
if BW.run.retargetCursor > n then
|
||||
BW.run.retargetCursor = 1;
|
||||
end
|
||||
local e = enemies[BW.run.retargetCursor];
|
||||
if e.alive and not (e.stunUntil and BW.run.time < e.stunUntil) and not e.actor:IsFalling() then
|
||||
if e.fearUntil then
|
||||
local x, y = e.actor:GetPosition();
|
||||
local dx, dy = x - hx, y - hy;
|
||||
local d = math.sqrt(dx * dx + dy * dy);
|
||||
if d < 0.1 then
|
||||
dx, dy, d = math.cos(e.angle), math.sin(e.angle), 1;
|
||||
end
|
||||
local step = TUNING.FEAR_STEP / d;
|
||||
e.actor:MoveTo(x + dx * step, y + dy * step, hz);
|
||||
elseif Taunted(e) then
|
||||
local cx, cy, cz = e.taunter.actor:GetPosition();
|
||||
e.actor:MoveTo(cx + SURROUND_RADIUS * math.cos(e.angle), cy + SURROUND_RADIUS * math.sin(e.angle), cz);
|
||||
else
|
||||
e.actor:MoveTo(hx + SURROUND_RADIUS * math.cos(e.angle), hy + SURROUND_RADIUS * math.sin(e.angle), hz);
|
||||
end
|
||||
end
|
||||
BW.run.retargetCursor = BW.run.retargetCursor + 1;
|
||||
end
|
||||
end
|
||||
|
||||
local function Step(dt)
|
||||
BW.run.time = BW.run.time + dt;
|
||||
local hx, hy, hz = BW.hero:GetPosition();
|
||||
|
||||
-- Spawns.
|
||||
BW.run.spawnGap = BW.run.spawnGap - dt;
|
||||
if BW.run.spawnGap <= 0 then
|
||||
BW.run.spawnGap = 0.1;
|
||||
local want = TargetAlive(BW.run.time) - BW.run.alive;
|
||||
for _ = 1, math.min(want, 5) do
|
||||
Spawn(PickKind());
|
||||
end
|
||||
end
|
||||
if BW.run.time >= BW.run.nextElite then
|
||||
BW.run.nextElite = BW.run.nextElite + ELITE_EVERY;
|
||||
for _ = 1, 1 + math.floor(BW.run.time / 180) do
|
||||
Spawn(ELITE);
|
||||
end
|
||||
end
|
||||
|
||||
Chase(dt, hx, hy, hz);
|
||||
|
||||
-- Contact damage, and the corpses. Thorns burns the biters once a second.
|
||||
local hurt = 0;
|
||||
local thorns = false;
|
||||
if BW.run.thornsUntil then
|
||||
if BW.run.time >= BW.run.thornsUntil then
|
||||
BW.run.thornsUntil = nil;
|
||||
else
|
||||
BW.run.thornsTick = BW.run.thornsTick - dt;
|
||||
if BW.run.thornsTick <= 0 then
|
||||
BW.run.thornsTick = BW.run.thornsTick + 1;
|
||||
thorns = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
for i = table.getn(enemies), 1, -1 do
|
||||
local e = enemies[i];
|
||||
if e.lit then
|
||||
e.flash = e.flash - dt;
|
||||
if e.flash <= 0 then
|
||||
Unflash(e);
|
||||
end
|
||||
end
|
||||
if e.alive and e.dotLeft then
|
||||
e.dotTick = e.dotTick - dt;
|
||||
if e.dotTick <= 0 then
|
||||
e.dotTick = e.dotTick + 1;
|
||||
e.dotLeft = e.dotLeft - 1;
|
||||
-- The last tick can kill too, and then it still spreads (Kill
|
||||
-- reads dotLeft), so it ends after the damage.
|
||||
Damage(e, e.dotDamage, SPELLS.corruption.color);
|
||||
if e.dotLeft and e.dotLeft <= 0 then
|
||||
e.dotLeft = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
if e.alive and e.slowUntil and BW.run.time >= e.slowUntil then
|
||||
Unslow(e);
|
||||
end
|
||||
if e.alive then
|
||||
UpdateStates(e, dt);
|
||||
end
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
local reach = e.kind.reach;
|
||||
if not Taunted(e) and not e.fearUntil and Dist2(x, y, hx, hy) <= reach * reach then
|
||||
hurt = hurt + e.dps * BiteShare(e) * (1 - PASSIVES.devotion.per * (BW.run.passives.devotion or 0) / 100);
|
||||
if thorns then
|
||||
Damage(e, BW.run.thornsDamage, SPELLS.thorns.color);
|
||||
if Mastered("thorns") then
|
||||
BW.run.pendingHeal = BW.run.pendingHeal + SPELLS.thorns.mastery.heal;
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
-- The corpse lies, then fades out as the world's do, and goes
|
||||
-- once the fade has hidden it.
|
||||
e.dying = e.dying - dt;
|
||||
if e.dying <= 0 then
|
||||
if not e.fading then
|
||||
e.fading = true;
|
||||
e.actor:FadeOut();
|
||||
elseif not e.actor:IsShown() then
|
||||
Release(i);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Corruption passes from each enemy that died with it to its neighbours.
|
||||
for i = table.getn(BW.run.spreads), 1, -1 do
|
||||
local from = BW.run.spreads[i];
|
||||
BW.run.spreads[i] = nil;
|
||||
local fx, fy = from.actor:GetPosition();
|
||||
for _ = 1, from.dotSpread do
|
||||
local e = Nearest(fx, fy, SPELLS.corruption.hop, IsCorrupted);
|
||||
if not e then
|
||||
break;
|
||||
end
|
||||
Corrupt(e, from.dotDamage, from.dotSeconds, from.dotSpread);
|
||||
end
|
||||
end
|
||||
UpdateTotems(dt);
|
||||
UpdateAreas(dt);
|
||||
UpdateLandings(dt);
|
||||
UpdateItems(dt, hx, hy, hz);
|
||||
UpdateWards(dt);
|
||||
UpdatePet(dt, hx, hy, hz);
|
||||
if BW.run.shoutUntil and BW.run.time >= BW.run.shoutUntil then
|
||||
BW.run.shoutUntil = nil;
|
||||
BW.ApplyHeroSpeed();
|
||||
end
|
||||
|
||||
-- Power Word: Shield soaks the bites first.
|
||||
local taken = hurt * dt;
|
||||
if BW.run.shield then
|
||||
local soak = math.min(BW.run.shield, taken);
|
||||
BW.run.shield = BW.run.shield - soak;
|
||||
taken = taken - soak;
|
||||
if BW.run.shield <= 0 then
|
||||
EndShield(true);
|
||||
elseif BW.run.time >= BW.run.shieldUntil then
|
||||
EndShield();
|
||||
elseif soak > 0 then
|
||||
UpdateHealth();
|
||||
end
|
||||
end
|
||||
|
||||
-- The screen's edge glows red while the brood bites, and fades after.
|
||||
local glow = BW.run.hurtGlow;
|
||||
if taken > 0 then
|
||||
glow = math.min(0.8, glow + 4 * dt);
|
||||
elseif glow > 0 then
|
||||
glow = math.max(0, glow - 1.5 * dt);
|
||||
end
|
||||
if glow ~= BW.run.hurtGlow then
|
||||
BW.run.hurtGlow = glow;
|
||||
BW.hurtTexture:SetAlpha(glow);
|
||||
end
|
||||
|
||||
if BW.run.leech > 0 then
|
||||
-- Vampiric Embrace heals at most its rate; the rest of a burst is
|
||||
-- kept for at most a second.
|
||||
local rate = PASSIVES.vampiric.per * (BW.run.passives.vampiric or 0);
|
||||
BW.run.leech = math.min(BW.run.leech, rate);
|
||||
local heal = math.min(BW.run.leech, rate * dt);
|
||||
BW.run.leech = BW.run.leech - heal;
|
||||
if heal > 0 and BW.run.hp < MaxHp() then
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + heal);
|
||||
UpdateHealth();
|
||||
end
|
||||
end
|
||||
if BW.run.pendingHeal > 0 then
|
||||
local heal = math.floor(BW.run.pendingHeal + 0.5);
|
||||
BW.run.pendingHeal = 0;
|
||||
if heal > 0 and BW.run.hp < MaxHp() then
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + heal);
|
||||
UpdateHealth();
|
||||
BW.heroNumber.actor = BW.hero;
|
||||
ShowNumber(BW.heroNumber, heal, HEAL_COLOR, "+" .. heal);
|
||||
end
|
||||
end
|
||||
|
||||
local regen = PASSIVES.renewal.per * (BW.run.passives.renewal or 0);
|
||||
if taken > 0 or (regen > 0 and BW.run.hp < MaxHp()) then
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + regen * dt - taken);
|
||||
UpdateHealth();
|
||||
if BW.run.hp <= 0 and BW.run.passives.soulstone and not BW.run.soulstoneUsed then
|
||||
-- Rise again at half health, and throw back the brood close by.
|
||||
BW.run.soulstoneUsed = true;
|
||||
BW.run.hp = MaxHp() * 0.5;
|
||||
UpdateHealth();
|
||||
BW.hero:PlaySpellVisual(SOULSTONE_SPELL, BW.hero, 0);
|
||||
local r2 = SOULSTONE_BLAST * SOULSTONE_BLAST;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, hx, hy) <= r2 then
|
||||
if e.kind ~= ELITE then
|
||||
Kill(e);
|
||||
end
|
||||
Blast(e, hx, hy, TUNING.SOULSTONE_KNOCK);
|
||||
end
|
||||
end
|
||||
end
|
||||
UpdateBuild();
|
||||
end
|
||||
if BW.run.hp <= 0 then
|
||||
EndRun(false);
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
-- Gems.
|
||||
local magnet = MAGNET * (1 + 0.4 * (BW.run.passives.reach or 0));
|
||||
local m2 = magnet * magnet;
|
||||
local take2 = GEM_TAKE * GEM_TAKE;
|
||||
local gained = 0;
|
||||
for i = table.getn(gems), 1, -1 do
|
||||
local gem = gems[i];
|
||||
local d2 = Dist2(gem.x, gem.y, hx, hy);
|
||||
if not gem.pulled and d2 <= m2 then
|
||||
gem.pulled = true;
|
||||
end
|
||||
if gem.pulled then
|
||||
if d2 <= take2 then
|
||||
gained = gained + gem.xp;
|
||||
gem.actor:Hide();
|
||||
tinsert(gemPool, gem);
|
||||
table.remove(gems, i);
|
||||
else
|
||||
-- It flies at you, faster than you run, and rises to your chest.
|
||||
local d = math.sqrt(d2);
|
||||
local step = math.min(1, GEM_SPEED * dt / d);
|
||||
gem.x = gem.x + (hx - gem.x) * step;
|
||||
gem.y = gem.y + (hy - gem.y) * step;
|
||||
gem.z = gem.z + (hz + 1 - gem.z) * step;
|
||||
gem.actor:SetPosition(gem.x, gem.y, gem.z);
|
||||
end
|
||||
end
|
||||
end
|
||||
if gained > 0 then
|
||||
GainXp(gained * (1 + PASSIVES.wisdom.per * (BW.run.passives.wisdom or 0) / 100));
|
||||
end
|
||||
|
||||
-- Spells.
|
||||
local haste = 1 - 0.08 * (BW.run.passives.haste or 0);
|
||||
for key, state in pairs(BW.run.spells) do
|
||||
state.cooldown = state.cooldown - dt;
|
||||
if state.cooldown <= 0 and state.pending == 0 and Fire(key, state) then
|
||||
state.cooldown = Stat(SPELLS[key], "cooldown", state.level) * haste;
|
||||
state.cdFull = state.cooldown;
|
||||
if state.slot then
|
||||
state.slot.cd:SetCooldown(GetTime(), state.cooldown);
|
||||
end
|
||||
end
|
||||
end
|
||||
BW.run.castGap = BW.run.castGap - dt;
|
||||
if BW.run.castGap <= 0 then
|
||||
CastNext();
|
||||
end
|
||||
|
||||
if BW.run.time >= RUN_SECONDS then
|
||||
EndRun(true);
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateHud(dt, luaMs)
|
||||
local second = math.floor(BW.run.time);
|
||||
if second ~= BW.run.shownSecond then
|
||||
BW.run.shownSecond = second;
|
||||
BW.infoText:SetText(string.format("Level %d %s Kills %d", BW.run.level, Clock(BW.run.time), BW.run.kills));
|
||||
end
|
||||
BW.run.perfFrames = BW.run.perfFrames + 1;
|
||||
BW.run.perfLua = BW.run.perfLua + luaMs;
|
||||
BW.run.perfClock = BW.run.perfClock + dt;
|
||||
if BW.run.perfClock >= 1 then
|
||||
BW.perfText:SetText(string.format("%.0f fps Lua %.2f ms/frame %d alive %d gems",
|
||||
BW.run.perfFrames / BW.run.perfClock, BW.run.perfLua / BW.run.perfFrames, BW.run.alive, table.getn(gems)));
|
||||
BW.run.perfFrames, BW.run.perfLua, BW.run.perfClock = 0, 0, 0;
|
||||
end
|
||||
end
|
||||
|
||||
local function OnUpdate()
|
||||
if not BW.run or BW.paused or BW.run.over then
|
||||
return;
|
||||
end
|
||||
local dt = arg1;
|
||||
local t0 = debugprofilestop();
|
||||
Steer();
|
||||
Step(dt);
|
||||
UpdateNumbers(dt);
|
||||
local luaMs = debugprofilestop() - t0;
|
||||
if not BW.run.over then
|
||||
UpdateHud(dt, luaMs);
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Keys
|
||||
|
||||
local MOVE_KEYS = { W = true, A = true, S = true, D = true }
|
||||
|
||||
local function OnKeyDown()
|
||||
local key = arg1;
|
||||
if key == "ESCAPE" then
|
||||
BW.frame:Hide();
|
||||
return;
|
||||
end
|
||||
if not BW.run then
|
||||
return;
|
||||
end
|
||||
if BW.run.over then
|
||||
if key == "R" then
|
||||
StartRun();
|
||||
end
|
||||
return;
|
||||
end
|
||||
if BW.cards:IsShown() then
|
||||
if key == "1" or key == "2" or key == "3" then
|
||||
Broodwatch_PickCard(tonumber(key));
|
||||
end
|
||||
return;
|
||||
end
|
||||
if key == "P" then
|
||||
SetPaused(not BW.paused);
|
||||
BW.bannerText:SetText(BW.paused and "Paused" or "");
|
||||
BW.hintText:SetText(BW.paused and "P: resume." or "");
|
||||
return;
|
||||
end
|
||||
if MOVE_KEYS[key] then
|
||||
BW.held[key] = true;
|
||||
Steer();
|
||||
end
|
||||
end
|
||||
|
||||
local function OnKeyUp()
|
||||
BW.held[arg1] = nil;
|
||||
Steer();
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Setup
|
||||
|
||||
local function Build()
|
||||
debugprofilestart();
|
||||
BW.scene:TransitionToModelSceneID(4, CAMERA_TRANSITION_TYPE_IMMEDIATE, CAMERA_MODIFICATION_TYPE_DISCARD, true);
|
||||
BW.scene:SetWorldModelByMapID(LAIR_MAP);
|
||||
BW.scene:SetCameraFarClip(FAR_CLIP);
|
||||
|
||||
local _, cameraIDs = C_ModelInfo.GetModelSceneInfoByID(4);
|
||||
BW.camera = BW.scene:AddCamera(CameraRegistry:CreateCameraByType("FollowCamera"));
|
||||
BW.camera:ApplyFromModelSceneCameraInfo(C_ModelInfo.GetModelSceneCameraInfoByID(cameraIDs[1]),
|
||||
CAMERA_TRANSITION_TYPE_IMMEDIATE, CAMERA_MODIFICATION_TYPE_DISCARD);
|
||||
BW.camera:SetTargetSpline(nil);
|
||||
BW.camera:SetOrientationSpline(nil);
|
||||
BW.camera:SetMinZoomDistance(12);
|
||||
BW.camera:SetMaxZoomDistance(60);
|
||||
|
||||
BW.hero = BW.scene:AcquireActor();
|
||||
BW.hero:SetModelByUnit("player");
|
||||
BW.hero:HookScript("OnSpellVisualImpact", BW.OnImpact);
|
||||
BW.scene:SetSoundListenerActor(BW.hero);
|
||||
|
||||
BW.camera:SetFollowActor(BW.hero);
|
||||
BW.camera:SetYaw(CAMERA_YAW);
|
||||
BW.camera:SetPitch(CAMERA_PITCH);
|
||||
BW.camera:SetZoomDistance(CAMERA_ZOOM);
|
||||
BW.camera:SnapAllInterpolatedValues();
|
||||
BW.scene:SetActiveCamera(BW.camera);
|
||||
end
|
||||
|
||||
function Broodwatch_OnLoad(self)
|
||||
BW.frame = self;
|
||||
BW.scene = getglobal(self:GetName() .. "Scene");
|
||||
BW.feedback = getglobal(self:GetName() .. "Feedback");
|
||||
BW.hurtTexture = getglobal(self:GetName() .. "FeedbackHurt");
|
||||
BW.hurtTexture:SetAlpha(0);
|
||||
-- Siblings at one frame level draw in no set order, and the scene's
|
||||
-- paint covered the feedback (item icons, numbers). Chained parents
|
||||
-- order them: scene, feedback over it, the HUD over both. SetParent
|
||||
-- moves a frame's children with it; SetFrameLevel would not.
|
||||
local hud = self:GetName() .. "Hud";
|
||||
BW.feedback:SetParent(BW.scene);
|
||||
getglobal(hud):SetParent(BW.feedback);
|
||||
BW.infoText = getglobal(hud .. "Info");
|
||||
BW.perfText = getglobal(hud .. "Perf");
|
||||
BW.bannerText = getglobal(hud .. "Banner");
|
||||
BW.hintText = getglobal(hud .. "Hint");
|
||||
BW.xpBar = getglobal(hud .. "XP");
|
||||
BW.healthBar = getglobal(hud .. "Health");
|
||||
BW.healthText = getglobal(hud .. "HealthText");
|
||||
BW.cards = getglobal(hud .. "Cards");
|
||||
BW.build = getglobal(hud .. "Build");
|
||||
self:SetScript("OnUpdate", OnUpdate);
|
||||
self:SetScript("OnKeyDown", OnKeyDown);
|
||||
self:SetScript("OnKeyUp", OnKeyUp);
|
||||
self:SetScript("OnShow", function()
|
||||
BW.frame:EnableKeyboard(true);
|
||||
end);
|
||||
self:SetScript("OnHide", function()
|
||||
BW.frame:EnableKeyboard(false);
|
||||
if BW.run and not BW.run.over then
|
||||
EndRun(false);
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
SLASH_BROODWATCH1 = "/broodwatch";
|
||||
SlashCmdList["BROODWATCH"] = function()
|
||||
BW.frame:Show();
|
||||
if not BW.hero then
|
||||
Build();
|
||||
end
|
||||
StartRun();
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
## Interface: 11200
|
||||
## Title: Broodwatch
|
||||
## Notes: Hold Onyxia's Lair against an endless brood. /broodwatch starts a run.
|
||||
## Dependencies: !!!ClassicAPI
|
||||
Data.lua
|
||||
Core.lua
|
||||
Brood.lua
|
||||
Spells.lua
|
||||
Hud.lua
|
||||
Items.lua
|
||||
Broodwatch.lua
|
||||
Broodwatch.xml
|
||||
@@ -0,0 +1,227 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<!-- A level-up card: icon, name, what kind of pick it is, what it changes,
|
||||
five rank pips and the key that picks it. Broodwatch.lua fills it. -->
|
||||
<Button name="BroodwatchCardTemplate" virtual="true">
|
||||
<Size x="210" y="280"/>
|
||||
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
|
||||
<EdgeSize>
|
||||
<AbsValue val="16"/>
|
||||
</EdgeSize>
|
||||
<TileSize>
|
||||
<AbsValue val="16"/>
|
||||
</TileSize>
|
||||
<BackgroundInsets>
|
||||
<AbsInset left="4" right="4" top="4" bottom="4"/>
|
||||
</BackgroundInsets>
|
||||
</Backdrop>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="$parentIcon">
|
||||
<Size x="64" y="64"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP" x="0" y="-20"/>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
<Texture name="$parentPip3">
|
||||
<Size x="18" y="6"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM" x="0" y="40"/>
|
||||
</Anchors>
|
||||
<Color r="1" g="1" b="1" a="1"/>
|
||||
</Texture>
|
||||
<Texture name="$parentPip2">
|
||||
<Size x="18" y="6"/>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" relativeTo="$parentPip3" relativePoint="LEFT" x="-4" y="0"/>
|
||||
</Anchors>
|
||||
<Color r="1" g="1" b="1" a="1"/>
|
||||
</Texture>
|
||||
<Texture name="$parentPip1">
|
||||
<Size x="18" y="6"/>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" relativeTo="$parentPip2" relativePoint="LEFT" x="-4" y="0"/>
|
||||
</Anchors>
|
||||
<Color r="1" g="1" b="1" a="1"/>
|
||||
</Texture>
|
||||
<Texture name="$parentPip4">
|
||||
<Size x="18" y="6"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentPip3" relativePoint="RIGHT" x="4" y="0"/>
|
||||
</Anchors>
|
||||
<Color r="1" g="1" b="1" a="1"/>
|
||||
</Texture>
|
||||
<Texture name="$parentPip5">
|
||||
<Size x="18" y="6"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentPip4" relativePoint="RIGHT" x="4" y="0"/>
|
||||
</Anchors>
|
||||
<Color r="1" g="1" b="1" a="1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentName" inherits="GameFontNormalLarge">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="$parentIcon" relativePoint="BOTTOM" x="0" y="-10"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentTag" inherits="GameFontHighlightSmall">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="$parentName" relativePoint="BOTTOM" x="0" y="-4"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentDesc" inherits="GameFontHighlight" justifyH="CENTER" justifyV="TOP">
|
||||
<Size x="184" y="96"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="$parentTag" relativePoint="BOTTOM" x="0" y="-14"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentKey" inherits="GameFontDisableSmall">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM" x="0" y="16"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
Broodwatch_PickCard(this:GetID());
|
||||
</OnClick>
|
||||
<OnEnter>
|
||||
Broodwatch_CardHover(this, true);
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
Broodwatch_CardHover(this, false);
|
||||
</OnLeave>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<Frame name="BroodwatchFrame" parent="UIParent" toplevel="true" hidden="true" frameStrata="DIALOG">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="40" y="-40"/>
|
||||
<Anchor point="BOTTOMRIGHT" x="-40" y="40"/>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture setAllPoints="true">
|
||||
<Color r="0" g="0" b="0" a="1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<ModelScene name="$parentScene" inherits="ModelSceneMixinTemplate" setAllPoints="true"/>
|
||||
<Frame name="$parentFeedback" setAllPoints="true">
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentHurt" file="Interface\FullScreenTextures\LowHealth" alphaMode="ADD" setAllPoints="true"/>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
<Frame name="$parentHud" setAllPoints="true">
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentInfo" inherits="GameFontHighlightLarge" justifyH="LEFT">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="14" y="-30"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentPerf" inherits="GameFontHighlightSmall" justifyH="RIGHT">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMRIGHT" x="-10" y="10"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentBanner" inherits="GameFontNormalHuge">
|
||||
<Anchors>
|
||||
<Anchor point="CENTER" x="0" y="140"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentHint" inherits="GameFontHighlight">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="$parentBanner" relativePoint="BOTTOM" x="0" y="-10"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<StatusBar name="$parentXP">
|
||||
<Size x="10" y="12"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="8" y="-8"/>
|
||||
<Anchor point="TOPRIGHT" x="-8" y="-8"/>
|
||||
</Anchors>
|
||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
||||
<BarColor r="0.58" g="0.0" b="0.55"/>
|
||||
</StatusBar>
|
||||
<StatusBar name="$parentHealth">
|
||||
<Size x="320" y="16"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM" x="0" y="24"/>
|
||||
</Anchors>
|
||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
||||
<BarColor r="0.1" g="0.85" b="0.1"/>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentText" inherits="GameFontHighlightSmall">
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</StatusBar>
|
||||
<!-- What you have: one slot per spell and passive, filled by Lua. -->
|
||||
<Frame name="$parentBuild">
|
||||
<Size x="10" y="34"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMLEFT" x="12" y="12"/>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
<!-- The level-up screen: the scene dims behind three cards. -->
|
||||
<Frame name="$parentCards" hidden="true" setAllPoints="true">
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture setAllPoints="true">
|
||||
<Color r="0" g="0" b="0" a="0.55"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentTitle" inherits="GameFontNormalHuge">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM" relativePoint="CENTER" x="0" y="160"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentHint" inherits="GameFontHighlight" text="Choose an upgrade: click a card, or press 1, 2 or 3.">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="$parentTitle" relativePoint="BOTTOM" x="0" y="-6"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="$parentCard2" inherits="BroodwatchCardTemplate" id="2">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="$parentHint" relativePoint="BOTTOM" x="0" y="-24"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
<Button name="$parentCard1" inherits="BroodwatchCardTemplate" id="1">
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" relativeTo="$parentCard2" relativePoint="LEFT" x="-18" y="0"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
<Button name="$parentCard3" inherits="BroodwatchCardTemplate" id="3">
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentCard2" relativePoint="RIGHT" x="18" y="0"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
</Frames>
|
||||
</Frame>
|
||||
</Frames>
|
||||
</Frame>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
Broodwatch_OnLoad(this);
|
||||
</OnLoad>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,105 @@
|
||||
-- Broodwatch core: the shared state, small helpers and the gems.
|
||||
--
|
||||
-- The files share the addon table BW (`local _, BW = ...`). A file keeps
|
||||
-- its own helpers local. What another file calls, and state that changes,
|
||||
-- lives on BW; constants are copied into a local at the top of each file
|
||||
-- that reads them. The TOC loads Data first and Broodwatch.lua last.
|
||||
|
||||
local _, BW = ...
|
||||
local GEM_PATH = BW.GEM_PATH
|
||||
local MAX_GEMS = BW.MAX_GEMS
|
||||
local MAX_LEVEL = BW.MAX_LEVEL
|
||||
|
||||
-- Whether the hero has the spell at rank V (its mastery).
|
||||
local function Mastered(key)
|
||||
local state = BW.run and BW.run.spells[key];
|
||||
return state ~= nil and state.level >= MAX_LEVEL;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- State
|
||||
|
||||
-- BW.OnImpact (the impact of any cast in flight) is defined with the spells,
|
||||
-- BW.DropItem (a kill's chance of an item) with the items.
|
||||
|
||||
BW.held = {}
|
||||
BW.moving = false
|
||||
|
||||
local pools = {} -- kind key -> list of free enemy records
|
||||
local enemies = {} -- live and dying enemy records
|
||||
local gemPool = {}
|
||||
local gems = {}
|
||||
|
||||
-- BW.run is the current run, or nil.
|
||||
BW.paused = false
|
||||
local offers = {} -- the three level-up choices
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Helpers
|
||||
|
||||
local function FloorAt(x, y, z)
|
||||
local _, _, hz = BW.scene:TraceSegment(x, y, z + 10, x, y, z - 50);
|
||||
return hz or z;
|
||||
end
|
||||
|
||||
local function Dist2(ax, ay, bx, by)
|
||||
local dx, dy = ax - bx, ay - by;
|
||||
return dx * dx + dy * dy;
|
||||
end
|
||||
|
||||
local function Clock(seconds)
|
||||
local s = math.floor(seconds);
|
||||
return string.format("%d:%02d", math.floor(s / 60), math.mod(s, 60));
|
||||
end
|
||||
|
||||
local function Shuffle(list)
|
||||
for i = table.getn(list), 2, -1 do
|
||||
local j = math.random(i);
|
||||
list[i], list[j] = list[j], list[i];
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Gems
|
||||
|
||||
local function DropGem(x, y, z, xp, scale)
|
||||
if table.getn(gems) >= MAX_GEMS then
|
||||
-- Too many on the floor: the experience goes straight to the oldest gem.
|
||||
gems[1].xp = gems[1].xp + xp;
|
||||
return;
|
||||
end
|
||||
local gem = table.remove(gemPool);
|
||||
if not gem then
|
||||
gem = { actor = BW.scene:AcquireActor() };
|
||||
gem.actor:SetModelByPath(GEM_PATH);
|
||||
end
|
||||
gem.xp = xp;
|
||||
gem.x, gem.y, gem.z = x, y, z;
|
||||
gem.pulled = false;
|
||||
gem.actor:SetScale(scale or 0.3);
|
||||
gem.actor:SetPosition(x, y, z);
|
||||
gem.actor:Show();
|
||||
tinsert(gems, gem);
|
||||
end
|
||||
|
||||
local function ClearGems()
|
||||
for i = table.getn(gems), 1, -1 do
|
||||
gems[i].actor:Hide();
|
||||
tinsert(gemPool, gems[i]);
|
||||
gems[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.Mastered = Mastered
|
||||
BW.pools = pools
|
||||
BW.enemies = enemies
|
||||
BW.gemPool = gemPool
|
||||
BW.gems = gems
|
||||
BW.offers = offers
|
||||
BW.FloorAt = FloorAt
|
||||
BW.Dist2 = Dist2
|
||||
BW.Clock = Clock
|
||||
BW.Shuffle = Shuffle
|
||||
BW.DropGem = DropGem
|
||||
BW.ClearGems = ClearGems
|
||||
@@ -0,0 +1,346 @@
|
||||
-- Broodwatch tuning and data: the game's constants, spells, passives and items.
|
||||
|
||||
local _, BW = ...
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Tuning
|
||||
|
||||
local LAIR_MAP = 249
|
||||
local CENTER = { 0.7, -211.97, -86.08 } -- Onyxia's spawn point, the room's floor
|
||||
local ROOM_RADIUS = 22 -- yards around CENTER where the brood spawns
|
||||
local SPAWN_MIN_DISTANCE = 13 -- yards from the hero
|
||||
local FAR_CLIP = 500
|
||||
|
||||
local CAMERA_YAW = 3.09
|
||||
local CAMERA_PITCH = 1.0
|
||||
local CAMERA_ZOOM = 50 -- each run starts zoomed out; the wheel zooms in
|
||||
|
||||
local RUN_SECONDS = 600
|
||||
local ELITE_EVERY = 90
|
||||
local MAX_ALIVE = 200
|
||||
local RETARGET_SECONDS = 0.3 -- each chaser gets a new MoveTo this often
|
||||
local SURROUND_RADIUS = 1.0 -- chasers aim at a ring around the hero, not its centre
|
||||
local CAST_SPACING = 0.3 -- seconds between two of the hero's casts
|
||||
local DEATH_SECONDS = 4.5 -- the death animation (1.5 s), then the corpse lies; then it fades out
|
||||
local ANIM_STAND = 0
|
||||
local ANIM_DEATH = 1
|
||||
|
||||
local HERO_HP = 100
|
||||
local MAGNET = 5 -- gems this close (yards) fly to you
|
||||
local GEM_SPEED = 20 -- yards a second, once pulled
|
||||
local GEM_TAKE = 0.8 -- a pulled gem this close is taken
|
||||
local GEM_PATH = "World\\Generic\\ActiveDoodads\\Rocks\\GemRock01.m2"
|
||||
local MAX_GEMS = 150
|
||||
|
||||
-- The brood. `from` is the second of the run when the kind starts to spawn.
|
||||
local KINDS = {
|
||||
{ key = "black", display = 497, hp = 20, dps = 5, xp = 1, reach = 1.4, speed = 3.0, from = 0 },
|
||||
{ key = "red", display = 956, hp = 30, dps = 6, xp = 2, reach = 1.4, speed = 4.4, from = 120 },
|
||||
{ key = "green", display = 694, hp = 70, dps = 9, xp = 3, reach = 1.4, speed = 2.4, from = 240 },
|
||||
}
|
||||
-- `speed` is the run speed in yards a second (the hero runs at 7): black
|
||||
-- whelps plod, red ones dart, green ones are slow and tough, and the elite
|
||||
-- is faster than any whelp but still slower than you.
|
||||
local ELITE = { key = "elite", display = 2534, hp = 220, dps = 9, xp = 20, reach = 2.2, speed = 4.8, gemScale = 0.6 }
|
||||
|
||||
-- Spells, by level 1 to 5. A missile spell hits `count` enemies in `range`;
|
||||
-- a nova spell hits every enemy within `radius` of the hero. At rank V a
|
||||
-- spell masters: `mastery` is what it does then, and its numbers.
|
||||
local SPELLS = {
|
||||
frostbolt = {
|
||||
spell = 116, blurb = "Bolts the nearest enemy.", color = { 0.55, 0.8, 1 }, kind = "missile", pick = "nearest", range = 24,
|
||||
damage = { 14, 18, 18, 24, 26 }, count = { 1, 1, 2, 2, 3 }, cooldown = { 1.2, 1.05, 1.05, 0.9, 0.8 },
|
||||
mastery = { text = "Frostbite: each bolt freezes its target for 1 second.", freeze = 1 },
|
||||
},
|
||||
shadowbolt = {
|
||||
spell = 686, blurb = "A heavy bolt at the toughest enemy in range.", color = { 0.8, 0.45, 1 }, kind = "missile", pick = "strongest", range = 26,
|
||||
damage = { 45, 60, 75, 75, 100 }, count = { 1, 1, 1, 2, 2 }, cooldown = { 2.6, 2.4, 2.2, 2.2, 2.0 },
|
||||
mastery = { text = "Shadowburn: a bolt that kills bursts, hitting enemies within 4 yards for half its damage.", radius = 4, share = 0.5 },
|
||||
},
|
||||
lightning = {
|
||||
spell = 403, blurb = "Strikes enemies at random.", color = { 1, 0.95, 0.4 }, kind = "missile", pick = "random", range = 22,
|
||||
damage = { 22, 28, 34, 40, 48 }, count = { 1, 2, 2, 3, 4 }, cooldown = { 1.8, 1.7, 1.6, 1.5, 1.3 },
|
||||
mastery = { text = "Overload: each bolt has a 1 in 3 chance to strike another enemy.", chance = 1 / 3 },
|
||||
},
|
||||
nova = {
|
||||
spell = 1449, blurb = "Bursts arcane power at every enemy around you.", color = { 0.85, 0.55, 1 }, kind = "nova",
|
||||
damage = { 10, 14, 18, 22, 28 }, radius = { 6, 6.5, 7, 7.5, 8 }, cooldown = { 5, 4.6, 4.2, 3.8, 3.4 },
|
||||
mastery = { text = "Echo: each explosion bursts a second time." },
|
||||
},
|
||||
chain = {
|
||||
spell = 421, blurb = "Leaps from enemy to enemy.",
|
||||
color = { 0.6, 0.75, 1 }, kind = "chain", pick = "nearest", range = 22, hop = 8,
|
||||
damage = { 20, 24, 30, 36, 44 }, jumps = { 3, 4, 4, 5, 6 }, cooldown = { 2.4, 2.2, 2.0, 1.8, 1.6 },
|
||||
mastery = { text = "Each leap strikes 20% harder than the one before.", grow = 1.2 },
|
||||
},
|
||||
corruption = {
|
||||
spell = 172, blurb = "Rots enemies every second, and spreads when they die.",
|
||||
color = { 0.55, 0.85, 0.3 }, kind = "dot", pick = "nearest", range = 22, hop = 6,
|
||||
damage = { 6, 8, 10, 12, 15 }, count = { 2, 3, 3, 4, 5 }, spread = { 1, 1, 2, 2, 3 },
|
||||
duration = { 6, 6, 7, 7, 8 }, cooldown = { 3, 3, 2.8, 2.6, 2.4 },
|
||||
mastery = { text = "Corrupted enemies take 25% more damage from everything.", amplify = 1.25 },
|
||||
},
|
||||
holynova = {
|
||||
spell = 15237, blurb = "Burns enemies around you, and heals you for each one.",
|
||||
color = { 1, 0.95, 0.6 }, kind = "nova",
|
||||
damage = { 12, 16, 20, 26, 32 }, radius = { 6, 6.5, 7, 7.5, 8 }, heal = { 1, 1, 1.5, 1.5, 2 },
|
||||
cooldown = { 4, 3.7, 3.4, 3.1, 2.8 },
|
||||
mastery = { text = "Enemies it burns are dazed: half speed for 2 seconds.", slow = 0.5, seconds = 2 },
|
||||
},
|
||||
-- A fire totem model (display 4589, FireElementalTotem.mdx) that pulses
|
||||
-- as the real Magma Totem does (its pulse spell 8187, whose visual puts an
|
||||
-- Immolate impact on each enemy it burns).
|
||||
totem = {
|
||||
spell = 8190, pulse = 8187, display = 4589,
|
||||
blurb = "Plants a totem that burns every enemy near it every 2 seconds.",
|
||||
color = { 1, 0.5, 0.15 }, kind = "totem", pulseEvery = 2,
|
||||
damage = { 14, 18, 23, 28, 35 }, radius = { 6, 6.5, 7, 7.5, 8 }, totems = { 1, 1, 2, 2, 3 },
|
||||
duration = { 12, 12, 14, 14, 16 }, cooldown = { 14, 13, 12, 11, 10 },
|
||||
mastery = { text = "When a totem's time runs out, it erupts for triple damage.", burst = 3 },
|
||||
},
|
||||
-- Ground areas: the spell's own area model (SetModelBySpellArea) stands
|
||||
-- where it lands and burns every enemy inside it each second. An area
|
||||
-- with `slow` also slows them to that share of their speed.
|
||||
blizzard = {
|
||||
spell = 10, blurb = "Ice storms down on the thickest pack and chills it.",
|
||||
color = { 0.55, 0.8, 1 }, kind = "area", place = "pack", range = 22,
|
||||
damage = { 8, 10, 13, 16, 20 }, radius = { 5, 5.5, 6, 6.5, 7 }, slow = { 0.7, 0.65, 0.6, 0.55, 0.5 },
|
||||
duration = { 4, 4, 5, 5, 6 },
|
||||
cooldown = { 6, 5.5, 5, 4.5, 4 },
|
||||
mastery = { text = "Deep Freeze: when the storm ends, it freezes everything inside for 2 seconds.", freeze = 2 },
|
||||
},
|
||||
rainoffire = {
|
||||
spell = 5740, blurb = "Fire rains on the thickest pack.",
|
||||
color = { 1, 0.45, 0.15 }, kind = "area", place = "pack", range = 22,
|
||||
damage = { 14, 18, 23, 28, 35 }, radius = { 4, 4.4, 4.8, 5.2, 6 }, duration = { 3, 3, 3, 4, 4 },
|
||||
cooldown = { 7, 6.5, 6, 5.5, 5 },
|
||||
mastery = { text = "A second storm falls on another enemy." },
|
||||
},
|
||||
consecration = {
|
||||
spell = 26573, blurb = "Hallows the ground at your feet.",
|
||||
color = { 1, 0.85, 0.4 }, kind = "area", place = "self",
|
||||
damage = { 6, 8, 10, 13, 16 }, radius = { 5, 5.5, 6, 6.5, 7 }, duration = { 6, 6, 7, 7, 8 },
|
||||
cooldown = { 7, 6.5, 6, 5.5, 5 },
|
||||
mastery = { text = "The hallowed ground moves with you." },
|
||||
},
|
||||
thunder = {
|
||||
spell = 6343, blurb = "Shakes the ground close around you.", color = { 1, 0.6, 0.2 }, kind = "nova",
|
||||
damage = { 16, 20, 26, 32, 40 }, radius = { 4, 4.4, 4.8, 5.2, 6 }, cooldown = { 3, 2.8, 2.6, 2.4, 2.2 },
|
||||
mastery = { text = "Aftershock: the clap throws the brood back.", knock = { 8, 5 } },
|
||||
},
|
||||
-- Tanky spells keep the brood off you. `kit` is a SpellVisualKit shown
|
||||
-- while the effect lasts: the shield's bubble, Frost Nova's ice on each
|
||||
-- frozen enemy, Psychic Scream's terror over each fleeing one.
|
||||
shield = {
|
||||
spell = 17, kit = 847, blurb = "A shield soaks up the brood's bites until it breaks.",
|
||||
color = { 1, 1, 0.75 }, kind = "shield",
|
||||
absorb = { 30, 40, 50, 60, 75 }, duration = { 10, 10, 12, 12, 15 }, cooldown = { 15, 14, 13, 12, 11 },
|
||||
mastery = { text = "When the shield breaks, it bursts and hurls the brood around you away.", radius = 6, damage = 30, knock = { 12, 8 } },
|
||||
},
|
||||
thorns = {
|
||||
spell = 467, blurb = "Every enemy biting you takes damage each second.",
|
||||
color = { 0.6, 0.9, 0.3 }, kind = "thorns",
|
||||
damage = { 6, 8, 10, 13, 16 }, duration = { 8, 9, 10, 11, 12 }, cooldown = { 14, 13, 12, 11, 10 },
|
||||
mastery = { text = "You heal 1 health for every biter it burns.", heal = 1 },
|
||||
},
|
||||
frostnova = {
|
||||
spell = 122, kit = 285, blurb = "Freezes every enemy around you in place.",
|
||||
color = { 0.6, 0.85, 1 }, kind = "nova",
|
||||
damage = { 6, 8, 10, 12, 15 }, radius = { 6, 6.5, 7, 7.5, 8 }, root = { 2, 2.5, 3, 3.5, 4 },
|
||||
cooldown = { 10, 9.5, 9, 8.5, 8 },
|
||||
mastery = { text = "Shatter: frozen enemies take double damage.", amplify = 2 },
|
||||
},
|
||||
scream = {
|
||||
spell = 8122, kit = 4031, blurb = "Every enemy around you flees in terror.",
|
||||
color = { 0.75, 0.5, 1 }, kind = "nova",
|
||||
radius = { 6, 6.5, 7, 7.5, 8 }, fear = { 2, 2.5, 3, 3.5, 4 }, cooldown = { 14, 13, 12, 11, 10 },
|
||||
mastery = { text = "Terror: fleeing enemies take 10 damage a second.", damage = 10 },
|
||||
},
|
||||
demoshout = {
|
||||
spell = 1160, blurb = "Enemies around you bite softer for a while.",
|
||||
color = { 1, 0.55, 0.35 }, kind = "nova",
|
||||
radius = { 8, 8.5, 9, 9.5, 10 }, weaken = { 0.3, 0.35, 0.4, 0.45, 0.5 }, duration = { 6, 7, 8, 9, 10 },
|
||||
cooldown = { 10, 9.5, 9, 8.5, 8 },
|
||||
mastery = { text = "Battle cry: you run 20% faster while it lasts.", speed = 1.2 },
|
||||
},
|
||||
stoneclaw = {
|
||||
spell = 5730, display = 4588, blurb = "Plants a totem that the brood around it must attack.",
|
||||
color = { 0.8, 0.65, 0.4 }, kind = "ward",
|
||||
radius = { 6, 7, 8, 9, 10 }, duration = { 5, 6, 7, 8, 9 }, cooldown = { 16, 15, 14, 13, 12 },
|
||||
mastery = { text = "When the totem crumbles, it stuns the brood around it for 2 seconds.", stun = 2 },
|
||||
},
|
||||
-- Fun ones. `knock` is the knockback (out, up), as the items' are.
|
||||
blastwave = {
|
||||
spell = 11113, blurb = "A ring of fire hurls the brood away from you.",
|
||||
color = { 1, 0.5, 0.2 }, kind = "nova", knock = { 10, 7 },
|
||||
damage = { 18, 24, 30, 38, 48 }, radius = { 6, 6.5, 7, 7.5, 8 }, cooldown = { 7, 6.6, 6.2, 5.8, 5.4 },
|
||||
mastery = { text = "The brood it hits is dazed: half speed for 3 seconds.", slow = 0.5, seconds = 3 },
|
||||
},
|
||||
-- The imp (display 4449) stays for the run and casts the real Firebolt.
|
||||
imp = {
|
||||
spell = 688, missile = 3110, display = 4449, blurb = "An imp follows you and hurls Firebolts.",
|
||||
color = { 1, 0.55, 0.2 }, kind = "pet", range = 22,
|
||||
damage = { 12, 15, 19, 24, 30 }, cooldown = { 1.6, 1.5, 1.4, 1.3, 1.2 },
|
||||
mastery = { text = "The imp hurls two Firebolts at once.", bolts = 2 },
|
||||
},
|
||||
}
|
||||
local SPELL_ORDER = {
|
||||
"frostbolt", "shadowbolt", "lightning", "chain", "corruption", "nova", "holynova", "thunder", "totem", "blizzard",
|
||||
"rainoffire", "consecration", "shield", "thorns", "frostnova", "scream", "demoshout", "stoneclaw", "blastwave", "imp",
|
||||
}
|
||||
local MAX_SPELLS = 6
|
||||
local EARLY_SPELLS = 3 -- until you have this many spells, each level offers a new one
|
||||
|
||||
-- `icon` is the spell whose icon the passive shows; `total` is what all its
|
||||
-- ranks give together.
|
||||
local PASSIVES = {
|
||||
might = { name = "Might", icon = 19740, text = "+15% damage", total = "+%d%% damage", per = 15 },
|
||||
haste = { name = "Haste", icon = 12043, text = "-8% spell cooldowns", total = "-%d%% cooldowns", per = 8 },
|
||||
reach = { name = "Reach", icon = 2481, text = "+40% gem pickup radius", total = "+%d%% pickup radius", per = 40 },
|
||||
vigor = { name = "Vigor", icon = 1243, text = "+25 maximum health, and heal 25", total = "+%d maximum health", per = 25 },
|
||||
renewal = { name = "Renewal", icon = 139, text = "+1.2 health per second", total = "%.1f health per second", per = 1.2 },
|
||||
sprint = { icon = 2983, text = "+8% run speed", total = "+%d%% run speed", per = 8 },
|
||||
devotion = { icon = 465, text = "-5% damage taken", total = "-%d%% damage taken", per = 5 },
|
||||
amplify = { icon = 1008, text = "+10% spell radius", total = "+%d%% spell radius", per = 10 },
|
||||
multishot = {
|
||||
icon = 2643, text = "+1 target for bolts and Corruption", total = "+%d targets", per = 1,
|
||||
max = 3,
|
||||
},
|
||||
wisdom = { icon = 19742, text = "+15% experience", total = "+%d%% experience", per = 15 },
|
||||
soulstone = {
|
||||
name = "Soulstone", icon = 20707, text = "Once per run, rise again with half your health",
|
||||
total = "%d revive", per = 1, max = 1,
|
||||
},
|
||||
vampiric = {
|
||||
icon = 15286,
|
||||
text = "Heals you for 0.2% of the damage you deal, at most 2 health a second",
|
||||
total = "up to %d health a second", per = 2,
|
||||
},
|
||||
}
|
||||
-- The names are the client's own, in its language: each spell's from its
|
||||
-- spell, and each passive without a name of its own from its icon's spell.
|
||||
for _, def in pairs(SPELLS) do
|
||||
def.name = C_Spell.GetSpellName(def.spell)
|
||||
end
|
||||
for _, p in pairs(PASSIVES) do
|
||||
p.name = p.name or C_Spell.GetSpellName(p.icon)
|
||||
end
|
||||
|
||||
local HEAL_ICON = 5185 -- Healing Touch
|
||||
local UNKNOWN_ICON = "Interface\\Icons\\INV_Misc_QuestionMark"
|
||||
local PASSIVE_ORDER = {
|
||||
"might", "haste", "reach", "vigor", "renewal", "sprint", "vampiric", "devotion", "amplify", "multishot",
|
||||
"wisdom", "soulstone",
|
||||
}
|
||||
local VAMPIRIC_SHARE = 0.002 -- of the damage dealt, per rank
|
||||
local HOLY_NOVA_HEAL_TARGETS = 8 -- Holy Nova heals for at most this many enemies
|
||||
local SOULSTONE_SPELL = 20707 -- Soulstone Resurrection, the revive's visual
|
||||
local SOULSTONE_BLAST = 10 -- yards: rising again throws back the enemies this close
|
||||
local MAX_LEVEL = 5
|
||||
|
||||
local ROMAN = { "I", "II", "III", "IV", "V" }
|
||||
|
||||
-- The bars ease to a new value in the engine (StatusBar interpolation); a
|
||||
-- reset (a new run, a level's wrap) shows at once.
|
||||
local EASE = Enum.StatusBarInterpolation.ExponentialEaseOut
|
||||
local AT_ONCE = Enum.StatusBarInterpolation.Immediate
|
||||
|
||||
-- The items' tuning.
|
||||
local TUNING = {
|
||||
SWIFTNESS_FACTOR = 1.5,
|
||||
SWIFTNESS_SECONDS = 10,
|
||||
ITEM_CHANCE = 1 / 150, -- a whelp's chance to drop an item
|
||||
ITEM_REACH = 1.5, -- yards: walking this close uses a drop
|
||||
ITEM_SECONDS = 30, -- a drop lies this long
|
||||
ITEM_ICON = 28, -- pixels
|
||||
ITEM_COLOR = { 1, 0.82, 0 },
|
||||
POTION_HEAL = 0.3, -- of the maximum health
|
||||
GRENADE_RADIUS = 5,
|
||||
GRENADE_DAMAGE = 45, -- grows with the run as the brood's health does
|
||||
GRENADE_STUN = 2, -- seconds
|
||||
SAPPER_RADIUS = 10,
|
||||
SAPPER_SELF = 0.15, -- of the maximum health; it never kills you
|
||||
SAPPER_ELITE = 0.25, -- of an elite's maximum health
|
||||
CHICKEN_DISPLAY = 6909, -- the robot chicken (ChickenSkinRobot)
|
||||
CHICKEN_SECONDS = 20,
|
||||
CHICKEN_SPEED = 9,
|
||||
CHICKEN_DAMAGE = 12, -- a peck, grows with the run
|
||||
CHICKEN_PECK = 0.8, -- seconds between pecks
|
||||
CHICKEN_REACH = 1.8,
|
||||
CHICKEN_LEASH = 20, -- yards from the hero: it hunts no farther
|
||||
CHICKEN_HUNT_MIN = 6, -- yards from the hero: it prefers prey past this
|
||||
CHICKEN_TAUNT = 3, -- seconds a peck holds the whelps around it
|
||||
CHICKEN_TAUNT_RADIUS = 3, -- yards around the chicken that a peck taunts
|
||||
CHICKEN_ROAM = 6, -- yards around the hero that it roams with no prey
|
||||
-- Knockbacks (actor:KnockBack): yards a second out and up.
|
||||
GRENADE_KNOCK = { 10, 7 },
|
||||
SOULSTONE_KNOCK = { 16, 10 },
|
||||
ELITE_KNOCK_SHARE = 0.4, -- an elite takes this share of a knockback
|
||||
ELITE_HOLD_SHARE = 0.5, -- and this share of a root's or a fear's time
|
||||
FEAR_STEP = 6, -- yards a fleeing enemy runs before its next MoveTo
|
||||
IMP_SPEED = 8.5,
|
||||
IMP_FOLLOW = 3, -- yards: farther from you than this, the imp follows
|
||||
}
|
||||
-- `itemID` is the real item (its name and icon), `spell` the visual its use plays.
|
||||
local ITEMS = {
|
||||
{ key = "potion", itemID = 929, spell = 17534, weight = 30 },
|
||||
{ key = "grenade", itemID = 15993, spell = 19769, weight = 25 },
|
||||
{ key = "swift", itemID = 2459, spell = 2379, weight = 15 },
|
||||
{ key = "hearth", itemID = 6948, spell = 8690, weight = 10 },
|
||||
{ key = "sapper", itemID = 10646, spell = 13241, weight = 10 },
|
||||
{ key = "chicken", itemID = 10725, spell = 23133, weight = 10 },
|
||||
}
|
||||
-- Each item's name and icon are the client's (the Item mixin), once its data
|
||||
-- has come from the server.
|
||||
for _, item in ipairs(ITEMS) do
|
||||
local data = Item:CreateFromItemID(item.itemID);
|
||||
data:ContinueOnItemLoad(function()
|
||||
item.name = data:GetItemName();
|
||||
item.icon = data:GetItemIcon();
|
||||
end);
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.LAIR_MAP = LAIR_MAP
|
||||
BW.CENTER = CENTER
|
||||
BW.ROOM_RADIUS = ROOM_RADIUS
|
||||
BW.SPAWN_MIN_DISTANCE = SPAWN_MIN_DISTANCE
|
||||
BW.FAR_CLIP = FAR_CLIP
|
||||
BW.CAMERA_YAW = CAMERA_YAW
|
||||
BW.CAMERA_PITCH = CAMERA_PITCH
|
||||
BW.CAMERA_ZOOM = CAMERA_ZOOM
|
||||
BW.RUN_SECONDS = RUN_SECONDS
|
||||
BW.ELITE_EVERY = ELITE_EVERY
|
||||
BW.MAX_ALIVE = MAX_ALIVE
|
||||
BW.RETARGET_SECONDS = RETARGET_SECONDS
|
||||
BW.SURROUND_RADIUS = SURROUND_RADIUS
|
||||
BW.CAST_SPACING = CAST_SPACING
|
||||
BW.DEATH_SECONDS = DEATH_SECONDS
|
||||
BW.ANIM_STAND = ANIM_STAND
|
||||
BW.ANIM_DEATH = ANIM_DEATH
|
||||
BW.HERO_HP = HERO_HP
|
||||
BW.MAGNET = MAGNET
|
||||
BW.GEM_SPEED = GEM_SPEED
|
||||
BW.GEM_TAKE = GEM_TAKE
|
||||
BW.GEM_PATH = GEM_PATH
|
||||
BW.MAX_GEMS = MAX_GEMS
|
||||
BW.KINDS = KINDS
|
||||
BW.ELITE = ELITE
|
||||
BW.SPELLS = SPELLS
|
||||
BW.SPELL_ORDER = SPELL_ORDER
|
||||
BW.MAX_SPELLS = MAX_SPELLS
|
||||
BW.EARLY_SPELLS = EARLY_SPELLS
|
||||
BW.PASSIVES = PASSIVES
|
||||
BW.HEAL_ICON = HEAL_ICON
|
||||
BW.UNKNOWN_ICON = UNKNOWN_ICON
|
||||
BW.PASSIVE_ORDER = PASSIVE_ORDER
|
||||
BW.VAMPIRIC_SHARE = VAMPIRIC_SHARE
|
||||
BW.HOLY_NOVA_HEAL_TARGETS = HOLY_NOVA_HEAL_TARGETS
|
||||
BW.SOULSTONE_SPELL = SOULSTONE_SPELL
|
||||
BW.SOULSTONE_BLAST = SOULSTONE_BLAST
|
||||
BW.MAX_LEVEL = MAX_LEVEL
|
||||
BW.ROMAN = ROMAN
|
||||
BW.EASE = EASE
|
||||
BW.AT_ONCE = AT_ONCE
|
||||
BW.TUNING = TUNING
|
||||
BW.ITEMS = ITEMS
|
||||
@@ -0,0 +1,458 @@
|
||||
-- Broodwatch HUD: the level-up cards, the build bar, health and experience.
|
||||
|
||||
local _, BW = ...
|
||||
local HERO_HP = BW.HERO_HP
|
||||
local SPELLS = BW.SPELLS
|
||||
local SPELL_ORDER = BW.SPELL_ORDER
|
||||
local MAX_SPELLS = BW.MAX_SPELLS
|
||||
local EARLY_SPELLS = BW.EARLY_SPELLS
|
||||
local PASSIVES = BW.PASSIVES
|
||||
local HEAL_ICON = BW.HEAL_ICON
|
||||
local UNKNOWN_ICON = BW.UNKNOWN_ICON
|
||||
local PASSIVE_ORDER = BW.PASSIVE_ORDER
|
||||
local MAX_LEVEL = BW.MAX_LEVEL
|
||||
local ROMAN = BW.ROMAN
|
||||
local EASE = BW.EASE
|
||||
local AT_ONCE = BW.AT_ONCE
|
||||
local offers = BW.offers
|
||||
local Shuffle = BW.Shuffle
|
||||
local Stat = BW.Stat
|
||||
local TUNING = BW.TUNING
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Level-up offers
|
||||
|
||||
local function BuildOffers()
|
||||
local all = {};
|
||||
local owned = 0;
|
||||
for _ in pairs(BW.run.spells) do
|
||||
owned = owned + 1;
|
||||
end
|
||||
for _, key in ipairs(SPELL_ORDER) do
|
||||
local state = BW.run.spells[key];
|
||||
if state and state.level < MAX_LEVEL then
|
||||
tinsert(all, { kind = "spell", key = key, level = state.level + 1 });
|
||||
elseif not state and owned < MAX_SPELLS then
|
||||
tinsert(all, { kind = "spell", key = key, level = 1 });
|
||||
end
|
||||
end
|
||||
for _, key in ipairs(PASSIVE_ORDER) do
|
||||
local level = (BW.run.passives[key] or 0) + 1;
|
||||
if level <= (PASSIVES[key].max or MAX_LEVEL) then
|
||||
tinsert(all, { kind = "passive", key = key, level = level });
|
||||
end
|
||||
end
|
||||
Shuffle(all);
|
||||
-- An early build must not stall: the first level offers only new spells,
|
||||
-- and until you have EARLY_SPELLS spells each level offers at least one.
|
||||
local fresh = {};
|
||||
for _, o in ipairs(all) do
|
||||
if o.kind == "spell" and o.level == 1 then
|
||||
tinsert(fresh, o);
|
||||
end
|
||||
end
|
||||
if owned == 1 and table.getn(fresh) >= 3 then
|
||||
all = fresh;
|
||||
elseif owned < EARLY_SPELLS and table.getn(fresh) > 0 then
|
||||
local hasFresh = false;
|
||||
for i = 1, 3 do
|
||||
local o = all[i];
|
||||
if o and o.kind == "spell" and o.level == 1 then
|
||||
hasFresh = true;
|
||||
end
|
||||
end
|
||||
if not hasFresh then
|
||||
-- The new spell takes the third card's place.
|
||||
for i, o in ipairs(all) do
|
||||
if o == fresh[1] then
|
||||
table.remove(all, i);
|
||||
break;
|
||||
end
|
||||
end
|
||||
tinsert(all, 3, fresh[1]);
|
||||
end
|
||||
end
|
||||
for i = 1, 3 do
|
||||
offers[i] = all[i] or { kind = "heal" };
|
||||
end
|
||||
end
|
||||
|
||||
-- Card colours by kind: a new spell gold, a spell's next rank blue, a
|
||||
-- passive green, the heal when nothing is left red.
|
||||
local CARD_COLORS = {
|
||||
new = { 1, 0.82, 0 },
|
||||
spell = { 0.35, 0.65, 1 },
|
||||
passive = { 0.3, 0.9, 0.4 },
|
||||
heal = { 1, 0.45, 0.45 },
|
||||
}
|
||||
local STAT_LABELS = {
|
||||
damage = "Damage", count = "Targets", jumps = "Jumps", totems = "Totems", spread = "Spreads to",
|
||||
heal = "Heal per hit", radius = "Radius", slow = "Enemy speed", duration = "Lasts", cooldown = "Every",
|
||||
absorb = "Absorbs", root = "Freezes for", fear = "Fears for", weaken = "Their bites",
|
||||
}
|
||||
local STAT_UNITS = { radius = " yd", duration = " s", cooldown = " s", root = " s", fear = " s" }
|
||||
local STAT_ORDER = {
|
||||
"damage", "count", "jumps", "totems", "spread", "heal", "absorb", "radius", "slow", "root", "fear", "weaken",
|
||||
"duration", "cooldown",
|
||||
}
|
||||
|
||||
local function Num(v)
|
||||
if v == math.floor(v) then
|
||||
return string.format("%d", v);
|
||||
end
|
||||
local text = string.format("%.2f", v);
|
||||
if string.sub(text, -1) == "0" then
|
||||
text = string.sub(text, 1, -2);
|
||||
end
|
||||
return text;
|
||||
end
|
||||
|
||||
-- A stat as the cards show it: the slow as a percentage of speed, the rest
|
||||
-- with their unit.
|
||||
local function StatText(field, v)
|
||||
if field == "slow" then
|
||||
return math.floor(v * 100 + 0.5) .. "%";
|
||||
elseif field == "weaken" then
|
||||
return "-" .. math.floor(v * 100 + 0.5) .. "%";
|
||||
elseif field == "absorb" then
|
||||
return Num(v) .. "% of health";
|
||||
end
|
||||
return Num(v) .. (STAT_UNITS[field] or "");
|
||||
end
|
||||
|
||||
local function Icon(spellID)
|
||||
return C_Spell.GetSpellTexture(spellID) or UNKNOWN_ICON;
|
||||
end
|
||||
|
||||
-- The card's lines: a new spell's blurb and stats, a rank's changes (old in
|
||||
-- grey, new in green), a passive's rank and its total.
|
||||
local function OfferLines(o)
|
||||
local lines = {};
|
||||
if o.kind == "spell" then
|
||||
local def = SPELLS[o.key];
|
||||
if o.level == 1 then
|
||||
tinsert(lines, def.blurb);
|
||||
tinsert(lines, "");
|
||||
end
|
||||
for _, field in ipairs(STAT_ORDER) do
|
||||
local b = Stat(def, field, o.level);
|
||||
if b then
|
||||
if o.level == 1 then
|
||||
tinsert(lines, STAT_LABELS[field] .. " " .. StatText(field, b));
|
||||
else
|
||||
local a = Stat(def, field, o.level - 1);
|
||||
if a ~= b then
|
||||
tinsert(lines, string.format("%s |cff888888%s|r > |cff40ff40%s|r",
|
||||
STAT_LABELS[field], StatText(field, a), StatText(field, b)));
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- The mastery: a teaser on a new spell's card, the prize at rank V.
|
||||
if def.mastery then
|
||||
if o.level == MAX_LEVEL then
|
||||
tinsert(lines, "");
|
||||
tinsert(lines, "|cffffd100Mastery:|r " .. def.mastery.text);
|
||||
elseif o.level == 1 then
|
||||
tinsert(lines, "");
|
||||
tinsert(lines, "|cff888888At rank V: " .. def.mastery.text .. "|r");
|
||||
end
|
||||
end
|
||||
elseif o.kind == "passive" then
|
||||
local p = PASSIVES[o.key];
|
||||
tinsert(lines, p.text);
|
||||
tinsert(lines, "");
|
||||
tinsert(lines, "|cff888888Total:|r " .. string.format(p.total, p.per * o.level));
|
||||
else
|
||||
tinsert(lines, "Heal 50 health.");
|
||||
end
|
||||
return table.concat(lines, "\n");
|
||||
end
|
||||
|
||||
-- A card grows to its text: the parts above the description (the icon at
|
||||
-- 20 from the top, 64 tall, the name 10 below it, the tag 4 below that, the
|
||||
-- description 14 below that) and below it (12 above the pips, which sit 40
|
||||
-- from the bottom, 6 tall), as Broodwatch.xml lays them out.
|
||||
local CARD_ABOVE = 20 + 64 + 10 + 4 + 14
|
||||
local CARD_BELOW = 12 + 6 + 40
|
||||
local CARD_MIN_HEIGHT = 280
|
||||
local CARD_DESC_ROOM = 1000 -- the description's height while it is measured
|
||||
|
||||
-- Fills card i with offer o; returns the height the card needs.
|
||||
local function FillCard(i, o)
|
||||
local card = getglobal(BW.cards:GetName() .. "Card" .. i);
|
||||
local name = card:GetName();
|
||||
local icon, title, tag, colorKey;
|
||||
if o.kind == "spell" then
|
||||
local def = SPELLS[o.key];
|
||||
icon, title = Icon(def.spell), def.name;
|
||||
if o.level == 1 then
|
||||
tag, colorKey = "New spell", "new";
|
||||
else
|
||||
tag, colorKey = "Rank " .. ROMAN[o.level], "spell";
|
||||
end
|
||||
elseif o.kind == "passive" then
|
||||
local p = PASSIVES[o.key];
|
||||
icon, title = Icon(p.icon), p.name;
|
||||
tag, colorKey = "Passive, rank " .. ROMAN[o.level], "passive";
|
||||
else
|
||||
icon, title, tag, colorKey = Icon(HEAL_ICON), "Second Wind", "Everything is at rank V", "heal";
|
||||
end
|
||||
local color = CARD_COLORS[colorKey];
|
||||
card.color = color;
|
||||
card:SetBackdropColor(0.04, 0.04, 0.06, 0.95);
|
||||
card:SetBackdropBorderColor(color[1], color[2], color[3]);
|
||||
getglobal(name .. "Icon"):SetTexture(icon);
|
||||
local nameText = getglobal(name .. "Name");
|
||||
nameText:SetText(title);
|
||||
nameText:SetTextColor(color[1], color[2], color[3]);
|
||||
local tagText = getglobal(name .. "Tag");
|
||||
tagText:SetText(tag);
|
||||
local desc = getglobal(name .. "Desc");
|
||||
desc:SetHeight(CARD_DESC_ROOM);
|
||||
desc:SetText(OfferLines(o));
|
||||
local descHeight = desc:GetStringHeight();
|
||||
desc:SetHeight(descHeight);
|
||||
getglobal(name .. "Key"):SetText("Press " .. i);
|
||||
-- The ranks: those you have in the card's colour, the new one white.
|
||||
local maxRank = o.kind == "passive" and PASSIVES[o.key].max or MAX_LEVEL;
|
||||
for r = 1, MAX_LEVEL do
|
||||
local pip = getglobal(name .. "Pip" .. r);
|
||||
if o.kind == "heal" or r > maxRank then
|
||||
pip:Hide();
|
||||
else
|
||||
pip:Show();
|
||||
if r < o.level then
|
||||
pip:SetVertexColor(color[1], color[2], color[3]);
|
||||
elseif r == o.level then
|
||||
pip:SetVertexColor(1, 1, 1);
|
||||
else
|
||||
pip:SetVertexColor(0.2, 0.2, 0.2);
|
||||
end
|
||||
end
|
||||
end
|
||||
return CARD_ABOVE + nameText:GetStringHeight() + tagText:GetStringHeight() + descHeight + CARD_BELOW;
|
||||
end
|
||||
|
||||
function Broodwatch_CardHover(card, over)
|
||||
if over then
|
||||
card:SetBackdropBorderColor(1, 1, 1);
|
||||
elseif card.color then
|
||||
card:SetBackdropBorderColor(card.color[1], card.color[2], card.color[3]);
|
||||
end
|
||||
end
|
||||
|
||||
-- The build bar: an icon for each spell and passive you have, with its rank.
|
||||
local BUILD_SLOT = 32
|
||||
local buildSlots = {}
|
||||
|
||||
local function BuildSlot(i)
|
||||
local slot = buildSlots[i];
|
||||
if not slot then
|
||||
slot = CreateFrame("Frame", nil, BW.build);
|
||||
slot:SetWidth(BUILD_SLOT);
|
||||
slot:SetHeight(BUILD_SLOT);
|
||||
slot.icon = slot:CreateTexture(nil, "ARTWORK");
|
||||
slot.icon:SetAllPoints(slot);
|
||||
-- The spell's recharge sweeps over its icon. No flash at the end:
|
||||
-- the fast spells would flash all the time.
|
||||
slot.cd = CreateFrame("Cooldown", nil, slot);
|
||||
slot.cd:SetAllPoints(slot);
|
||||
slot.cd:SetDrawEdge(true);
|
||||
slot.cd:SetDrawBling(false);
|
||||
slot.cd:SetHideCountdownNumbers(true);
|
||||
slot.cd:SetPaused(BW.paused);
|
||||
-- The rank sits above the sweep.
|
||||
slot.top = CreateFrame("Frame", nil, slot);
|
||||
slot.top:SetAllPoints(slot);
|
||||
slot.top:SetFrameLevel(slot.cd:GetFrameLevel() + 1);
|
||||
slot.rank = slot.top:CreateFontString(nil, "OVERLAY");
|
||||
slot.rank:SetFontObject(NumberFontNormal);
|
||||
slot.rank:SetPoint("BOTTOMRIGHT", slot, "BOTTOMRIGHT", -1, 1);
|
||||
buildSlots[i] = slot;
|
||||
end
|
||||
return slot;
|
||||
end
|
||||
|
||||
local buildCount = 0
|
||||
|
||||
local PASSIVE_SLOT = 24
|
||||
|
||||
local function AddBuildSlot(icon, level, x, y, size)
|
||||
buildCount = buildCount + 1;
|
||||
local slot = BuildSlot(buildCount);
|
||||
slot:SetWidth(size);
|
||||
slot:SetHeight(size);
|
||||
slot:ClearAllPoints();
|
||||
slot:SetPoint("BOTTOMLEFT", BW.build, "BOTTOMLEFT", x, y);
|
||||
slot.icon:SetTexture(icon);
|
||||
slot.icon:SetDesaturated(false);
|
||||
slot.rank:SetText(ROMAN[level]);
|
||||
slot:Show();
|
||||
return slot;
|
||||
end
|
||||
|
||||
-- Shows a spell's recharge on its slot: the part of `full` seconds left in
|
||||
-- `left`, measured on the game's clock (the sweep pauses with the game).
|
||||
local function ShowRecharge(slot, left, full)
|
||||
if left > 0 and full and full > 0 then
|
||||
slot.cd:SetCooldown(GetTime() - (full - left), full);
|
||||
else
|
||||
slot.cd:Clear();
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateBuild()
|
||||
buildCount = 0;
|
||||
local x = 0;
|
||||
for _, key in ipairs(SPELL_ORDER) do
|
||||
local state = BW.run.spells[key];
|
||||
if state then
|
||||
-- A new spell can move the others along the bar: each slot shows
|
||||
-- its own spell's recharge again.
|
||||
state.slot = AddBuildSlot(Icon(SPELLS[key].spell), state.level, x, 0, BUILD_SLOT);
|
||||
ShowRecharge(state.slot, state.cooldown, state.cdFull);
|
||||
x = x + BUILD_SLOT + 4;
|
||||
end
|
||||
end
|
||||
x = 0;
|
||||
for _, key in ipairs(PASSIVE_ORDER) do
|
||||
local level = BW.run.passives[key];
|
||||
if level then
|
||||
local slot = AddBuildSlot(Icon(PASSIVES[key].icon), level, x, BUILD_SLOT + 6, PASSIVE_SLOT);
|
||||
slot.cd:Clear();
|
||||
slot.icon:SetDesaturated(key == "soulstone" and BW.run.soulstoneUsed and true or false);
|
||||
x = x + PASSIVE_SLOT + 3;
|
||||
end
|
||||
end
|
||||
for i = buildCount + 1, table.getn(buildSlots) do
|
||||
buildSlots[i]:Hide();
|
||||
end
|
||||
end
|
||||
|
||||
local function SetPaused(on)
|
||||
BW.paused = on;
|
||||
BW.scene:SetPaused(on);
|
||||
for _, slot in ipairs(buildSlots) do
|
||||
slot.cd:SetPaused(on);
|
||||
end
|
||||
if on and BW.moving then
|
||||
BW.hero:StopMoving();
|
||||
BW.moving = false;
|
||||
end
|
||||
end
|
||||
|
||||
local function ShowCards()
|
||||
BuildOffers();
|
||||
-- The three cards share the tallest one's height.
|
||||
local height = CARD_MIN_HEIGHT;
|
||||
for i = 1, 3 do
|
||||
height = math.max(height, FillCard(i, offers[i]));
|
||||
end
|
||||
for i = 1, 3 do
|
||||
getglobal(BW.cards:GetName() .. "Card" .. i):SetHeight(height);
|
||||
end
|
||||
getglobal(BW.cards:GetName() .. "Title"):SetText("Level " .. (BW.run.level - BW.run.pendingLevels + 1));
|
||||
BW.cards:Show();
|
||||
SetPaused(true);
|
||||
end
|
||||
|
||||
local function ApplyHeroSpeed()
|
||||
local factor = 1 + PASSIVES.sprint.per * (BW.run.passives.sprint or 0) / 100;
|
||||
if BW.run.swiftUntil then
|
||||
factor = factor * TUNING.SWIFTNESS_FACTOR;
|
||||
end
|
||||
if BW.run.shoutUntil then
|
||||
factor = factor * SPELLS.demoshout.mastery.speed;
|
||||
end
|
||||
BW.hero:SetMovementSpeed("run", BW.run.heroRun * factor);
|
||||
end
|
||||
|
||||
local function MaxHp()
|
||||
return HERO_HP + 25 * (BW.run.passives.vigor or 0);
|
||||
end
|
||||
|
||||
function Broodwatch_PickCard(i)
|
||||
if not BW.run or not BW.cards:IsShown() then
|
||||
return;
|
||||
end
|
||||
local o = offers[i];
|
||||
if o.kind == "spell" then
|
||||
local state = BW.run.spells[o.key];
|
||||
if state then
|
||||
state.level = o.level;
|
||||
else
|
||||
BW.run.spells[o.key] = { level = 1, cooldown = 0, pending = 0 };
|
||||
end
|
||||
elseif o.kind == "passive" then
|
||||
BW.run.passives[o.key] = o.level;
|
||||
if o.key == "might" then
|
||||
BW.run.damageScale = 1 + 0.15 * o.level;
|
||||
elseif o.key == "vigor" then
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + 25);
|
||||
elseif o.key == "sprint" then
|
||||
ApplyHeroSpeed();
|
||||
end
|
||||
else
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + 50);
|
||||
end
|
||||
BW.cards:Hide();
|
||||
UpdateBuild();
|
||||
BW.run.pendingLevels = BW.run.pendingLevels - 1;
|
||||
if BW.run.pendingLevels > 0 then
|
||||
ShowCards();
|
||||
else
|
||||
SetPaused(false);
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateHealth(mode)
|
||||
local max = MaxHp();
|
||||
BW.healthBar:SetMinMaxValues(0, max, mode or EASE);
|
||||
BW.healthBar:SetValue(math.max(BW.run.hp, 0), mode or EASE);
|
||||
local text = string.format("%d / %d", math.max(math.ceil(BW.run.hp), 0), max);
|
||||
if BW.run.shield then
|
||||
text = text .. string.format(" (+%d)", math.ceil(BW.run.shield));
|
||||
end
|
||||
BW.healthText:SetText(text);
|
||||
end
|
||||
|
||||
local LEVEL_HEAL = 0.12 -- each level heals this share of the maximum health
|
||||
|
||||
local function XpNeeded(level)
|
||||
return 5 + 3 * level + math.floor(level * level / 8);
|
||||
end
|
||||
|
||||
local function GainXp(xp)
|
||||
BW.run.xp = BW.run.xp + xp;
|
||||
local wrapped = false;
|
||||
while BW.run.xp >= XpNeeded(BW.run.level) do
|
||||
BW.run.xp = BW.run.xp - XpNeeded(BW.run.level);
|
||||
BW.run.level = BW.run.level + 1;
|
||||
BW.run.pendingLevels = BW.run.pendingLevels + 1;
|
||||
wrapped = true;
|
||||
end
|
||||
-- A level starts the bar again from empty at once; a gain within a
|
||||
-- level eases.
|
||||
if wrapped then
|
||||
local levels = BW.run.pendingLevels;
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + MaxHp() * LEVEL_HEAL * levels);
|
||||
UpdateHealth();
|
||||
end
|
||||
local mode = wrapped and AT_ONCE or EASE;
|
||||
BW.xpBar:SetMinMaxValues(0, XpNeeded(BW.run.level), mode);
|
||||
BW.xpBar:SetValue(BW.run.xp, mode);
|
||||
if BW.run.pendingLevels > 0 and not BW.cards:IsShown() then
|
||||
ShowCards();
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.Icon = Icon
|
||||
BW.UpdateBuild = UpdateBuild
|
||||
BW.SetPaused = SetPaused
|
||||
BW.ApplyHeroSpeed = ApplyHeroSpeed
|
||||
BW.MaxHp = MaxHp
|
||||
BW.UpdateHealth = UpdateHealth
|
||||
BW.XpNeeded = XpNeeded
|
||||
BW.GainXp = GainXp
|
||||
@@ -0,0 +1,325 @@
|
||||
-- Broodwatch items: drops and what each one does.
|
||||
|
||||
local _, BW = ...
|
||||
local Blast = BW.Blast
|
||||
local Hold = BW.Hold
|
||||
local ELITE = BW.ELITE
|
||||
local enemies = BW.enemies
|
||||
local gems = BW.gems
|
||||
local FloorAt = BW.FloorAt
|
||||
local Dist2 = BW.Dist2
|
||||
local Kill = BW.Kill
|
||||
local ShowNumber = BW.ShowNumber
|
||||
local Damage = BW.Damage
|
||||
local PackCenter = BW.PackCenter
|
||||
local markerPool = BW.markerPool
|
||||
local ReleaseMarker = BW.ReleaseMarker
|
||||
local UNKNOWN_ICON = BW.UNKNOWN_ICON
|
||||
local TUNING = BW.TUNING
|
||||
local ApplyHeroSpeed = BW.ApplyHeroSpeed
|
||||
local MaxHp = BW.MaxHp
|
||||
local UpdateHealth = BW.UpdateHealth
|
||||
local ITEMS = BW.ITEMS
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Items: a whelp drops one now and then, an elite always. A drop lies on the
|
||||
-- floor (a reticle under it, its icon bobbing over it) and is used when you
|
||||
-- walk over it. The icons are the items' use spells' (C_Spell.GetSpellTexture).
|
||||
|
||||
local ANIM_ATTACK_UNARMED = 16
|
||||
|
||||
local drops = {}
|
||||
local dropPool = {}
|
||||
local chickens = {}
|
||||
local chickenPool = {}
|
||||
|
||||
local function RunGrowth()
|
||||
local m = BW.run.time / 300;
|
||||
return 1 + 2 * m + m * m;
|
||||
end
|
||||
|
||||
local function PickItem()
|
||||
local total = 0;
|
||||
for _, item in ipairs(ITEMS) do
|
||||
total = total + item.weight;
|
||||
end
|
||||
local roll = math.random() * total;
|
||||
for _, item in ipairs(ITEMS) do
|
||||
if roll < item.weight then
|
||||
return item;
|
||||
end
|
||||
roll = roll - item.weight;
|
||||
end
|
||||
return ITEMS[1];
|
||||
end
|
||||
|
||||
BW.DropItem = function(e, x, y, z)
|
||||
if e.kind ~= ELITE and math.random() >= TUNING.ITEM_CHANCE then
|
||||
return;
|
||||
end
|
||||
local drop = table.remove(dropPool);
|
||||
if not drop then
|
||||
drop = { actor = BW.scene:AcquireActor(), icon = BW.feedback:CreateTexture(nil, "ARTWORK") };
|
||||
drop.icon:SetWidth(TUNING.ITEM_ICON);
|
||||
drop.icon:SetHeight(TUNING.ITEM_ICON);
|
||||
end
|
||||
drop.item = PickItem();
|
||||
drop.x, drop.y, drop.z = x, y, z;
|
||||
drop.life = TUNING.ITEM_SECONDS;
|
||||
drop.actor:SetPosition(x, y, z);
|
||||
drop.actor:SetGroundReticle(0.9);
|
||||
drop.actor:Show();
|
||||
drop.icon:SetTexture(drop.item.icon or UNKNOWN_ICON);
|
||||
drop.icon:Show();
|
||||
tinsert(drops, drop);
|
||||
end
|
||||
|
||||
local function ReleaseDrop(i)
|
||||
local drop = drops[i];
|
||||
drop.actor:ClearGroundReticle();
|
||||
drop.actor:Hide();
|
||||
drop.icon:Hide();
|
||||
tinsert(dropPool, drop);
|
||||
table.remove(drops, i);
|
||||
end
|
||||
|
||||
-- A grenade lands on the thickest pack: the reticle marks it while it flies.
|
||||
local function ThrowGrenade(item)
|
||||
local x, y, z, target = PackCenter(24, TUNING.GRENADE_RADIUS);
|
||||
if not target then
|
||||
return;
|
||||
end
|
||||
local marker = table.remove(markerPool) or { actor = BW.scene:AcquireActor() };
|
||||
marker.actor:SetPosition(x, y, FloorAt(x, y, z));
|
||||
marker.actor:SetGroundReticle(TUNING.GRENADE_RADIUS);
|
||||
marker.actor:Show();
|
||||
local damage = TUNING.GRENADE_DAMAGE * RunGrowth();
|
||||
local function Explode(entry)
|
||||
ReleaseMarker(marker);
|
||||
local ex, ey = entry.target.actor:GetPosition();
|
||||
local r2 = TUNING.GRENADE_RADIUS * TUNING.GRENADE_RADIUS;
|
||||
for _, o in ipairs(enemies) do
|
||||
if o.alive then
|
||||
local ox, oy = o.actor:GetPosition();
|
||||
if Dist2(ox, oy, ex, ey) <= r2 then
|
||||
Hold(o, TUNING.GRENADE_STUN);
|
||||
Damage(o, damage, TUNING.ITEM_COLOR);
|
||||
Blast(o, ex, ey, TUNING.GRENADE_KNOCK);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local castID = BW.hero:PlaySpellVisual(item.spell, target.actor, 0);
|
||||
local entry = { target = target, onImpact = Explode };
|
||||
if castID then
|
||||
BW.run.inFlight[castID] = entry;
|
||||
else
|
||||
Explode(entry);
|
||||
end
|
||||
end
|
||||
|
||||
-- The Sapper Charge goes off on you: it clears the brood close by, and hurts.
|
||||
local function Sapper(item, hx, hy)
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
local r2 = TUNING.SAPPER_RADIUS * TUNING.SAPPER_RADIUS;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, hx, hy) <= r2 then
|
||||
if e.kind == ELITE then
|
||||
Damage(e, e.maxHp * TUNING.SAPPER_ELITE / BW.run.damageScale, TUNING.ITEM_COLOR);
|
||||
else
|
||||
Kill(e);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
BW.run.hp = math.max(1, BW.run.hp - MaxHp() * TUNING.SAPPER_SELF);
|
||||
UpdateHealth();
|
||||
end
|
||||
|
||||
local function SummonChicken(item, hx, hy, hz)
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
local chicken = table.remove(chickenPool);
|
||||
if not chicken then
|
||||
chicken = { actor = BW.scene:AcquireActor() };
|
||||
chicken.actor:SetModelByCreatureDisplayID(TUNING.CHICKEN_DISPLAY);
|
||||
end
|
||||
chicken.life = TUNING.CHICKEN_SECONDS;
|
||||
chicken.peck = 0;
|
||||
chicken.move = 0;
|
||||
chicken.prey = nil;
|
||||
chicken.gone = nil;
|
||||
chicken.actor:SetPosition(hx + 1.5, hy, hz + 0.5);
|
||||
chicken.actor:Show();
|
||||
chicken.actor:SetMovementEnabled(true);
|
||||
chicken.actor:SetMovementSpeed("run", TUNING.CHICKEN_SPEED);
|
||||
tinsert(chickens, chicken);
|
||||
end
|
||||
|
||||
-- The chicken's prey: the whelp nearest to it among those at the edge of
|
||||
-- the pack (past CHICKEN_HUNT_MIN from the hero, within the leash), else the
|
||||
-- nearest within the leash. It keeps its prey until the prey dies or strays.
|
||||
local function ChickenPrey(cx, cy, hx, hy)
|
||||
local leash2 = TUNING.CHICKEN_LEASH * TUNING.CHICKEN_LEASH;
|
||||
local min2 = TUNING.CHICKEN_HUNT_MIN * TUNING.CHICKEN_HUNT_MIN;
|
||||
local edge, edgeD2, any, anyD2;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
local fromHero = Dist2(x, y, hx, hy);
|
||||
if fromHero <= leash2 then
|
||||
local d2 = Dist2(x, y, cx, cy);
|
||||
if fromHero >= min2 and (not edge or d2 < edgeD2) then
|
||||
edge, edgeD2 = e, d2;
|
||||
end
|
||||
if not any or d2 < anyD2 then
|
||||
any, anyD2 = e, d2;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return edge or any;
|
||||
end
|
||||
|
||||
local function UpdateChickens(dt, hx, hy, hz)
|
||||
local leash2 = TUNING.CHICKEN_LEASH * TUNING.CHICKEN_LEASH;
|
||||
local reach2 = TUNING.CHICKEN_REACH * TUNING.CHICKEN_REACH;
|
||||
local taunt2 = TUNING.CHICKEN_TAUNT_RADIUS * TUNING.CHICKEN_TAUNT_RADIUS;
|
||||
for i = table.getn(chickens), 1, -1 do
|
||||
local chicken = chickens[i];
|
||||
chicken.life = chicken.life - dt;
|
||||
if chicken.life <= 0 then
|
||||
chicken.gone = true;
|
||||
chicken.prey = nil;
|
||||
chicken.actor:SetMovementEnabled(false);
|
||||
chicken.actor:Hide();
|
||||
tinsert(chickenPool, chicken);
|
||||
table.remove(chickens, i);
|
||||
else
|
||||
local cx, cy, cz = chicken.actor:GetPosition();
|
||||
chicken.peck = chicken.peck - dt;
|
||||
chicken.move = chicken.move - dt;
|
||||
local prey = chicken.prey;
|
||||
if prey then
|
||||
local px, py = prey.actor:GetPosition();
|
||||
if not prey.alive or Dist2(px, py, hx, hy) > leash2 then
|
||||
prey = nil;
|
||||
end
|
||||
end
|
||||
if not prey then
|
||||
prey = ChickenPrey(cx, cy, hx, hy);
|
||||
chicken.prey = prey;
|
||||
end
|
||||
if prey then
|
||||
local tx, ty, tz = prey.actor:GetPosition();
|
||||
if Dist2(tx, ty, cx, cy) <= reach2 then
|
||||
if chicken.peck <= 0 then
|
||||
chicken.peck = TUNING.CHICKEN_PECK;
|
||||
chicken.actor:SetYaw(math.atan2(ty - cy, tx - cx));
|
||||
chicken.actor:SetAnimation(ANIM_ATTACK_UNARMED);
|
||||
Damage(prey, TUNING.CHICKEN_DAMAGE * RunGrowth(), TUNING.ITEM_COLOR);
|
||||
-- The peck taunts the whelps around the chicken: they
|
||||
-- chase it, and leave the hero be (Chase, Step).
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, cx, cy) <= taunt2 then
|
||||
e.taunter = chicken;
|
||||
e.tauntUntil = BW.run.time + TUNING.CHICKEN_TAUNT;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif chicken.move <= 0 then
|
||||
chicken.move = 0.25;
|
||||
chicken.actor:MoveTo(tx, ty, tz);
|
||||
end
|
||||
elseif chicken.move <= 0 then
|
||||
-- Nothing to hunt: it struts about the hero.
|
||||
chicken.move = 1.5 + math.random();
|
||||
local angle = math.random() * 2 * math.pi;
|
||||
local r = TUNING.CHICKEN_ROAM * (0.4 + 0.6 * math.random());
|
||||
chicken.actor:MoveTo(hx + r * math.cos(angle), hy + r * math.sin(angle), hz);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function UseItem(item, hx, hy, hz)
|
||||
BW.heroNumber.actor = BW.hero;
|
||||
ShowNumber(BW.heroNumber, 0, TUNING.ITEM_COLOR, item.name);
|
||||
if item.key == "potion" then
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
local heal = math.floor(MaxHp() * TUNING.POTION_HEAL + 0.5);
|
||||
BW.run.hp = math.min(MaxHp(), BW.run.hp + heal);
|
||||
UpdateHealth();
|
||||
elseif item.key == "grenade" then
|
||||
ThrowGrenade(item);
|
||||
elseif item.key == "swift" then
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
BW.run.swiftUntil = BW.run.time + TUNING.SWIFTNESS_SECONDS;
|
||||
ApplyHeroSpeed();
|
||||
elseif item.key == "hearth" then
|
||||
BW.hero:PlaySpellVisual(item.spell, BW.hero, 0);
|
||||
for _, gem in ipairs(gems) do
|
||||
gem.pulled = true;
|
||||
end
|
||||
elseif item.key == "sapper" then
|
||||
Sapper(item, hx, hy);
|
||||
elseif item.key == "chicken" then
|
||||
SummonChicken(item, hx, hy, hz);
|
||||
end
|
||||
end
|
||||
|
||||
-- Whether a Battle Chicken holds this whelp: taunted, and the chicken out.
|
||||
local function Taunted(e)
|
||||
return e.taunter and not e.taunter.gone and BW.run.time < e.tauntUntil;
|
||||
end
|
||||
|
||||
local function UpdateItems(dt, hx, hy, hz)
|
||||
if BW.run.swiftUntil and BW.run.time >= BW.run.swiftUntil then
|
||||
BW.run.swiftUntil = nil;
|
||||
ApplyHeroSpeed();
|
||||
end
|
||||
local reach2 = TUNING.ITEM_REACH * TUNING.ITEM_REACH;
|
||||
local scale = BW.scene:GetEffectiveScale();
|
||||
local bob = math.sin(BW.run.time * 4) * 4;
|
||||
for i = table.getn(drops), 1, -1 do
|
||||
local drop = drops[i];
|
||||
drop.life = drop.life - dt;
|
||||
if Dist2(drop.x, drop.y, hx, hy) <= reach2 then
|
||||
local item = drop.item;
|
||||
ReleaseDrop(i);
|
||||
UseItem(item, hx, hy, hz);
|
||||
elseif drop.life <= 0 then
|
||||
ReleaseDrop(i);
|
||||
else
|
||||
local sx, sy = BW.scene:Project3DPointTo2D(drop.x, drop.y, drop.z + 1.2);
|
||||
if sx then
|
||||
drop.icon:ClearAllPoints();
|
||||
drop.icon:SetPoint("BOTTOM", BW.scene, "BOTTOMLEFT", sx / scale, sy / scale + bob);
|
||||
end
|
||||
end
|
||||
end
|
||||
UpdateChickens(dt, hx, hy, hz);
|
||||
end
|
||||
|
||||
local function ClearItems()
|
||||
for i = table.getn(drops), 1, -1 do
|
||||
ReleaseDrop(i);
|
||||
end
|
||||
for i = table.getn(chickens), 1, -1 do
|
||||
chickens[i].gone = true;
|
||||
chickens[i].prey = nil;
|
||||
chickens[i].actor:SetMovementEnabled(false);
|
||||
chickens[i].actor:Hide();
|
||||
tinsert(chickenPool, chickens[i]);
|
||||
chickens[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.Taunted = Taunted
|
||||
BW.UpdateItems = UpdateItems
|
||||
BW.ClearItems = ClearItems
|
||||
@@ -0,0 +1,814 @@
|
||||
-- Broodwatch spells: targets, totems, ground areas, casts and leaps.
|
||||
|
||||
local _, BW = ...
|
||||
local CAST_SPACING = BW.CAST_SPACING
|
||||
local SPELLS = BW.SPELLS
|
||||
local PASSIVES = BW.PASSIVES
|
||||
local HOLY_NOVA_HEAL_TARGETS = BW.HOLY_NOVA_HEAL_TARGETS
|
||||
local enemies = BW.enemies
|
||||
local FloorAt = BW.FloorAt
|
||||
local Dist2 = BW.Dist2
|
||||
local Flash = BW.Flash
|
||||
local Damage = BW.Damage
|
||||
local Blast = BW.Blast
|
||||
local Hold = BW.Hold
|
||||
local Fear = BW.Fear
|
||||
local Weaken = BW.Weaken
|
||||
local TUNING = BW.TUNING
|
||||
local Mastered = BW.Mastered
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The hero's spells
|
||||
|
||||
local function Stat(def, field, level)
|
||||
local v = def[field];
|
||||
v = v and v[level];
|
||||
if v and BW.run then
|
||||
if field == "radius" and BW.run.passives.amplify then
|
||||
v = v * (1 + PASSIVES.amplify.per * BW.run.passives.amplify / 100);
|
||||
elseif field == "count" and BW.run.passives.multishot then
|
||||
v = v + BW.run.passives.multishot;
|
||||
end
|
||||
end
|
||||
return v;
|
||||
end
|
||||
|
||||
-- Missile targets: up to `count` live enemies in range, by the spell's rule.
|
||||
local picked = {}
|
||||
local function PickTargets(def, count, hx, hy)
|
||||
for i = table.getn(picked), 1, -1 do
|
||||
picked[i] = nil;
|
||||
end
|
||||
local range2 = def.range * def.range;
|
||||
for _ = 1, count do
|
||||
local best, bestScore;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive and not e.marked and not (def.kind == "dot" and e.dotLeft) then
|
||||
local x, y = e.actor:GetPosition();
|
||||
local d2 = Dist2(x, y, hx, hy);
|
||||
if d2 <= range2 then
|
||||
local score;
|
||||
if def.pick == "nearest" then
|
||||
score = -d2;
|
||||
elseif def.pick == "strongest" then
|
||||
score = e.hp;
|
||||
else
|
||||
score = math.random();
|
||||
end
|
||||
if not bestScore or score > bestScore then
|
||||
best, bestScore = e, score;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not best then
|
||||
break;
|
||||
end
|
||||
best.marked = true;
|
||||
tinsert(picked, best);
|
||||
end
|
||||
for _, e in ipairs(picked) do
|
||||
e.marked = nil;
|
||||
end
|
||||
return picked;
|
||||
end
|
||||
|
||||
-- The live enemy nearest to (x, y) within `range` that `skip` allows.
|
||||
local function Nearest(x, y, range, skip)
|
||||
local best, bestD2 = nil, range * range;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive and not skip(e) then
|
||||
local ex, ey = e.actor:GetPosition();
|
||||
local d2 = Dist2(ex, ey, x, y);
|
||||
if d2 <= bestD2 then
|
||||
best, bestD2 = e, d2;
|
||||
end
|
||||
end
|
||||
end
|
||||
return best;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Magma Totems: each stands where it was planted and pulses until its time
|
||||
-- runs out. A pulse burns every enemy in its radius and shows the impact on
|
||||
-- up to MAGMA_IMPACTS of them.
|
||||
|
||||
local totems = {}
|
||||
local totemPool = {}
|
||||
local MAGMA_IMPACTS = 6
|
||||
|
||||
local function PlantTotems(def, level)
|
||||
local hx, hy, hz = BW.hero:GetPosition();
|
||||
local n = Stat(def, "totems", level);
|
||||
for k = 1, n do
|
||||
local totem = table.remove(totemPool);
|
||||
if not totem then
|
||||
totem = { actor = BW.scene:AcquireActor() };
|
||||
totem.actor:SetModelByCreatureDisplayID(def.display);
|
||||
totem.actor:HookScript("OnSpellVisualImpact", function(_, _, _, castID) BW.OnImpact(nil, nil, nil, castID); end);
|
||||
end
|
||||
local a = (k - 1) * 2 * math.pi / n + math.random() * 0.5;
|
||||
local x, y = hx + 2 * math.cos(a), hy + 2 * math.sin(a);
|
||||
totem.x, totem.y = x, y;
|
||||
totem.life = Stat(def, "duration", level);
|
||||
totem.damage = Stat(def, "damage", level);
|
||||
totem.radius = Stat(def, "radius", level);
|
||||
totem.pulseGap = 0.5;
|
||||
totem.actor:SetPosition(x, y, FloorAt(x, y, hz));
|
||||
totem.actor:Show();
|
||||
tinsert(totems, totem);
|
||||
end
|
||||
end
|
||||
|
||||
local function UpdateTotems(dt)
|
||||
local def = SPELLS.totem;
|
||||
for i = table.getn(totems), 1, -1 do
|
||||
local totem = totems[i];
|
||||
totem.life = totem.life - dt;
|
||||
if totem.life <= 0 then
|
||||
if Mastered("totem") then
|
||||
-- The eruption: every enemy in reach, at the mastery's multiple.
|
||||
local r2 = totem.radius * totem.radius;
|
||||
local shown = 0;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, totem.x, totem.y) <= r2 then
|
||||
if shown < MAGMA_IMPACTS then
|
||||
shown = shown + 1;
|
||||
totem.actor:PlaySpellVisual(def.pulse, e.actor, 0);
|
||||
end
|
||||
Damage(e, totem.damage * def.mastery.burst, def.color);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
totem.actor:Hide();
|
||||
tinsert(totemPool, totem);
|
||||
table.remove(totems, i);
|
||||
else
|
||||
totem.pulseGap = totem.pulseGap - dt;
|
||||
if totem.pulseGap <= 0 then
|
||||
totem.pulseGap = totem.pulseGap + def.pulseEvery;
|
||||
local r2 = totem.radius * totem.radius;
|
||||
local shown = 0;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, totem.x, totem.y) <= r2 then
|
||||
if shown < MAGMA_IMPACTS then
|
||||
shown = shown + 1;
|
||||
totem.actor:PlaySpellVisual(def.pulse, e.actor, 0);
|
||||
end
|
||||
Damage(e, totem.damage, def.color);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearTotems()
|
||||
for i = table.getn(totems), 1, -1 do
|
||||
totems[i].actor:Hide();
|
||||
tinsert(totemPool, totems[i]);
|
||||
totems[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Ground areas: each stands where it landed for its duration and burns every
|
||||
-- enemy inside it once a second.
|
||||
|
||||
local areas = {}
|
||||
local areaPool = {}
|
||||
|
||||
-- Slows an enemy to `factor` of its run speed until a little after the next
|
||||
-- tick; the step lets it go when that time passes.
|
||||
local SLOW_SECONDS = 1.2
|
||||
local function Slow(e, factor, seconds)
|
||||
e.slowUntil = math.max(e.slowUntil or 0, BW.run.time + (seconds or SLOW_SECONDS));
|
||||
if e.slowFactor ~= factor then
|
||||
e.slowFactor = factor;
|
||||
e.actor:SetMovementSpeed("run", e.kind.speed * factor);
|
||||
end
|
||||
end
|
||||
|
||||
local function Unslow(e)
|
||||
e.slowUntil = nil;
|
||||
e.slowFactor = nil;
|
||||
e.actor:SetMovementSpeed("run", e.kind.speed);
|
||||
end
|
||||
|
||||
-- The point of the thickest pack in range: of up to 24 enemies in range,
|
||||
-- the one with the most others within `radius` of it.
|
||||
local function PackCenter(range, radius)
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local range2, r2 = range * range, radius * radius;
|
||||
local best, bestCount = nil, -1;
|
||||
local tried = 0;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, hx, hy) <= range2 then
|
||||
local count = 0;
|
||||
for _, o in ipairs(enemies) do
|
||||
if o.alive then
|
||||
local ox, oy = o.actor:GetPosition();
|
||||
if Dist2(ox, oy, x, y) <= r2 then
|
||||
count = count + 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
if count > bestCount then
|
||||
best, bestCount = e, count;
|
||||
end
|
||||
tried = tried + 1;
|
||||
if tried >= 24 then
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not best then
|
||||
return nil;
|
||||
end
|
||||
local x, y, z = best.actor:GetPosition();
|
||||
return x, y, z, best;
|
||||
end
|
||||
|
||||
local function PlantArea(key, level, x, y, z)
|
||||
local def = SPELLS[key];
|
||||
local area = table.remove(areaPool);
|
||||
if not area then
|
||||
area = { actor = BW.scene:AcquireActor() };
|
||||
end
|
||||
area.key = key;
|
||||
area.x, area.y = x, y;
|
||||
area.radius = Stat(def, "radius", level);
|
||||
area.damage = Stat(def, "damage", level);
|
||||
area.life = Stat(def, "duration", level);
|
||||
area.slow = Stat(def, "slow", level);
|
||||
area.tick = 1;
|
||||
area.actor:SetModelBySpellArea(def.spell, area.radius);
|
||||
area.actor:SetPosition(x, y, FloorAt(x, y, z));
|
||||
area.actor:Show();
|
||||
tinsert(areas, area);
|
||||
end
|
||||
|
||||
local function UpdateAreas(dt)
|
||||
local follow = Mastered("consecration");
|
||||
for i = table.getn(areas), 1, -1 do
|
||||
local area = areas[i];
|
||||
if follow and area.key == "consecration" then
|
||||
local hx, hy, hz = BW.hero:GetPosition();
|
||||
area.x, area.y = hx, hy;
|
||||
area.actor:SetPosition(hx, hy, hz);
|
||||
end
|
||||
area.life = area.life - dt;
|
||||
area.tick = area.tick - dt;
|
||||
if area.tick <= 0 then
|
||||
area.tick = area.tick + 1;
|
||||
local r2 = area.radius * area.radius;
|
||||
local color = SPELLS[area.key].color;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, area.x, area.y) <= r2 then
|
||||
if area.slow then
|
||||
Slow(e, area.slow);
|
||||
end
|
||||
Damage(e, area.damage, color);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if area.life <= 0 then
|
||||
if area.key == "blizzard" and Mastered("blizzard") then
|
||||
local def = SPELLS.blizzard;
|
||||
local r2 = area.radius * area.radius;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, area.x, area.y) <= r2 then
|
||||
Hold(e, def.mastery.freeze, SPELLS.frostnova.kit, true);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
area.actor:Hide();
|
||||
tinsert(areaPool, area);
|
||||
table.remove(areas, i);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- A storm aimed at a pack marks the ground with the reticle first
|
||||
-- (actor:SetGroundReticle on an actor with no model), then lands.
|
||||
local LANDING_SECONDS = 0.6
|
||||
local markerPool = {}
|
||||
|
||||
local function QueueLanding(key, level, x, y, z)
|
||||
local marker = table.remove(markerPool);
|
||||
if not marker then
|
||||
marker = { actor = BW.scene:AcquireActor() };
|
||||
end
|
||||
local floorZ = FloorAt(x, y, z);
|
||||
marker.actor:SetPosition(x, y, floorZ);
|
||||
marker.actor:SetGroundReticle(Stat(SPELLS[key], "radius", level));
|
||||
marker.actor:Show();
|
||||
tinsert(BW.run.landings, { marker = marker, key = key, level = level, x = x, y = y, z = floorZ, left = LANDING_SECONDS });
|
||||
end
|
||||
|
||||
local function ReleaseMarker(marker)
|
||||
marker.actor:ClearGroundReticle();
|
||||
marker.actor:Hide();
|
||||
tinsert(markerPool, marker);
|
||||
end
|
||||
|
||||
local function UpdateLandings(dt)
|
||||
for i = table.getn(BW.run.landings), 1, -1 do
|
||||
local landing = BW.run.landings[i];
|
||||
landing.left = landing.left - dt;
|
||||
if landing.left <= 0 then
|
||||
ReleaseMarker(landing.marker);
|
||||
PlantArea(landing.key, landing.level, landing.x, landing.y, landing.z);
|
||||
table.remove(BW.run.landings, i);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearLandings()
|
||||
for i = table.getn(BW.run.landings), 1, -1 do
|
||||
ReleaseMarker(BW.run.landings[i].marker);
|
||||
BW.run.landings[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearAreas()
|
||||
for i = table.getn(areas), 1, -1 do
|
||||
areas[i].actor:Hide();
|
||||
tinsert(areaPool, areas[i]);
|
||||
areas[i] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Stoneclaw Totems: each pulls the brood around it onto itself (the Battle
|
||||
-- Chicken's taunt, BW.Taunted) until it crumbles.
|
||||
|
||||
local wards = {}
|
||||
local wardPool = {}
|
||||
local WARD_PULSE = 0.5 -- seconds between two taunts
|
||||
|
||||
local function PlantWard(def, level)
|
||||
local hx, hy, hz = BW.hero:GetPosition();
|
||||
local ward = table.remove(wardPool);
|
||||
if not ward then
|
||||
ward = { actor = BW.scene:AcquireActor() };
|
||||
ward.actor:SetModelByCreatureDisplayID(def.display);
|
||||
end
|
||||
local a = math.random() * 2 * math.pi;
|
||||
local x, y = hx + 2.5 * math.cos(a), hy + 2.5 * math.sin(a);
|
||||
ward.x, ward.y = x, y;
|
||||
ward.life = Stat(def, "duration", level);
|
||||
ward.radius = Stat(def, "radius", level);
|
||||
ward.pulse = 0;
|
||||
ward.gone = nil;
|
||||
ward.actor:SetPosition(x, y, FloorAt(x, y, hz));
|
||||
ward.actor:Show();
|
||||
tinsert(wards, ward);
|
||||
end
|
||||
|
||||
-- `crumbled`: its time ran out (the mastery's stun), not a run's end.
|
||||
local function DropWard(i, crumbled)
|
||||
local ward = wards[i];
|
||||
if crumbled and Mastered("stoneclaw") then
|
||||
local r2 = ward.radius * ward.radius;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, ward.x, ward.y) <= r2 then
|
||||
Hold(e, SPELLS.stoneclaw.mastery.stun);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
ward.gone = true;
|
||||
ward.actor:Hide();
|
||||
tinsert(wardPool, ward);
|
||||
table.remove(wards, i);
|
||||
end
|
||||
|
||||
local function UpdateWards(dt)
|
||||
for i = table.getn(wards), 1, -1 do
|
||||
local ward = wards[i];
|
||||
ward.life = ward.life - dt;
|
||||
if ward.life <= 0 then
|
||||
DropWard(i, true);
|
||||
else
|
||||
ward.pulse = ward.pulse - dt;
|
||||
if ward.pulse <= 0 then
|
||||
ward.pulse = ward.pulse + WARD_PULSE;
|
||||
local r2 = ward.radius * ward.radius;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, ward.x, ward.y) <= r2 then
|
||||
e.taunter = ward;
|
||||
e.tauntUntil = BW.run.time + WARD_PULSE * 2;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearWards()
|
||||
for i = table.getn(wards), 1, -1 do
|
||||
DropWard(i);
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- The imp: summoned the first time Summon Imp casts, it stays for the run,
|
||||
-- follows you and casts Firebolt (the spell's `missile`) at the nearest enemy.
|
||||
|
||||
local pet
|
||||
|
||||
local function SummonPet(def)
|
||||
if pet and pet.out then
|
||||
return pet;
|
||||
end
|
||||
if not pet then
|
||||
pet = { actor = BW.scene:AcquireActor() };
|
||||
pet.actor:SetModelByCreatureDisplayID(def.display);
|
||||
pet.actor:HookScript("OnSpellVisualImpact", function(_, _, _, castID) BW.OnImpact(nil, nil, nil, castID); end);
|
||||
end
|
||||
local hx, hy, hz = BW.hero:GetPosition();
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
pet.actor:SetPosition(hx + 1.5, hy + 1.5, hz + 0.5);
|
||||
pet.actor:Show();
|
||||
pet.actor:SetMovementEnabled(true);
|
||||
pet.actor:SetMovementSpeed("run", TUNING.IMP_SPEED);
|
||||
pet.out = true;
|
||||
pet.move = 0;
|
||||
return pet;
|
||||
end
|
||||
|
||||
local function UpdatePet(dt, hx, hy, hz)
|
||||
if not (pet and pet.out) then
|
||||
return;
|
||||
end
|
||||
pet.move = pet.move - dt;
|
||||
if pet.move > 0 then
|
||||
return;
|
||||
end
|
||||
pet.move = 0.4;
|
||||
local px, py = pet.actor:GetPosition();
|
||||
if Dist2(px, py, hx, hy) > TUNING.IMP_FOLLOW * TUNING.IMP_FOLLOW then
|
||||
-- To your side and behind you, as a pet follows.
|
||||
local a = BW.run.heroYaw + 2.4;
|
||||
pet.actor:MoveTo(hx + 2 * math.cos(a), hy + 2 * math.sin(a), hz);
|
||||
end
|
||||
end
|
||||
|
||||
local function ClearPet()
|
||||
if pet and pet.out then
|
||||
pet.out = nil;
|
||||
pet.actor:SetMovementEnabled(false);
|
||||
pet.actor:Hide();
|
||||
end
|
||||
end
|
||||
|
||||
local petHit = {}
|
||||
local function NotPetHit(e)
|
||||
return petHit[e];
|
||||
end
|
||||
|
||||
local function PetCast(key, def, level)
|
||||
local imp = SummonPet(def);
|
||||
local px, py = imp.actor:GetPosition();
|
||||
local bolts = Mastered(key) and def.mastery.bolts or 1;
|
||||
local cast = false;
|
||||
for _ = 1, bolts do
|
||||
local target = Nearest(px, py, def.range, NotPetHit);
|
||||
if not target then
|
||||
break;
|
||||
end
|
||||
petHit[target] = true;
|
||||
local castID = imp.actor:PlaySpellVisual(def.missile, target.actor, 0);
|
||||
if castID then
|
||||
BW.run.inFlight[castID] = { key = key, target = target, damage = Stat(def, "damage", level) };
|
||||
end
|
||||
cast = true;
|
||||
end
|
||||
for e in pairs(petHit) do
|
||||
petHit[e] = nil;
|
||||
end
|
||||
return cast;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Power Word: Shield: the bites soak into it first (the step), and its bubble
|
||||
-- shows until it breaks or its time ends.
|
||||
|
||||
-- `broken`: the bites used it up (the mastery's burst), not its time.
|
||||
local function EndShield(broken)
|
||||
if broken and Mastered("shield") then
|
||||
local m = SPELLS.shield.mastery;
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local r2 = m.radius * m.radius;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, hx, hy) <= r2 then
|
||||
Damage(e, m.damage, SPELLS.shield.color);
|
||||
Blast(e, hx, hy, m.knock);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if BW.run.shield then
|
||||
BW.run.shield = nil;
|
||||
BW.run.shieldUntil = nil;
|
||||
BW.hero:SetSpellVisualKit();
|
||||
BW.UpdateHealth();
|
||||
end
|
||||
end
|
||||
|
||||
-- The hero casts one visual at a time; casts wait in a queue.
|
||||
local function QueueCast(entry)
|
||||
tinsert(BW.run.castQueue, entry);
|
||||
end
|
||||
|
||||
local function Fire(key, state)
|
||||
local def = SPELLS[key];
|
||||
local level = state.level;
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
if def.kind == "missile" or def.kind == "dot" or def.kind == "chain" then
|
||||
local targets = PickTargets(def, Stat(def, "count", level) or 1, hx, hy);
|
||||
if table.getn(targets) == 0 then
|
||||
return false;
|
||||
end
|
||||
for _, e in ipairs(targets) do
|
||||
QueueCast({
|
||||
key = key, state = state, target = e, damage = Stat(def, "damage", level),
|
||||
jumps = Stat(def, "jumps", level), level = level,
|
||||
});
|
||||
state.pending = state.pending + 1;
|
||||
end
|
||||
elseif def.kind == "pet" then
|
||||
return PetCast(key, def, level);
|
||||
elseif def.kind == "totem" or def.kind == "ward" or def.kind == "shield" or def.kind == "thorns" then
|
||||
QueueCast({ key = key, state = state, level = level });
|
||||
state.pending = state.pending + 1;
|
||||
elseif def.kind == "area" then
|
||||
local x, y, z;
|
||||
if def.place == "pack" then
|
||||
x, y, z = PackCenter(def.range, Stat(def, "radius", level));
|
||||
if not x then
|
||||
return false;
|
||||
end
|
||||
else
|
||||
x, y, z = BW.hero:GetPosition();
|
||||
end
|
||||
QueueCast({ key = key, state = state, level = level, x = x, y = y, z = z });
|
||||
state.pending = state.pending + 1;
|
||||
else
|
||||
QueueCast({
|
||||
key = key, state = state, level = level, damage = Stat(def, "damage", level),
|
||||
radius = Stat(def, "radius", level), heal = Stat(def, "heal", level),
|
||||
});
|
||||
state.pending = state.pending + 1;
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
local function CastNext()
|
||||
local entry = table.remove(BW.run.castQueue, 1);
|
||||
if not entry then
|
||||
return;
|
||||
end
|
||||
entry.state.pending = entry.state.pending - 1;
|
||||
local def = SPELLS[entry.key];
|
||||
if entry.target then
|
||||
if not entry.target.alive then
|
||||
return CastNext();
|
||||
end
|
||||
local castID = BW.hero:PlaySpellVisual(def.spell, entry.target.actor, 0);
|
||||
if castID then
|
||||
BW.run.inFlight[castID] = entry;
|
||||
end
|
||||
elseif def.kind == "totem" then
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
PlantTotems(def, entry.level);
|
||||
elseif def.kind == "ward" then
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
PlantWard(def, entry.level);
|
||||
elseif def.kind == "shield" then
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
BW.run.shield = BW.MaxHp() * Stat(def, "absorb", entry.level) / 100;
|
||||
BW.run.shieldUntil = BW.run.time + Stat(def, "duration", entry.level);
|
||||
BW.hero:SetSpellVisualKit(def.kit);
|
||||
BW.UpdateHealth();
|
||||
elseif def.kind == "thorns" then
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
BW.run.thornsUntil = BW.run.time + Stat(def, "duration", entry.level);
|
||||
BW.run.thornsDamage = Stat(def, "damage", entry.level);
|
||||
BW.run.thornsTick = 1;
|
||||
elseif def.kind == "area" then
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
if def.place == "pack" then
|
||||
QueueLanding(entry.key, entry.level, entry.x, entry.y, entry.z);
|
||||
if entry.key == "rainoffire" and Mastered("rainoffire") then
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local other = PickTargets(def, 1, hx, hy)[1];
|
||||
if other then
|
||||
local x, y, z = other.actor:GetPosition();
|
||||
QueueLanding(entry.key, entry.level, x, y, z);
|
||||
end
|
||||
end
|
||||
else
|
||||
PlantArea(entry.key, entry.level, entry.x, entry.y, entry.z);
|
||||
end
|
||||
else
|
||||
BW.hero:PlaySpellVisual(def.spell, BW.hero, 0);
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local r2 = entry.radius * entry.radius;
|
||||
local root = Stat(def, "root", entry.level);
|
||||
local fear = Stat(def, "fear", entry.level);
|
||||
local weaken = Stat(def, "weaken", entry.level);
|
||||
local mastery = Mastered(entry.key) and def.mastery;
|
||||
local hits = 0;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, hx, hy) <= r2 then
|
||||
if entry.damage then
|
||||
Damage(e, entry.damage, def.color);
|
||||
end
|
||||
if e.alive then
|
||||
if root then
|
||||
Hold(e, root, def.kit);
|
||||
end
|
||||
if fear then
|
||||
Fear(e, fear, def.kit);
|
||||
end
|
||||
if weaken then
|
||||
Weaken(e, weaken, Stat(def, "duration", entry.level));
|
||||
end
|
||||
end
|
||||
if def.knock then
|
||||
Blast(e, hx, hy, def.knock);
|
||||
end
|
||||
if mastery then
|
||||
if mastery.slow and e.alive then
|
||||
Slow(e, mastery.slow, mastery.seconds);
|
||||
end
|
||||
if mastery.knock then
|
||||
Blast(e, hx, hy, mastery.knock);
|
||||
end
|
||||
end
|
||||
hits = hits + 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
if entry.heal and hits > 0 then
|
||||
BW.run.pendingHeal = BW.run.pendingHeal + entry.heal * math.min(hits, HOLY_NOVA_HEAL_TARGETS);
|
||||
end
|
||||
-- Arcane Explosion's Echo bursts again (once); the shout's battle cry
|
||||
-- speeds you while it lasts.
|
||||
if mastery and entry.key == "nova" and not entry.echo then
|
||||
QueueCast({
|
||||
key = entry.key, state = entry.state, level = entry.level, damage = entry.damage,
|
||||
radius = entry.radius, echo = true,
|
||||
});
|
||||
entry.state.pending = entry.state.pending + 1;
|
||||
end
|
||||
if mastery and entry.key == "demoshout" then
|
||||
BW.run.shoutUntil = BW.run.time + Stat(def, "duration", entry.level);
|
||||
BW.ApplyHeroSpeed();
|
||||
end
|
||||
end
|
||||
BW.run.castGap = CAST_SPACING;
|
||||
end
|
||||
|
||||
-- Puts Corruption on an enemy: `damage` each second for `seconds`, and it
|
||||
-- spreads to `spread` neighbours when the enemy dies.
|
||||
local function Corrupt(e, damage, seconds, spread)
|
||||
e.dotLeft = seconds;
|
||||
e.dotTick = 1;
|
||||
e.dotDamage = damage;
|
||||
e.dotSeconds = seconds;
|
||||
e.dotSpread = spread;
|
||||
Flash(e);
|
||||
end
|
||||
|
||||
-- Chain Lightning leaps on from the enemy it hit: that enemy casts the next
|
||||
-- leap at its nearest neighbour not yet hit.
|
||||
local chainHit
|
||||
local function NotChainHit(e)
|
||||
return chainHit[e];
|
||||
end
|
||||
|
||||
local function Leap(entry)
|
||||
if not entry.jumps or entry.jumps <= 0 then
|
||||
return;
|
||||
end
|
||||
entry.hit = entry.hit or {};
|
||||
entry.hit[entry.target] = true;
|
||||
local from = entry.target;
|
||||
local fx, fy = from.actor:GetPosition();
|
||||
chainHit = entry.hit;
|
||||
local nextTarget = Nearest(fx, fy, SPELLS[entry.key].hop, NotChainHit);
|
||||
chainHit = nil;
|
||||
if not nextTarget then
|
||||
return;
|
||||
end
|
||||
local castID = from.actor:PlaySpellVisual(SPELLS[entry.key].spell, nextTarget.actor, 0);
|
||||
if castID then
|
||||
BW.run.inFlight[castID] = {
|
||||
key = entry.key, target = nextTarget, jumps = entry.jumps - 1, hit = entry.hit,
|
||||
damage = Mastered(entry.key) and entry.damage * SPELLS[entry.key].mastery.grow or entry.damage,
|
||||
};
|
||||
end
|
||||
end
|
||||
|
||||
BW.OnImpact = function(_, _, _, castID)
|
||||
if not BW.run then
|
||||
return;
|
||||
end
|
||||
local entry = BW.run.inFlight[castID];
|
||||
if not entry then
|
||||
return;
|
||||
end
|
||||
BW.run.inFlight[castID] = nil;
|
||||
if entry.onImpact then
|
||||
entry.onImpact(entry);
|
||||
return;
|
||||
end
|
||||
local def = SPELLS[entry.key];
|
||||
if def.kind == "dot" then
|
||||
if entry.target.alive then
|
||||
Corrupt(entry.target, entry.damage, Stat(def, "duration", entry.level), Stat(def, "spread", entry.level));
|
||||
end
|
||||
return;
|
||||
end
|
||||
if def.kind == "chain" then
|
||||
Leap(entry);
|
||||
end
|
||||
local target = entry.target;
|
||||
local wasAlive = target.alive;
|
||||
Damage(target, entry.damage, def.color);
|
||||
if not Mastered(entry.key) or not def.mastery then
|
||||
return;
|
||||
end
|
||||
local m = def.mastery;
|
||||
if entry.key == "frostbolt" and target.alive then
|
||||
Hold(target, m.freeze, SPELLS.frostnova.kit, true);
|
||||
elseif entry.key == "shadowbolt" and wasAlive and not target.alive then
|
||||
local tx, ty = target.actor:GetPosition();
|
||||
local r2 = m.radius * m.radius;
|
||||
for _, e in ipairs(enemies) do
|
||||
if e.alive then
|
||||
local x, y = e.actor:GetPosition();
|
||||
if Dist2(x, y, tx, ty) <= r2 then
|
||||
Damage(e, entry.damage * m.share, def.color);
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif entry.key == "lightning" and not entry.overload and math.random() < m.chance then
|
||||
local hx, hy = BW.hero:GetPosition();
|
||||
local other = PickTargets(def, 1, hx, hy)[1];
|
||||
local state = BW.run.spells[entry.key];
|
||||
if other and state then
|
||||
QueueCast({ key = entry.key, state = state, target = other, damage = entry.damage, level = entry.level, overload = true });
|
||||
state.pending = state.pending + 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- For the files that load after this one.
|
||||
BW.UpdateWards = UpdateWards
|
||||
BW.ClearWards = ClearWards
|
||||
BW.UpdatePet = UpdatePet
|
||||
BW.ClearPet = ClearPet
|
||||
BW.EndShield = EndShield
|
||||
BW.Stat = Stat
|
||||
BW.Nearest = Nearest
|
||||
BW.UpdateTotems = UpdateTotems
|
||||
BW.ClearTotems = ClearTotems
|
||||
BW.Unslow = Unslow
|
||||
BW.PackCenter = PackCenter
|
||||
BW.UpdateAreas = UpdateAreas
|
||||
BW.markerPool = markerPool
|
||||
BW.ReleaseMarker = ReleaseMarker
|
||||
BW.UpdateLandings = UpdateLandings
|
||||
BW.ClearLandings = ClearLandings
|
||||
BW.ClearAreas = ClearAreas
|
||||
BW.Fire = Fire
|
||||
BW.CastNext = CastNext
|
||||
BW.Corrupt = Corrupt
|
||||
@@ -22,6 +22,7 @@ reference is in **[docs/API.md](docs/API.md)**.
|
||||
| Inline textures | Draws inline texture markup (`\|T…\|t`) and atlas markup (`\|A…\|a`) as icons in FontStrings, chat, and tooltips. This covers item and spell icons, raid-target markers, and the coin icons in money strings. `GetStringWidth` and `GetStringHeight` count the icons, so measured width and text wrapping stay correct. Done in pure C++ by hooking the engine's text pipeline — no addon. |
|
||||
| [Soft targeting](docs/API.md#soft-interact-softinteract--anyinteract) | The Interact Key and soft targets. Bind **Interact With Target** to a key to interact with the nearest NPC, corpse, mailbox, chest or gathering node in front of you. The [`softenemy` and `softfriend`](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend) tokens point at the nearest enemy and friendly unit without a change of target. With no target, harmful spells go to `softenemy` and helpful spells go to `softfriend`. `PLAYER_SOFT_*_CHANGED` events fire when the units change. |
|
||||
| [Animations](docs/API.md#animation) | Animation groups on frames, textures and font strings, made in Lua or XML: `CreateAnimationGroup`, looping, smoothing, scripts and templates, with `Alpha`, `Translation`, `Scale`, `Rotation` and `Path` applied. |
|
||||
| [Model scenes](docs/API.md#modelscene) | The `ModelScene` frame type shows many models in one 3D scene, with its own camera, light and fog. Each model is an actor that you place, turn, scale and animate. An actor shows a creature display, a model file or a copy of a unit. It can also seat a rider, show spell visual kits and pause on one frame. A scene can show a WMO, for example a dungeon, around the actors, with its props and their sounds. An actor can move on the WMO as a character moves in the world: it runs, jumps, falls, swims and walks to a point. You hear the sounds of a scene from its camera, or from an actor. A follow camera follows an actor as the camera of the world follows your character, and stops in front of walls. An actor can glow, show a selection circle and show a name over its head, and a scene can show the click-to-move marker. |
|
||||
| Tooltip line cap | Removes `GameTooltip`'s 30-line limit for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). A tooltip adds a new line pair when it needs one, so long tooltips and comparison blocks (for example, pfUI's eqcompare) keep all their lines. |
|
||||
| [XML attributes](docs/API.md#parentkey-in-xml) | More XML attributes work: `parentKey="Name"` makes `parent.Name` point to a frame, texture or font string, `<OnLoad function="Name"/>` uses a global function as a script handler, `<OnLoad method="Name"/>` uses the frame's own function, `mixin="A, B"` copies mixins onto the object, `<KeyValues>` sets fields on the object, a button's `<NormalFont style="..."/>` sets its font, and `inherits="A, B"` applies more than one template. `parentArray="Name"` adds the object to a list on its parent. An `<Anchor>` takes `x` and `y` offsets and `relativeKey="$parent.Key"`. `<OnEnter inherit="prepend">` runs the template's handler and then its own, and `inherit="append"` runs them in the other order. A frame with `intrinsic="true"` makes a new frame type. Frames take `scale`, `registerForDrag`, `clipChildren`, `useParentLevel`, `dontSavePosition`, `propagateKeyboardInput`, `propagateMouseInput`, `enableMouseClicks`, `enableMouseMotion` and `motionScriptsWhileDisabled`, and buttons take `registerForClicks`. Textures take `alpha`, `atlas`, `useAtlasSize`, `mask`, `desaturated`, `rotation`, `horizTile`, `vertTile`, `hWrapMode` and `vWrapMode`. Font strings take `alpha`, `wordwrap`, `indented`, `degrees` and `radians`. Status bars take `reverseFill`, `rotatesTexture` and `fillStyle`, and edit boxes take `countInvisibleLetters`. |
|
||||
| [Macro conditions](docs/API.md#more-commands-with-conditions) | `[conditions]` and `@unit` work on 38 slash commands, from `/cast` and `/use` to `/target`, `/follow`, `/cancelaura` and the `/pet` family. With `/cast [@mouseover,harm][] Fireball`, the spell goes to the mouseover unit. With no mouseover unit, it goes to your target. `@cursor` places a ground-target spell or item under the mouse and `@player` places it at your feet. A character name works where a unit token does (`[target=Feral]`). Addons read the same rules through `SecureCmdOptionParse`. |
|
||||
@@ -88,6 +89,8 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation`, `:SetTimerDuration` / `:GetTimerDuration`, `:SetValue` / `:SetMinMaxValues` with `interpolation`, `:GetInterpolatedValue`, `:IsInterpolating`, `:SetToTargetValue` |
|
||||
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--sliderisenabled) | `:Enable` / `:Disable` / `:IsEnabled` |
|
||||
| [Frame: Cooldown](docs/API.md#createframecooldown--name-parent-template) | `CreateFrame("Cooldown")`, `:SetCooldown`, `:SetCooldownDuration`, `:SetCooldownFromExpirationTime`, `:Clear`, `:GetCooldownTimes`, `:GetCooldownDuration`, `:GetCooldownDisplayDuration`, `:Pause` / `:Resume`, `:SetPaused` / `:IsPaused`, `:SetReverse` / `:GetReverse`, `:SetDrawEdge` / `:GetDrawEdge`, `:SetDrawSwipe` / `:GetDrawSwipe`, `:SetDrawBling` / `:GetDrawBling`, `:SetSwipeColor`, `:SetEdgeColor`, `:SetSwipeTexture`, `:SetEdgeTexture`, `:SetBlingTexture`, `:SetRotation` / `:GetRotation`, `:SetHideCountdownNumbers` / `:GetHideCountdownNumbers`, `:GetCountdownFontString`, `:SetCountdownAbbrevThreshold` / `:GetCountdownAbbrevThreshold`, `:SetCountdownMillisecondsThreshold` / `:GetCountdownMillisecondsThreshold`, `:SetMinimumCountdownDuration` / `:GetMinimumCountdownDuration`, `OnCooldownDone`, `CooldownFrame_Set`, `CooldownFrame_Clear`, `CooldownFrame_SetDisplayAsPercentage` |
|
||||
| [Frame: ModelScene](docs/API.md#modelscene) | `CreateFrame("ModelScene")`, `<ModelScene>` (`drawLayer`, `fogNear`, `fogFar`, `<FogColor>`, `<ViewInsets>`), `:CreateActor`, `:GetNumActors`, `:GetActorAtIndex`, `:SetCameraPosition` / `:GetCameraPosition`, `:SetCameraOrientationByYawPitchRoll`, `:SetCameraOrientationByAxisVectors`, `:GetCameraForward`, `:GetCameraRight`, `:GetCameraUp`, `:SetCameraFieldOfView` / `:GetCameraFieldOfView`, `:SetCameraNearClip` / `:GetCameraNearClip`, `:SetCameraFarClip` / `:GetCameraFarClip`, `:SetViewInsets` / `:GetViewInsets`, `:SetViewTranslation` / `:GetViewTranslation`, `:Project3DPointTo2D`, `"FollowCamera"` (`:SetFollowActor` / `:GetFollowActor`), `:SetLightType` / `:GetLightType`, `:SetLightVisible` / `:IsLightVisible`, `:SetLightDirection` / `:GetLightDirection`, `:SetLightPosition` / `:GetLightPosition`, `:SetLightAmbientColor` / `:GetLightAmbientColor`, `:SetLightDiffuseColor` / `:GetLightDiffuseColor`, `:SetFogColor` / `:GetFogColor`, `:SetFogNear` / `:GetFogNear`, `:SetFogFar` / `:GetFogFar`, `:ClearFog`, `:SetDrawLayer` / `:GetDrawLayer`, `:SetPaused`, `:GetSceneTime`, `:After`, `:NewTimer`, `:NewTicker`, `:SetAllowOverlappedModels` / `:GetAllowOverlappedModels`, `:SetGroundTerrain` / `:GetGroundTerrain`, `:SetWorldModel`, `:SetWorldModelByMapID`, `:ClearWorldModel`, `:SetPlayWorldModelSounds` / `:GetPlayWorldModelSounds`, `:TraceSegment`, `:GetLiquidAtPoint`, `:SweepCollider`, `:SetSoundListenerActor` / `:GetSoundListenerActor`, `:GetActorAtCursor`, `:GetCursorRay`, `:GetActorsInRadius`, `:SetMoveMarker` / `:ClearMoveMarker` / `:GetMoveMarker`, `OnActorEnter` / `OnActorLeave` |
|
||||
| [Frame: ModelSceneActor](docs/API.md#actorsetmodelbycreaturedisplayiddisplayid--useactiveplayercustomizations) | `:SetModelByCreatureDisplayID`, `:SetModelByPath`, `:SetModelByUnit`, `:SetModelByUnitCreatureDisplayID`, `:GetModelUnitGUID`, `:Dress`, `:Undress`, `:UndressSlot`, `:TryOn`, `:DressPlayerSlot`, `:IsSlotAllowed` / `:IsSlotVisible`, `:GetItemTransmogInfo`, `:GetItemTransmogInfoList`, `:SetItemTransmogInfo`, `:SetSheathed` / `:GetSheathed`, `:SetSheathedCategory`, `:UseUnitSheatheCategory`, `:SetAutoDress` / `:GetAutoDress`, `:IsGeoReady`, `:ClearModel`, `:IsLoaded`, `:GetModelPath`, `:SetPosition` / `:GetPosition`, `:SetYaw` / `:GetYaw`, `:SetPitch` / `:GetPitch`, `:SetRoll` / `:GetRoll`, `:SetScale` / `:GetScale`, `:SetUseCenterForOrigin` / `:IsUsingCenterForOrigin`, `:GetActiveBoundingBox`, `:GetMaxBoundingBox`, `:Show` / `:Hide` / `:SetShown` / `:IsShown` / `:IsVisible`, `:SetAlpha` / `:GetAlpha`, `:SetAnimation` / `:GetAnimation`, `:GetAnimationVariation`, `:SetAnimationBlendOperation` / `:GetAnimationBlendOperation`, `:SetPaused` / `:GetPaused`, `:SetSpellVisualKit` / `:GetSpellVisualKit`, `:ApplySpellVisualKit`, `:PlayAnimationKit` / `:StopAnimationKit`, `:AttachToMount`, `:DetachFromMount`, `:CalculateMountScale`, `:GetAttachmentPosition`, `:AttachToActor`, `:DetachFromActor`, `:PlaySpellVisual`, `:StopSpellVisual`, `:PlaySound`, `:SetPlayAnimationSounds` / `:GetPlayAnimationSounds`, `:SetShadowShown` / `:IsShadowShown`, `:SetHighlight` / `:IsHighlighted`, `:SetSelectionCircle` / `:ClearSelectionCircle` / `:GetSelectionCircle`, `:SetNameText` / `:ClearNameText` / `:GetNameText`, `:GetNamePosition`, `:SetMovementEnabled` / `:IsMovementEnabled`, `:StartMovingForward`, `:StartMovingBackward`, `:StopMoving`, `:StartStrafingLeft`, `:StartStrafingRight`, `:StopStrafing`, `:StartTurningLeft`, `:StartTurningRight`, `:StopTurning`, `:Jump`, `:StartPitchingUp`, `:StartPitchingDown`, `:StopPitching`, `:IsFalling`, `:IsSwimming`, `:MoveTo`, `:SetWalking` / `:IsWalking`, `:GetCameraPivotHeight`, `OnLoad`, `OnUpdate`, `OnModelLoading`, `OnModelLoaded`, `OnModelCleared`, `OnAnimFinished`, `OnSpellVisualImpact`, `OnMoveFinished`, `<Actor>` templates |
|
||||
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled`, `:GetMessageInfo` (with the extra `:AddMessage` values), `:BackFillMessage` (adds a message as the oldest), `:RemoveMessagesByPredicate`, `:SetScrollOffset` / `:GetScrollOffset`, `:GetMaxScrollRange`, `:ScrollByAmount`, `:SetInsertMode` / `:GetInsertMode`, `:AddOnDisplayRefreshedCallback` |
|
||||
| [Frame: scripts](docs/API.md#onenable-and-ondisable-scripts) | `OnEnable`, `OnDisable`, `OnMinMaxChanged`, `OnAttributeChanged`, `OnCharComposition`, `OnTextChanged` (`userInput`), `PreClick`, `PostClick`, `SetModernScriptArgs` / `GetModernScriptArgs` |
|
||||
| [FriendList](docs/API.md#friendlist) | `AddOrRemoveFriend`, `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
|
||||
@@ -96,7 +99,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:AddSpellByID`, `GameTooltip:AddTexture`, `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetMinimumWidth`, `GameTooltip:GetOwner`, `GameTooltip:GetPadding`, `GameTooltip:GetSpell`, `GameTooltip:GetUnit`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:IsUnit`, `GameTooltip:SetAnchorType`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetFrameStack`, `GameTooltip:SetHyperlink` (full links, `spell:`, `quest:`), `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetTotem`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem`, `OnTooltipSetSpell`, `OnTooltipSetUnit`, `OnTooltipSetGameObject`, `OnTooltipSetFrameStack` (scripts) |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetPoiForUiMapID`, `C_GossipInfo.GetPoiInfo`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex`, `GetNumGossipActiveQuests`, `GetNumGossipAvailableQuests`, `GetNumGossipOptions` |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `GetMouseButtonName`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Input](docs/API.md#input) | `GetCursorDelta`, `GetMouseButtonClicked`, `GetMouseButtonName`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo`, `IsActiveBattlefieldArena` |
|
||||
| [Item: C_Item](docs/API.md#item) | `C_Item.DoesItemExist` / `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemClassInfo`, `C_Item.GetItemCount`, `C_Item.GetItemData` / `C_Item.GetItemDataByID`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon` / `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfo`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType` / `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize` / `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName` / `C_Item.GetItemNameByID`, `C_Item.GetItemQuality` / `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice` / `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID` / `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemTempEnchantInfo`, `C_Item.GetItemUniqueness` / `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached` / `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.IsUsableItem`, `C_Item.ItemHasRange`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockItem`, `C_Item.UnlockAllItems`, `C_Item.PickupItem`, `C_Item.RequestLoadItemData` / `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName` |
|
||||
| [Item: info](docs/API.md#getitemclassinfoclassid) | `GetItemClassInfo`, `GetItemSubClassInfo`, `GetItemIcon`, `IsConsumableItem`, `IsCurrentItem`, `IsDressableItem`, `IsEquippedItemType`, `IsHarmfulItem`, `IsHelpfulItem`, `IsUsableItem` |
|
||||
@@ -112,7 +115,9 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Map](docs/API.md#map) | `C_Map.CanSetUserWaypointOnMap`, `C_Map.ClearUserWaypoint`, `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetFallbackWorldMapID`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapArtLayers`, `C_Map.GetMapArtLayerTextures`, `C_Map.GetMapChildrenInfo`, `C_Map.GetMapInfo`, `C_Map.GetMapInfoAtPosition`, `C_Map.GetMapOverlays`, `C_Map.GetMapPosFromWorldPos`, `C_Map.GetMapRectOnMap`, `C_Map.GetMapWorldSize`, `C_Map.GetPlayerMapPosition`, `C_Map.GetUserWaypoint`, `C_Map.GetUserWaypointFromHyperlink`, `C_Map.GetUserWaypointHyperlink`, `C_Map.GetUserWaypointPositionForMap`, `C_Map.GetWorldPosFromMapPos`, `C_Map.HasUserWaypoint`, `C_Map.MapHasArt`, `C_Map.SetUserWaypoint` |
|
||||
| [MapExplorationInfo](docs/API.md#mapexplorationinfo) | `C_MapExplorationInfo.GetExploredAreaIDsAtPosition`, `C_MapExplorationInfo.GetExploredMapTextures`, `C_MapExplorationInfo.GetUnexploredMapTextures` |
|
||||
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems`, `GetBuybackItemLink` |
|
||||
| [Model](docs/API.md#model) | `DressUpModel:TryOn` (links, recipes, names, hand, enchant glow, result), `DressUpModel:UndressSlot`, `DressUpModel:IsGeoReady`, `DressUpModel:SetSheathed` / `GetSheathed`, `DressUpModel:SetAutoDress` / `GetAutoDress`, `Model:SetGlow`, `PlayerModel:RefreshCamera`, `PlayerModel:SetCamDistanceScale`, `PlayerModel:SetCreature` (`displayID`), `PlayerModel:SetDisplayInfo`, `PlayerModel:SetItem`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`), `PlayerModel:SetUnit` (result), `PlayerModel:CanSetUnit`, `PlayerModel:GetDisplayInfo`, `PlayerModel:SetAnimation`, `PlayerModel:FreezeAnimation`, `PlayerModel:HasAnimation`, `PlayerModel:SetDoBlend` / `GetDoBlend`, `PlayerModel:SetKeepModelOnHide` / `GetKeepModelOnHide`, `PlayerModel:ApplySpellVisualKit` |
|
||||
| [Model](docs/API.md#model) | `DressUpModel:TryOn` (links, recipes, names, hand, enchant glow, result), `DressUpModel:UndressSlot`, `DressUpModel:IsGeoReady`, `DressUpModel:SetSheathed` / `GetSheathed`, `DressUpModel:SetAutoDress` / `GetAutoDress`, `DressUpModel:GetItemTransmogInfo` / `GetItemTransmogInfoList` / `SetItemTransmogInfo`, `DressUpModel:IsSlotAllowed` / `IsSlotVisible`, `Model:SetGlow`, `PlayerModel:RefreshCamera`, `PlayerModel:SetCamDistanceScale`, `PlayerModel:SetCreature` (`displayID`), `PlayerModel:SetDisplayInfo`, `PlayerModel:SetItem`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`), `PlayerModel:SetUnit` (result), `PlayerModel:CanSetUnit`, `PlayerModel:GetDisplayInfo`, `PlayerModel:SetAnimation`, `PlayerModel:FreezeAnimation`, `PlayerModel:HasAnimation`, `PlayerModel:SetDoBlend` / `GetDoBlend`, `PlayerModel:SetKeepModelOnHide` / `GetKeepModelOnHide`, `PlayerModel:ApplySpellVisualKit` |
|
||||
| [ModelInfo](docs/API.md#c_modelinfogetmodelsceneinfobyidmodelsceneid) | `C_ModelInfo.GetModelSceneInfoByID`, `C_ModelInfo.GetModelSceneCameraInfoByID`, `C_ModelInfo.GetModelSceneActorInfoByID`, `C_ModelInfo.GetModelSceneActorDisplayInfoByID` |
|
||||
| [Mount](docs/API.md#mount) | `C_MountJournal.GetMountFromItem`, `C_MountJournal.GetMountFromSpell`, `C_MountJournal.GetMountInfoExtraByID` |
|
||||
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` |
|
||||
@@ -161,6 +166,7 @@ the glue state because GlueXML had no way to reach them otherwise.
|
||||
| [CharacterList](docs/API.md#characterlist) | `GetSavedCharacterOrder`, `SetSavedCharacterOrder` (persist to `WTF\Account\...\ClassicAPI.txt`) |
|
||||
| CVar | `GetCVar`, `SetCVar`, `RegisterCVar`, `GetCVarDefault`, `C_CVar.GetCVarBool`, `C_CVar.SetTempCVar`, `C_CVar.RemoveTempCVar` (storage is process-global — writes from glue are visible in-world and vice versa) |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsFirstLoadThisSession`, `C_Glue.IsOnGlueScreen` |
|
||||
| [Input](docs/API.md#getcursordelta) | `GetCursorDelta` |
|
||||
| Script | `RunScript` (compile and run a Lua chunk in the glue state's globals — useful for slash-command-style helpers in GlueXML) |
|
||||
| State | `IsLoggedIn` |
|
||||
|
||||
@@ -221,7 +227,7 @@ open the game menu and click AddOn Options, then ClassicAPI.
|
||||
| [Combat log](docs/API.md#kept-entries) | `combatLogRetentionTime` | `300` | The number of seconds that the combat log keeps an entry. |
|
||||
| [Stand, dismount and form](docs/API.md#stand-dismount-and-form-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast, loot or open the flight map while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast, use an object, loot, attack or open the flight map while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when you cast a spell that cannot be cast in it, talk to an NPC, use an object or an item, or open the flight map. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when you cast a spell that cannot be cast in it, use an item, or open the flight map. It also lets you talk to NPCs and use objects in druid forms and Ghost Wolf. |
|
||||
| [Bag sorting](docs/API.md#c_containergetsortbagsrighttoleft--c_containersetsortbagsrighttoleftenable) | `sortBagsRightToLeft` | `0` | `1` makes a sort fill your bags from the last slot of the last bag. |
|
||||
| | `backpackAutosortDisabled` | `0` | `1` keeps the items in your backpack in place when you sort your bags. |
|
||||
| | `bankAutosortDisabled` | `0` | `1` keeps the items in the main bank slots in place when you sort your bank. |
|
||||
@@ -398,6 +404,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| Screen location type | `Enum.ScreenLocationType.{Center,Left,Right,Top,Bottom,TopLeft,TopRight,LeftOutside,RightOutside,LeftRight,TopBottom,LeftRightOutside}` (0–11) |
|
||||
| Self-resurrect option type | `Enum.SelfResurrectOptionType.{Spell,Item}` (0–1) |
|
||||
| Autocomplete entry flag | `Enum.AutoCompleteEntryFlag.{InGroup,InGuild,Friend,Bnet,InteractedWith,Online,InAOI,AccountCharacter,RecentPlayer}` (bits 1–256) |
|
||||
| Model blend operation | `Enum.ModelBlendOperation.{None,Anim}` (0–1) |
|
||||
| Autocomplete priority | `Enum.AutoCompletePriority.{Other,Interacted,InGroup,Guild,Friend,AccountCharacter,AccountCharacterSameRealm}` (0–6) |
|
||||
|
||||
</details>
|
||||
|
||||
+2963
-904
File diff suppressed because it is too large
Load Diff
+7
-4
@@ -80,10 +80,13 @@ static void __stdcall LoadGlueScriptFunctions_h() {
|
||||
}
|
||||
|
||||
// Every Lua-side `frame:RegisterEvent(...)` is a chance to claim a slot
|
||||
// for any custom event still waiting. By this point the engine's table
|
||||
// is fully populated and SuperWoWhook / other DLLs have done their
|
||||
// post-rebuild writes, so the table state is settled and our walk from the
|
||||
// low end finds genuinely free slots.
|
||||
// for any custom event still waiting. By the first FrameXML registration
|
||||
// the engine's table is fully populated and SuperWoWhook / other DLLs have
|
||||
// done their post-rebuild writes, so the walk from the low end finds
|
||||
// genuinely free slots. A registration that arrives EARLIER (another DLL's
|
||||
// frame, between LoadScriptFunctions and the table build — issue #134)
|
||||
// still sees glue's 26-entry table; `RetryClaims` refuses that table and
|
||||
// re-validates its cached slots against the live one on every call.
|
||||
static void __fastcall FrameRegisterEvent_h(void *frame, void *edx,
|
||||
const char *eventName) {
|
||||
Event::Custom::RetryClaims();
|
||||
|
||||
+13
-5
@@ -30,15 +30,23 @@ float PixelToInternal(float px) {
|
||||
const float div = Read<float>(Offsets::VAR_COORD_ASPECT_COMPENSATION);
|
||||
if (div == 0.0f)
|
||||
return 0.0f;
|
||||
return px * mul / (div * static_cast<float>(Offsets::UI_COORD_SCALE_UNIT));
|
||||
return px * mul / (div * Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE));
|
||||
}
|
||||
|
||||
double InternalToPixel(float internal) {
|
||||
const float mul = Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE);
|
||||
const float div = Read<float>(Offsets::VAR_COORD_ASPECT_COMPENSATION);
|
||||
if (mul == 0.0f)
|
||||
using GetScaledScreenAspect_t = float(__cdecl *)();
|
||||
// Declared double so the quotient is read from ST0, as the engine's
|
||||
// pushes store it with `fstp qword`, not narrowed to float.
|
||||
using DivideByScreenScaleX_t = double(__stdcall *)(float);
|
||||
if (Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE) == 0.0f)
|
||||
return 0.0;
|
||||
return static_cast<double>(internal) * div * Offsets::UI_COORD_SCALE_UNIT / mul;
|
||||
// `aspect * 1024` is exact, so the float product rounds once, as the
|
||||
// engine's `fstp dword` does.
|
||||
const float aspect =
|
||||
reinterpret_cast<GetScaledScreenAspect_t>(Offsets::FUN_COORD_GET_SCALED_SCREEN_ASPECT)();
|
||||
const float scaled = aspect * Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE) * internal;
|
||||
return reinterpret_cast<DivideByScreenScaleX_t>(Offsets::FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X)(
|
||||
scaled);
|
||||
}
|
||||
|
||||
bool StringToFlag(const char *text) {
|
||||
|
||||
+16
-2
@@ -58,6 +58,18 @@ inline T &Ref(uintptr_t address) {
|
||||
return *reinterpret_cast<T *>(address);
|
||||
}
|
||||
|
||||
// An engine constant at a fixed VA, read each time it is used, so the value is
|
||||
// the one the engine's own code reads: a runtime patch shows, and so does a
|
||||
// global the engine's static initializers fill after this DLL loads. Converts
|
||||
// to T, so it stands in a float expression as a float would.
|
||||
//
|
||||
// const Game::Live<float> kSkin{Offsets::VAR_MOVER_SKIN};
|
||||
template <typename T>
|
||||
struct Live {
|
||||
uintptr_t address;
|
||||
operator T() const { return Read<T>(address); }
|
||||
};
|
||||
|
||||
// Calls `fn(object)` for each object of one of a frame's intrusive lists,
|
||||
// OFF_FRAME_REGION_LIST or OFF_FRAME_CHILD_LIST: node+4 next, node+8 object,
|
||||
// a NULL or low-bit-1 link ends it - the walk the engine's own frame code
|
||||
@@ -89,8 +101,10 @@ void *ResolveUnitToken(const char *token);
|
||||
|
||||
// UI pixels <-> internal layout (anchor) units. Script_SetPoint converts its
|
||||
// pixel offsets with `px * [VAR_COORD_DDC_WIDTH_SCALE] / ([VAR_COORD_ASPECT_COMPENSATION]
|
||||
// * UI_COORD_SCALE_UNIT)`; Script_GetLeft and Script_GetStringWidth push the
|
||||
// inverse. A region's LOCAL pixels must be multiplied by its effective scale
|
||||
// * [FLOAT_SCRIPT_COORD_SCALE])`; every script coordinate push (GetLeft,
|
||||
// GetStringWidth, GetFont's height, GetCursorPosition, OnCursorChanged) is the
|
||||
// inverse, `DivideByScreenScaleX(GetScaledScreenAspect() * 1024 * value)`,
|
||||
// which InternalToPixel calls in that order. A region's LOCAL pixels must be multiplied by its effective scale
|
||||
// (OFF_REGION_EFFECTIVE_SCALE) first. Both return 0 while the scale globals
|
||||
// are unset, so an early caller gets a zero offset rather than a wild one.
|
||||
float PixelToInternal(float px);
|
||||
|
||||
+1704
-27
File diff suppressed because it is too large
Load Diff
@@ -77,6 +77,7 @@
|
||||
#include "addons/TocRewrite.h"
|
||||
#include "bindings/Inject.h"
|
||||
#include "embed/Resource.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -153,8 +154,6 @@ Embed::Bytes LookupEmbedded(const char *relPath) {
|
||||
// by the caller's standard SMemFree in turn.
|
||||
using AddOns::EngineIO::FileExistsFn;
|
||||
using AddOns::EngineIO::FileReadFn;
|
||||
using AddOns::EngineIO::SMemAllocFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
|
||||
// The trampolines to the original FUN_FILE_READ / FUN_FILE_EXISTS (installed
|
||||
// by the co-hooks below).
|
||||
@@ -277,8 +276,7 @@ void DecideSource(int addon) {
|
||||
// Free the disk buffer we just borrowed — we only needed it for
|
||||
// the version-line scrape. The actual content for the engine's
|
||||
// TOC read comes through the regular hook path below.
|
||||
auto SMemFree = reinterpret_cast<SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
SMemFree(diskBuf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(diskBuf, __FILE__, __LINE__);
|
||||
|
||||
// Missing or unparseable disk version → assume it's older than
|
||||
// anything we ship, embedded wins.
|
||||
@@ -320,10 +318,8 @@ int ReadAddonFile(int addon, const char *relPath, int unused, const char *path,
|
||||
flag1, flag2);
|
||||
}
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAllocFn>(
|
||||
Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
const size_t totalSize = entry.size + extraBytes;
|
||||
void *buf = SMemAlloc(totalSize, __FILE__, __LINE__, 0);
|
||||
void *buf = Storm::Alloc(totalSize, __FILE__, __LINE__);
|
||||
if (buf == nullptr) return 0;
|
||||
std::memcpy(buf, entry.data, entry.size);
|
||||
if (extraBytes > 0) {
|
||||
|
||||
@@ -32,10 +32,6 @@ using FileReadFn = int(__stdcall *)(int unused, const char *path, void **outBuf,
|
||||
// FUN_FILE_EXISTS (0x00648A30) — __stdcall, RET 8.
|
||||
using FileExistsFn = int(__stdcall *)(const char *path, int mode);
|
||||
|
||||
// FUN_STORM_SMEM_ALLOC / FUN_STORM_SMEM_FREE — __stdcall, RET 0x10. Buffers
|
||||
// FUN_FILE_READ hands out are freed with SMemFreeFn; a buffer we allocate with
|
||||
// SMemAllocFn is freed cleanly by the engine's own SMemFree in turn.
|
||||
using SMemAllocFn = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
|
||||
using SMemFreeFn = void(__stdcall *)(void *buf, const char *file, int line, int flags);
|
||||
// Buffers FUN_FILE_READ hands out are freed with Storm::Free (storm/Memory.h).
|
||||
|
||||
} // namespace AddOns::EngineIO
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/Toc.h"
|
||||
#include "luasyntax/AddonNamespace.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -51,7 +52,6 @@ using IsLoadedFn_t = uint8_t(__fastcall *)(const char *name);
|
||||
// FUN_FILE_READ address routes through Embedded's file-read hook, which forwards
|
||||
// every non-`!!!ClassicAPI` path to the original untouched — what we want here.
|
||||
using AddOns::EngineIO::FileReadFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
|
||||
// True iff the addon's `.toc` has a line `## AllowAddOnTableAccess: <nonzero>`.
|
||||
// The value is read as a TOC boolean flag: parse the leading integer, true iff
|
||||
@@ -87,8 +87,7 @@ bool TocAllowsTableAccess(const char *name) {
|
||||
allowed = anyDigit && value != 0;
|
||||
}
|
||||
|
||||
auto smemFree = reinterpret_cast<SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
smemFree(buf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(buf, __FILE__, __LINE__);
|
||||
return allowed;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
#include "addons/Embedded.h"
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/Registry.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -235,9 +236,7 @@ bool TryHashToc(const char *name, uint32_t *outHash) {
|
||||
if (!ReadToc(name, &buf, &size))
|
||||
return false;
|
||||
*outHash = HashTocMetadata(static_cast<const char *>(buf), size);
|
||||
reinterpret_cast<AddOns::EngineIO::SMemFreeFn>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_STORM_SMEM_FREE))(
|
||||
buf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(buf, __FILE__, __LINE__);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/Toc.h"
|
||||
#include "script/LoadFile.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -66,7 +67,6 @@ constexpr size_t NPOS = static_cast<size_t>(-1);
|
||||
// FlavorToc applies), the exists-check, and the Storm free. See addons/EngineIO.h.
|
||||
using AddOns::EngineIO::FileExistsFn;
|
||||
using AddOns::EngineIO::FileReadFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
using LoadTocFilesFn = uint32_t(__fastcall *)(char *tocPath, int *a2, int *a3);
|
||||
using NameFn = const char *(*)(); // no-arg readers (return in EAX)
|
||||
|
||||
@@ -160,8 +160,7 @@ SvFlags ReadSvFlags(const char *tocPath) {
|
||||
f.perChar =
|
||||
AddOns::Toc::FindValue(b, size, "SavedVariablesPerCharacter", &v, &n) && n > 0;
|
||||
|
||||
reinterpret_cast<SMemFreeFn>(static_cast<uintptr_t>(Offsets::FUN_STORM_SMEM_FREE))(
|
||||
buf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(buf, __FILE__, __LINE__);
|
||||
return f;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "localization/Locale.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "storm/Memory.h"
|
||||
#include "turtle/Detect.h"
|
||||
|
||||
#include <cstddef>
|
||||
@@ -28,8 +29,6 @@ namespace AddOns::TocRewrite {
|
||||
|
||||
namespace {
|
||||
|
||||
using AddOns::EngineIO::SMemAllocFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
using AddOns::Toc::EqCI;
|
||||
using AddOns::Toc::Lower;
|
||||
|
||||
@@ -288,15 +287,13 @@ void Transform(const char *path, void **outBuf, size_t *outSize) {
|
||||
std::memcmp(rebuilt.data(), content, size) == 0)
|
||||
return; // brackets/commas were only in unaffected lines — nothing changed
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAllocFn>(Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
auto SMemFree = reinterpret_cast<SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
const size_t newLen = rebuilt.size();
|
||||
void *buf = SMemAlloc(newLen + 1, __FILE__, __LINE__, 0); // +1 NUL (extraBytes=1)
|
||||
void *buf = Storm::Alloc(newLen + 1, __FILE__, __LINE__); // +1 NUL (extraBytes=1)
|
||||
if (buf == nullptr) return; // OOM — keep the original
|
||||
std::memcpy(buf, rebuilt.data(), newLen);
|
||||
static_cast<char *>(buf)[newLen] = '\0';
|
||||
|
||||
SMemFree(*outBuf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(*outBuf, __FILE__, __LINE__);
|
||||
*outBuf = buf;
|
||||
if (outSize != nullptr) *outSize = newLen;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Layout.h"
|
||||
#include "frame/Alpha.h"
|
||||
#include "frame/Destruction.h"
|
||||
#include "texture/Transform.h"
|
||||
@@ -115,7 +116,6 @@ using SetColor_t = void(__fastcall *)(void *region, void *edx, const uint32_t *b
|
||||
using StoreCorners_t = void(__fastcall *)(void *region, void *edx, float *rect);
|
||||
using Crop_t = void(__fastcall *)(void *region, void *edx, float *rect);
|
||||
using SetStringPos_t = void(__fastcall *)(void *block, const float *position);
|
||||
using NdcToDdc_t = void(__fastcall *)(float *outX, float *outY, float x, float y);
|
||||
using SizeChanged_t = void(__fastcall *)(void *layout, void *edx, const float *oldRect);
|
||||
using InvalidateNode_t = void(__fastcall *)(void *node);
|
||||
|
||||
@@ -216,7 +216,7 @@ bool GetStringPos(void *block, float out[3]) {
|
||||
if (node == nullptr)
|
||||
return false;
|
||||
const float *position = Game::Ptr<const float>(node, Offsets::OFF_TEXT_NODE_POSITION);
|
||||
reinterpret_cast<NdcToDdc_t>(Offsets::FUN_COORD_NDC_TO_DDC)(&out[0], &out[1], position[0], position[1]);
|
||||
Frame::Layout::NdcToDdc(&out[0], &out[1], position[0], position[1]);
|
||||
out[2] = position[2];
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@ bool FloatAttr(const void *node, const char *name, float &out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// object[parentKey] = child, as 4.3.4's LoadXML does for both.
|
||||
void BindParentKey(void *object, Object *child, const char *key) {
|
||||
if (key == nullptr || *key == '\0')
|
||||
@@ -145,6 +147,8 @@ void LoadKeyValues(Object *object, const void *node, void *status) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// LoadXML_AnimOrigin: <Origin point="..."><Offset x= y=/></Origin>.
|
||||
void LoadOrigin(Anim *anim, const void *node) {
|
||||
const void *origin = FindChild(node, "Origin");
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
// (RegisterXMLTemplate), whatever its tag, so `inherits` resolves through
|
||||
// Xml::Template as 4.3.4's XMLHashNodeRef does.
|
||||
namespace Animation {
|
||||
class Object;
|
||||
class Group;
|
||||
class Anim;
|
||||
class ControlPoint;
|
||||
@@ -39,4 +40,13 @@ void LoadControlPoint(ControlPoint *point, const void *node, void *status);
|
||||
// The template node `name` resolves to, or nullptr.
|
||||
const void *Template(const char *name);
|
||||
|
||||
// What every script object's element shares (Model::Scene's actors too):
|
||||
// object[key] = child for parentKey, an append to object[key] for
|
||||
// parentArray, the <Scripts> (a body, a `function` global or a `method`
|
||||
// field) and the <KeyValues>.
|
||||
void BindParentKey(void *object, Object *child, const char *key);
|
||||
void BindParentArray(void *object, Object *child, const char *key);
|
||||
void LoadScripts(Object *object, const void *node);
|
||||
void LoadKeyValues(Object *object, const void *node, void *status);
|
||||
|
||||
} // namespace Animation::Xml
|
||||
|
||||
+2
-9
@@ -1023,16 +1023,9 @@ CompressState *FindOrClaimCompressState(uint64_t targetGuid,
|
||||
// would compress it, this gate excludes it exactly as the server's does.
|
||||
// (Drain Soul passes via its effect-2 periodic damage: aura=[86,3,0].)
|
||||
bool HasPeriodicDamageOrLeech(const uint8_t *rec) {
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
constexpr int32_t kPeriodicDamage = 3, kPeriodicLeech = 53;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int32_t aura = *reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME + i * 4);
|
||||
if (aura == kPeriodicDamage || aura == kPeriodicLeech)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return Spell::Lookup::HasAuraEffect(rec, kPeriodicDamage) ||
|
||||
Spell::Lookup::HasAuraEffect(rec, kPeriodicLeech);
|
||||
}
|
||||
|
||||
// Compression pairs by CASTER, mirroring the server (each warlock's Dark
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
#include "Inject.h"
|
||||
#include "Common.h"
|
||||
#include "Offsets.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include "embed/Resource.h"
|
||||
#include "embedded_resources.h"
|
||||
@@ -213,8 +214,6 @@ const char *FindSplicePosition(const char *content, size_t size,
|
||||
return FindOutsideComment(content, end, "</Bindings>");
|
||||
}
|
||||
|
||||
using SMemAlloc_t = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
|
||||
using SMemFree_t = void(__stdcall *)(void *buf, const char *file, int line, int flags);
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -257,10 +256,8 @@ bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
const size_t splicePos = static_cast<size_t>(splice - content);
|
||||
const size_t newSize = origSize + config.innerSize;
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAlloc_t>(Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
auto SMemFree = reinterpret_cast<SMemFree_t>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
|
||||
void *newBuf = SMemAlloc(newSize + extraBytes, __FILE__, __LINE__, 0);
|
||||
void *newBuf = Storm::Alloc(newSize + extraBytes, __FILE__, __LINE__);
|
||||
if (newBuf == nullptr) {
|
||||
// Allocation failed — fall back to the original buffer so the
|
||||
// engine still loads its own bindings.
|
||||
@@ -277,7 +274,7 @@ bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
if (extraBytes > 0)
|
||||
std::memset(dest + newSize, 0, extraBytes);
|
||||
|
||||
SMemFree(origBuf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(origBuf, __FILE__, __LINE__);
|
||||
|
||||
if (outBuf != nullptr) *outBuf = newBuf;
|
||||
if (outSize != nullptr) *outSize = newSize;
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
#include "baselib/Ascii.h"
|
||||
#include "frame/ClickEvents.h"
|
||||
#include "input/Modifier.h"
|
||||
#include "storm/Memory.h"
|
||||
#include "xml/FrameLoad.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -219,8 +220,7 @@ void LoadModifiedClicks(const char *path, void *status) {
|
||||
return;
|
||||
void *tree = reinterpret_cast<TreeLoad_t>(Offsets::FUN_XML_TREE_LOAD)(
|
||||
static_cast<const char *>(data), static_cast<uint32_t>(size));
|
||||
reinterpret_cast<AddOns::EngineIO::SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE)(data, __FILE__,
|
||||
__LINE__, 0);
|
||||
Storm::Free(data, __FILE__, __LINE__);
|
||||
if (tree == nullptr)
|
||||
return; // the engine loader reports the parse error
|
||||
const void *root = Game::Read<const void *>(tree, 0);
|
||||
|
||||
+20
-6
@@ -72,18 +72,25 @@ struct State {
|
||||
uint32_t endMs = 0;
|
||||
bool active = false; // at least one reset has happened this session
|
||||
bool dirty = false; // needs a PLAYER_SWING fire on the next WorldTick
|
||||
bool held = false; // the latest reset was recorded but not reported yet
|
||||
};
|
||||
State g_swing[3];
|
||||
|
||||
// `notify` false records the server's new timer without firing PLAYER_SWING:
|
||||
// the reset is real, but no swing is coming yet (see OnSpellGo).
|
||||
// the reset is real, but no swing is coming yet (see OnSpellGo). It stays
|
||||
// `held` until the attack start reports it or a later reported reset replaces it.
|
||||
void MarkReset(SwingType type, uint32_t durationMs, bool notify = true) {
|
||||
if (durationMs == 0)
|
||||
return; // no weapon for this hand right now -- nothing to report
|
||||
State &s = g_swing[type];
|
||||
s.endMs = NowMs() + durationMs;
|
||||
s.active = true;
|
||||
s.dirty = s.dirty || notify;
|
||||
if (notify) {
|
||||
s.dirty = true;
|
||||
s.held = false;
|
||||
} else {
|
||||
s.held = true;
|
||||
}
|
||||
}
|
||||
|
||||
using HasAttackTarget_t = int(__fastcall *)(const void *unit);
|
||||
@@ -410,11 +417,17 @@ void OnPacket(uint32_t opcode, CDataStore *packet) {
|
||||
auto *playerObj = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (InMeleeRange(playerObj, victimObj)) {
|
||||
MarkReset(OFF_HAND, AttackTimeMs(OFF_HAND));
|
||||
// A cast reset made before the attack still holds the
|
||||
// main-hand swing back; report it now that one is coming.
|
||||
// A cast reset made before the attack is still held back;
|
||||
// report it now that a swing is coming. Only a held reset:
|
||||
// a target switch mid-attack also lands here, and there the
|
||||
// main-hand swing was never reset -- re-reporting its
|
||||
// remaining time would read as a fresh, short swing.
|
||||
State &main = g_swing[MAIN_HAND];
|
||||
if (main.active && Time::Clock::Remaining(NowMs(), main.endMs) > 0)
|
||||
main.dirty = true;
|
||||
if (main.held) {
|
||||
main.held = false;
|
||||
if (Time::Clock::Remaining(NowMs(), main.endMs) > 0)
|
||||
main.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -477,6 +490,7 @@ void OnWorldTick() {
|
||||
if (!s.dirty)
|
||||
continue;
|
||||
s.dirty = false;
|
||||
s.held = false;
|
||||
if (slot < 0)
|
||||
continue;
|
||||
const uint32_t remaining = Time::Clock::Remaining(now, s.endMs);
|
||||
|
||||
@@ -618,17 +618,6 @@ uint32_t SpellSchoolMask(const uint8_t *spell) {
|
||||
: 0;
|
||||
}
|
||||
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName) {
|
||||
if (spell == nullptr)
|
||||
return false;
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
if (Game::Read<int>(spell, Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME + i * 4) ==
|
||||
auraName)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Entry Begin(uint64_t source, uint64_t dest, int event, const uint8_t *spell) {
|
||||
Entry e;
|
||||
e.event = event;
|
||||
|
||||
@@ -167,10 +167,6 @@ uint32_t SchoolMask(uint32_t school);
|
||||
// The spell's school as a mask (4.3.4's SpellRec m_schoolMask), 0 for nullptr.
|
||||
uint32_t SpellSchoolMask(const uint8_t *spell);
|
||||
|
||||
// Whether one of the spell's three effects applies the aura `auraName`
|
||||
// (EffectApplyAuraName).
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName);
|
||||
|
||||
// GetCombatLogEntry: an entry for `event` from `source` to `dest` (either may
|
||||
// be 0), with the spell prefix when `spell` is a record.
|
||||
Entry Begin(uint64_t source, uint64_t dest, int event, const uint8_t *spell);
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
#include "aura/Data.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "time/Clock.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -501,7 +502,7 @@ const uint8_t *ShieldSpell(uint64_t owner, uint32_t school) {
|
||||
const uint8_t *spell = SpellRecord(static_cast<int>(spellId));
|
||||
if (spell == nullptr ||
|
||||
Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_SCHOOL) != school ||
|
||||
!HasAuraEffect(spell, AURA_DAMAGE_SHIELD))
|
||||
!Spell::Lookup::HasAuraEffect(spell, AURA_DAMAGE_SHIELD))
|
||||
continue;
|
||||
if (found != nullptr && found != spell)
|
||||
return nullptr;
|
||||
|
||||
@@ -131,10 +131,7 @@ int __fastcall Script_RequestLoadCreatureByID(void *L) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE);
|
||||
const bool accepted =
|
||||
Cache::QueryLoad::RequestLoad(cache, static_cast<uint32_t>(creatureID));
|
||||
Game::Lua::PushBool(L, accepted);
|
||||
Game::Lua::PushBool(L, RequestLoad(static_cast<uint32_t>(creatureID)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -381,4 +378,9 @@ const uint8_t *CacheRecord(uint32_t creatureID) {
|
||||
return creatureID != 0 ? PeekCreature(creatureID) : nullptr;
|
||||
}
|
||||
|
||||
bool RequestLoad(uint32_t creatureID) {
|
||||
return Cache::QueryLoad::RequestLoad(reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE),
|
||||
creatureID);
|
||||
}
|
||||
|
||||
} // namespace Creature::Info
|
||||
|
||||
@@ -24,6 +24,12 @@ namespace Creature::Info {
|
||||
// engine's SetDisplay takes, and `PlayerModel:SetCreature` passes it there.
|
||||
const uint8_t *CacheRecord(uint32_t creatureID);
|
||||
|
||||
// Starts loading `creatureID` into the creature cache; fires
|
||||
// `CREATURE_DATA_LOAD_RESULT(creatureID, success)` when it lands (at once
|
||||
// when already cached). False only when the load could not be tracked.
|
||||
// Backs `C_CreatureInfo.RequestLoadCreatureByID`.
|
||||
bool RequestLoad(uint32_t creatureID);
|
||||
|
||||
// The faction group a player race belongs to, resolved the way
|
||||
// `Script_UnitFactionGroup` does it for a player whose object isn't in the
|
||||
// world (its name-cache branch, which reads the race): ChrRaces -> FactionTemplate
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Layout.h"
|
||||
#include "text/InlineTexture.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -59,21 +60,17 @@ using IndexAtPoint_t = void(__fastcall *)(void *editBox, void *edx, float x, flo
|
||||
using FontHeight_t = float(__fastcall *)(void *fs, void *edx, int mode);
|
||||
using MeasureRun_t = float(__fastcall *)(void *fs, void *edx, const uint8_t *text, int len);
|
||||
using PixelAlignY_t = float(__stdcall *)(float length);
|
||||
using GetRect_t = int(__fastcall *)(void *anchor, void *edx, float *out);
|
||||
using ClearAllPoints_t = void(__fastcall *)(void *anchor);
|
||||
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint, float x, float y,
|
||||
int flag);
|
||||
using Resize_t = void(__fastcall *)(void *anchor, void *edx, int force);
|
||||
using Extent_t = float(__fastcall *)(void *anchor, void *edx);
|
||||
using ScreenAspect_t = float(__cdecl *)();
|
||||
using DivideByScreenScaleX_t = float(__stdcall *)(float value);
|
||||
using FireVariadic_t = void(__cdecl *)(void *frame, uint32_t *slotPtr, const char *fmt, ...);
|
||||
|
||||
PlaceCaret_t g_origPlaceCaret = nullptr;
|
||||
PlaceSelectionPiece_t g_origPlaceSelectionPiece = nullptr;
|
||||
IndexAtPoint_t g_origIndexAtPoint = nullptr;
|
||||
|
||||
void *AnchorOf(void *region) { return static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR; }
|
||||
using Frame::Layout::Of;
|
||||
|
||||
// A multi-line box's rows: its label, the buffer the line table indexes (the
|
||||
// '*' copy of a password box) and the table — each line's first byte, the
|
||||
@@ -179,9 +176,9 @@ bool AnchorX(const Rows &rows, int line, uint32_t pos, float &x, int &relPoint)
|
||||
|
||||
// Anchors `region` as the placer did, at `y`.
|
||||
void Anchor(const Rows &rows, void *region, int relPoint, float x, float y, int flag) {
|
||||
void *anchor = AnchorOf(region);
|
||||
void *anchor = Of(region);
|
||||
reinterpret_cast<ClearAllPoints_t>(Offsets::FUN_LAYOUT_CLEAR_ALL_POINTS)(anchor);
|
||||
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, Offsets::FRAMEPOINT_TOPLEFT, AnchorOf(rows.fs),
|
||||
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, Offsets::FRAMEPOINT_TOPLEFT, Of(rows.fs),
|
||||
relPoint, x, y, flag);
|
||||
}
|
||||
|
||||
@@ -192,7 +189,7 @@ void __fastcall IndexAtPoint_h(void *editBox, void *edx, float x, float y, int *
|
||||
if (!MultiLineRows(editBox, rows))
|
||||
return g_origIndexAtPoint(editBox, edx, x, y, index);
|
||||
float rect[4] = {0.0f, 0.0f, 0.0f, 0.0f}; // bottom, left, top, right
|
||||
if (reinterpret_cast<GetRect_t>(Offsets::FUN_REGION_GET_RECT)(AnchorOf(rows.fs), nullptr, rect) == 0)
|
||||
if (!Frame::Layout::GetRect(rows.fs, rect))
|
||||
return g_origIndexAtPoint(editBox, edx, x, y, index);
|
||||
// The engine walks rows down from the label's top while the click is
|
||||
// below the row (strictly), stopping at the last line. Walk the rows as
|
||||
@@ -240,7 +237,7 @@ void __fastcall PlaceSelectionPiece_h(void *editBox, void *edx, void *piece, int
|
||||
return;
|
||||
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
|
||||
Anchor(rows, piece, relPoint, x, y, 0);
|
||||
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(AnchorOf(piece), nullptr, 1);
|
||||
Frame::Layout::Resize(piece, true);
|
||||
}
|
||||
|
||||
// --- the caret ----------------------------------------------------------------
|
||||
@@ -276,15 +273,12 @@ void __fastcall PlaceCaret_h(void *editBox) {
|
||||
return;
|
||||
// OnCursorChanged(x, y, width, height) as the engine fires it: each
|
||||
// value by the anchor→script factor, pushed as a double.
|
||||
void *anchor = AnchorOf(caret);
|
||||
void *anchor = Of(caret);
|
||||
void **vtable = *static_cast<void ***>(anchor);
|
||||
const float width = reinterpret_cast<Extent_t>(vtable[7])(anchor, nullptr); // +0x1C GetWidth
|
||||
const float height = reinterpret_cast<Extent_t>(vtable[8])(anchor, nullptr); // +0x20 GetHeight
|
||||
const auto aspect = reinterpret_cast<ScreenAspect_t>(Offsets::FUN_COORD_GET_SCALED_SCREEN_ASPECT);
|
||||
const auto divide = reinterpret_cast<DivideByScreenScaleX_t>(Offsets::FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X);
|
||||
const float factor = Game::Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE);
|
||||
const auto push = [&](float value) { return static_cast<double>(divide(aspect() * factor * value)); };
|
||||
const double args[4] = {push(x), push(y), push(width), push(height)};
|
||||
const double args[4] = {Game::InternalToPixel(x), Game::InternalToPixel(y), Game::InternalToPixel(width),
|
||||
Game::InternalToPixel(height)};
|
||||
void *L = Game::Lua::State();
|
||||
const int savedTop = (L != nullptr) ? Game::Lua::GetTop(L) : 0;
|
||||
reinterpret_cast<FireVariadic_t>(Offsets::FUN_FRAME_RUN_SCRIPT_VARIADIC)(
|
||||
|
||||
@@ -144,6 +144,17 @@ FrameXML\ColorSwatch.lua
|
||||
FrameXML\ColorSwatch.xml
|
||||
FrameXML\ScriptErrorsFrame.lua
|
||||
FrameXML\ScriptErrorsFrame.xml
|
||||
FrameXML\ModelScene\CameraRegistry.lua
|
||||
FrameXML\ModelScene\CameraBaseMixin.lua
|
||||
FrameXML\ModelScene\OrbitCameraMixin.lua
|
||||
FrameXML\ModelScene\FollowCameraMixin.lua
|
||||
FrameXML\ModelScene\ModelSceneMixin.lua
|
||||
FrameXML\ModelScene\ModelSceneMixin.xml
|
||||
FrameXML\ModelScene\ModelSceneActorMixin.lua
|
||||
FrameXML\ModelScene\ModelSceneActorMixin.xml
|
||||
FrameXML\ModelScene\ModelSceneControlFrame.lua
|
||||
FrameXML\ModelScene\ModelSceneControlFrame.xml
|
||||
FrameXML\DressUpFrame.xml
|
||||
FrameXML\DressUpFrame.lua
|
||||
|
||||
Menu\MenuConstants.lua
|
||||
|
||||
@@ -24,3 +24,22 @@ end
|
||||
function C_AutoComplete.GetAutoCompleteRealms()
|
||||
return {}
|
||||
end
|
||||
|
||||
-- C_ModelInfo's getters come from the DLL. Retail's active-scene bookkeeping
|
||||
-- "does nothing in public clients" (its own documentation).
|
||||
function C_ModelInfo.AddActiveModelScene(modelSceneFrame, modelSceneID)
|
||||
end
|
||||
|
||||
function C_ModelInfo.AddActiveModelSceneActor(modelSceneFrameActor, modelSceneActorID)
|
||||
end
|
||||
|
||||
function C_ModelInfo.ClearActiveModelScene(modelSceneFrame)
|
||||
end
|
||||
|
||||
function C_ModelInfo.ClearActiveModelSceneActor(modelSceneFrameActor)
|
||||
end
|
||||
|
||||
-- This client has no alternate player forms.
|
||||
function C_PlayerInfo.GetAlternateFormInfo()
|
||||
return false, false
|
||||
end
|
||||
|
||||
@@ -10,10 +10,203 @@
|
||||
-- - An uncached item skips the IsDressableItem test, which reads only the
|
||||
-- cache. 1.12.1 opens no tooltip before a chat link's Control-click, so
|
||||
-- the test would drop most chat links; TryOn loads the item instead.
|
||||
-- - An item the model cannot wear goes on to DressUpMountLink, retail's
|
||||
-- next step in DressUpLink: 1.12.1's FrameXML (ItemRef, bags, loot, the
|
||||
-- auction house) calls DressUpItemLink alone.
|
||||
-- - It returns true when it showed the item or its mount, and false for a
|
||||
-- link of another type, as retail's does.
|
||||
|
||||
function DressUpItemLink(link)
|
||||
if ( not link or (C_Item.IsItemDataCachedByID(link) and not IsDressableItem(link)) ) then
|
||||
return;
|
||||
-- A link DressUpItemLink takes: an item link, or anything that is not a
|
||||
-- link (an item ID or name).
|
||||
local function IsItemLink(link)
|
||||
local linkType = type(link) == "string" and LinkUtil.ExtractLink(link);
|
||||
return not linkType or linkType == "item";
|
||||
end
|
||||
|
||||
-- 4.3.4's DressUpItemLink body: the item, when the model can wear it.
|
||||
local function DressUpWearable(link)
|
||||
if ( C_Item.IsItemDataCachedByID(link) and not IsDressableItem(link) ) then
|
||||
return false;
|
||||
end
|
||||
DressUpItem(link);
|
||||
return true;
|
||||
end
|
||||
|
||||
function DressUpItemLink(link)
|
||||
if ( not link or not IsItemLink(link) ) then
|
||||
return false;
|
||||
end
|
||||
return DressUpWearable(link) or DressUpMountLink(link);
|
||||
end
|
||||
|
||||
-- Retail's DressUpLink (Mainline/DressUpFrames.lua), without battle pets.
|
||||
function DressUpLink(link, forcedFrame)
|
||||
return link and ((IsItemLink(link) and DressUpWearable(link)) or DressUpMountLink(link, forcedFrame));
|
||||
end
|
||||
|
||||
-- Retail's mount preview (Mainline/DressUpFrames.lua DressUpMountLink and
|
||||
-- DressUpMount, Blizzard_SharedXMLGame/DressUpModelFrameMixin.lua's mode and
|
||||
-- last link) on 1.12.1's dressing room. Mount mode shows DressUpFrameModelScene
|
||||
-- in place of DressUpModel and hides the Reset button, as retail's SetMode
|
||||
-- hides it outside player mode; DressUpItem goes back to player mode.
|
||||
--
|
||||
-- Differences from the retail source:
|
||||
-- - DressUpFrame keeps its own background (retail switches to the warrior
|
||||
-- one) and has no view insets to set (retail sets them to 0, the default).
|
||||
-- - There are no mount links, so a link never sets the model itself, and
|
||||
-- shouldSetModelFromHyperlink changes nothing.
|
||||
-- - A mount whose creature is not cached yet returns false, as retail does
|
||||
-- with no display, and opens when CREATURE_DATA_LOAD_RESULT reports the
|
||||
-- creature: retail has every display on disk, this client asks the server.
|
||||
|
||||
DressUpFrame.ModelScene = DressUpFrameModelScene;
|
||||
DressUpFrame.ModelScene.ControlFrame:SetModelScene(DressUpFrame.ModelScene);
|
||||
|
||||
function DressUpFrame:GetLastLink()
|
||||
return self.lastLink;
|
||||
end
|
||||
|
||||
function DressUpFrame:SetLastLink(link)
|
||||
self.lastLink = link;
|
||||
end
|
||||
|
||||
function DressUpFrame:GetMode()
|
||||
return self.mode;
|
||||
end
|
||||
|
||||
function DressUpFrame:SetMode(mode)
|
||||
self.mode = mode;
|
||||
local inPlayerMode = mode ~= "mount";
|
||||
DressUpModel:SetShown(inPlayerMode);
|
||||
DressUpFrameResetButton:SetShown(inPlayerMode);
|
||||
self.ModelScene:SetShown(not inPlayerMode);
|
||||
end
|
||||
|
||||
DressUpFrame.ModelScene:SetResetCallback(function()
|
||||
local link = DressUpFrame:GetLastLink();
|
||||
if link then
|
||||
DressUpLink(link, DressUpFrame);
|
||||
end
|
||||
end);
|
||||
|
||||
-- The stock DressUpItem sets the unit only when it opens the window, and the
|
||||
-- model was hidden while a mount showed.
|
||||
local originalDressUpItem = DressUpItem;
|
||||
function DressUpItem(item)
|
||||
if ( DressUpFrame:GetMode() == "mount" ) then
|
||||
DressUpFrame:SetMode("player");
|
||||
DressUpFrame:SetLastLink(nil);
|
||||
if ( DressUpFrame:IsVisible() ) then
|
||||
DressUpModel:SetUnit("player");
|
||||
end
|
||||
end
|
||||
return originalDressUpItem(item);
|
||||
end
|
||||
|
||||
function DressUpMountLink(link, forcedFrame)
|
||||
if( link ) then
|
||||
local mountID = 0;
|
||||
|
||||
local linkType, linkOptions = LinkUtil.ExtractLink(link);
|
||||
if linkType == "item" then
|
||||
local linkID = LinkUtil.SplitLinkOptions(linkOptions);
|
||||
mountID = C_MountJournal.GetMountFromItem(tonumber(linkID));
|
||||
elseif linkType == "spell" then
|
||||
local linkID = LinkUtil.SplitLinkOptions(linkOptions);
|
||||
mountID = C_MountJournal.GetMountFromSpell(tonumber(linkID));
|
||||
end
|
||||
|
||||
if ( mountID ) then
|
||||
return DressUpMount(mountID, forcedFrame, false, link);
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local pendingMount;
|
||||
|
||||
-- The creature GetMountInfoExtraByID loads for a mount: a mount ID is its
|
||||
-- spell ID, and the mount aura's misc value is the creature.
|
||||
local SPELL_AURA_MOUNTED = 78;
|
||||
local function MountCreatureID(mountID)
|
||||
local effects = C_Spell.GetSpellEffectInfo(mountID);
|
||||
if ( effects ) then
|
||||
for _, effect in ipairs(effects) do
|
||||
if ( effect.auraName == SPELL_AURA_MOUNTED ) then
|
||||
return effect.miscValue;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function DressUpMount(mountID, forcedFrame, shouldSetModelFromHyperlink, link)
|
||||
if ( not mountID or mountID == 0 ) then
|
||||
return false;
|
||||
end
|
||||
|
||||
local creatureDisplayID, _, _, isSelfMount, _, modelSceneID, animID, spellVisualKitID, disablePlayerMountPreview = C_MountJournal.GetMountInfoExtraByID(mountID);
|
||||
if ( not creatureDisplayID ) then
|
||||
pendingMount = { mountID = mountID, creatureID = MountCreatureID(mountID), forcedFrame = forcedFrame, link = link };
|
||||
return false;
|
||||
end
|
||||
pendingMount = nil;
|
||||
|
||||
local frame = forcedFrame or DressUpFrame;
|
||||
|
||||
--Show the frame
|
||||
if ( not frame:IsShown() or frame:GetMode() ~= "mount" ) then
|
||||
frame:SetMode("mount");
|
||||
ShowUIPanel(frame);
|
||||
end
|
||||
frame:SetLastLink(link);
|
||||
frame.ModelScene:ClearScene();
|
||||
local forceEvenIfSame = true;
|
||||
frame.ModelScene:TransitionToModelSceneID(modelSceneID, CAMERA_TRANSITION_TYPE_IMMEDIATE, CAMERA_MODIFICATION_TYPE_DISCARD, forceEvenIfSame);
|
||||
|
||||
local mountActor = frame.ModelScene:GetActorByTag("unwrapped");
|
||||
if mountActor then
|
||||
mountActor:SetModelByCreatureDisplayID(creatureDisplayID);
|
||||
|
||||
-- mount self idle animation
|
||||
if (isSelfMount) then
|
||||
mountActor:SetAnimationBlendOperation(Enum.ModelBlendOperation.None);
|
||||
mountActor:SetAnimation(618); -- MountSelfIdle
|
||||
else
|
||||
mountActor:SetAnimationBlendOperation(Enum.ModelBlendOperation.Anim);
|
||||
mountActor:SetAnimation(0);
|
||||
end
|
||||
frame.ModelScene:AttachPlayerToMount(mountActor, animID, isSelfMount, disablePlayerMountPreview);
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
local creatureLoadFrame = CreateFrame("Frame");
|
||||
creatureLoadFrame:RegisterEvent("CREATURE_DATA_LOAD_RESULT");
|
||||
creatureLoadFrame:SetScript("OnEvent", function(self, event, creatureID, success)
|
||||
local pending = pendingMount;
|
||||
if ( not pending or creatureID ~= pending.creatureID ) then
|
||||
return;
|
||||
end
|
||||
pendingMount = nil;
|
||||
if ( success ) then
|
||||
DressUpMount(pending.mountID, pending.forcedFrame, false, pending.link);
|
||||
end
|
||||
end);
|
||||
|
||||
-- Control-click on a mount in the spellbook previews it. Retail has no such
|
||||
-- click (its mounts are in the mount journal); this client keeps mounts in
|
||||
-- the spellbook, where stock 1.12.1 casts on a Control-click. A mount spell
|
||||
-- never casts here, even while its creature loads; any other spell does.
|
||||
local originalSpellButtonOnClick = SpellButton_OnClick;
|
||||
function SpellButton_OnClick(drag)
|
||||
if ( not drag and IsControlKeyDown() and SpellBookFrame.bookType ~= BOOKTYPE_PET ) then
|
||||
local id = SpellBook_GetSpellID(this:GetID());
|
||||
local link = id <= MAX_SPELLS and GetSpellLink(id, SpellBookFrame.bookType);
|
||||
if ( link and C_MountJournal.GetMountFromSpell(link) ) then
|
||||
this:SetChecked("false");
|
||||
DressUpMountLink(link);
|
||||
return;
|
||||
end
|
||||
end
|
||||
return originalSpellButtonOnClick(drag);
|
||||
end
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<!-- The dressing room's mount mode (DressUpFrame.lua): a scene shown in
|
||||
place of DressUpModel, with retail's control frame. Like retail's, it
|
||||
fills the backdrop above the buttons: the four DressUpBackground
|
||||
textures, cut off at the top of the Reset / Close row (centred 422
|
||||
below the frame top, 22 tall, so 48 above the backdrop's bottom).
|
||||
DressUpModel's own rect is no guide: it is a scale 2 frame that
|
||||
reaches past the backdrop and over the buttons. -->
|
||||
<ModelScene name="DressUpFrameModelScene" parent="DressUpFrame" inherits="ModelSceneMixinTemplate" hidden="true">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="DressUpBackgroundTopLeft"/>
|
||||
<Anchor point="BOTTOMRIGHT" relativeTo="DressUpBackgroundBotRight">
|
||||
<Offset>
|
||||
<AbsDimension x="0" y="48"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<Frame parentKey="ControlFrame" inherits="ModelSceneControlFrameTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" x="0" y="-10"/>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
</Frames>
|
||||
</ModelScene>
|
||||
</Ui>
|
||||
@@ -0,0 +1,103 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/ModelSceneCameras/CameraBaseMixin.lua.
|
||||
|
||||
|
||||
CameraBaseMixin = {};
|
||||
|
||||
function CameraBaseMixin:SetOwningScene(owningScene)
|
||||
if self.owningScene ~= owningScene then
|
||||
if self.owningScene then
|
||||
self:OnRemoved();
|
||||
end
|
||||
|
||||
self.owningScene = owningScene;
|
||||
|
||||
if self.owningScene then
|
||||
self:OnAdded();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CameraBaseMixin:GetOwningScene()
|
||||
return self.owningScene;
|
||||
end
|
||||
|
||||
CAMERA_TRANSITION_TYPE_IMMEDIATE = 1; -- Switch to the camera view instantly
|
||||
CAMERA_TRANSITION_TYPE_INTERPOLATION = 2; -- Switch to the camera view by interpolating from the current view
|
||||
|
||||
CAMERA_MODIFICATION_TYPE_DISCARD = 1; -- Discard any modifications performed to the view by script
|
||||
CAMERA_MODIFICATION_TYPE_MAINTAIN = 2; -- Retain any modifications performed to the view by script
|
||||
|
||||
-- Override this to handle transitions to a modelSceneCamera data set
|
||||
function CameraBaseMixin:ApplyFromModelSceneCameraInfo(modelSceneCameraInfo, transitionType, modificationType)
|
||||
|
||||
end
|
||||
|
||||
-- Common accessors from the owning scene
|
||||
function CameraBaseMixin:IsLeftMouseButtonDown()
|
||||
return self:GetOwningScene():IsLeftMouseButtonDown();
|
||||
end
|
||||
|
||||
function CameraBaseMixin:IsRightMouseButtonDown()
|
||||
return self:GetOwningScene():IsRightMouseButtonDown();
|
||||
end
|
||||
|
||||
function CameraBaseMixin:SetPosition(x, y, z)
|
||||
self:GetOwningScene():SetCameraPosition(x, y, z);
|
||||
end
|
||||
|
||||
function CameraBaseMixin:GetPosition()
|
||||
return self:GetOwningScene():GetCameraPosition();
|
||||
end
|
||||
|
||||
function CameraBaseMixin:GetForwardVector()
|
||||
return self:GetOwningScene():GetCameraForward();
|
||||
end
|
||||
|
||||
function CameraBaseMixin:GetRightVector()
|
||||
return self:GetOwningScene():GetCameraRight();
|
||||
end
|
||||
|
||||
function CameraBaseMixin:GetUpVector()
|
||||
return self:GetOwningScene():GetCameraUp();
|
||||
end
|
||||
|
||||
-- Override this with a uniquely identifying name for the camera type
|
||||
function CameraBaseMixin:GetCameraType()
|
||||
return "UnknownCamera";
|
||||
end
|
||||
|
||||
-- Override this to perform any per-frame updates
|
||||
function CameraBaseMixin:OnUpdate(elapsed)
|
||||
end
|
||||
|
||||
-- Override this with code to synchronize the camera state with the model scene API, may be called by the ModelScene to synchronize deferred camera state in case it needs to be queried
|
||||
function CameraBaseMixin:SynchronizeCamera()
|
||||
end
|
||||
|
||||
-- Override this to be notified when the mouse is down on the model scene
|
||||
function CameraBaseMixin:OnMouseDown(button)
|
||||
end
|
||||
|
||||
-- Override this to be notified when the mouse is up on the model scene
|
||||
function CameraBaseMixin:OnMouseUp(button)
|
||||
end
|
||||
|
||||
-- Override this to be notified when the mouse wheel is changed on the model scene
|
||||
function CameraBaseMixin:OnMouseWheel(delta)
|
||||
end
|
||||
|
||||
-- Override this to be notified when the camera is added to a scene
|
||||
function CameraBaseMixin:OnAdded()
|
||||
end
|
||||
|
||||
-- Override this to be notified when the camera is remove from a scene
|
||||
function CameraBaseMixin:OnRemoved()
|
||||
end
|
||||
|
||||
-- Override this to be notified when the camera becomes activated
|
||||
function CameraBaseMixin:OnActivated()
|
||||
end
|
||||
|
||||
-- Override this to be notified when the camera becomes deactivated
|
||||
function CameraBaseMixin:OnDeactivated()
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/ModelSceneCameras/CameraRegistry.lua.
|
||||
|
||||
|
||||
CameraRegistry = { cameraTypeToFactoryFunction = {}, };
|
||||
|
||||
function CameraRegistry:AddCameraFactory(cameraTypeName, factoryFunction)
|
||||
self.cameraTypeToFactoryFunction[cameraTypeName] = factoryFunction;
|
||||
end
|
||||
|
||||
function CameraRegistry:AddCameraFactoryFromMixin(cameraTypeName, mixin)
|
||||
self:AddCameraFactory(cameraTypeName, function() return CreateFromMixins(mixin); end);
|
||||
end
|
||||
|
||||
function CameraRegistry:CreateCameraByType(cameraTypeName)
|
||||
if self.cameraTypeToFactoryFunction[cameraTypeName] then
|
||||
return self.cameraTypeToFactoryFunction[cameraTypeName]();
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,133 @@
|
||||
-- ClassicAPI extension: an orbit camera that follows an actor and stops in
|
||||
-- front of the WMO between the actor and the eye.
|
||||
|
||||
--[[
|
||||
FollowCameraMixin - An OrbitCamera whose target is an actor's pivot: its position, raised by the pivot height.
|
||||
|
||||
The pivot height is the one the world's follow camera eases to for a unit (actor:GetCameraPivotHeight), and
|
||||
it eases the same way: the camera's first height is taken at once (BeginHeightTransition 0x00512790), and
|
||||
each new one is armed (ArmHeight 0x005126B0) for |change| / cameraHeightSmoothSpeed seconds and eased with
|
||||
the camera's cosine curve (0x0050F160). Until the actor's model has loaded, the camera waits, as the world's
|
||||
does (0x0050E900).
|
||||
|
||||
The eye is placed the way the world's follow camera places it (the boom sweep of its solver 0x0050E570):
|
||||
a trace of the camera's faces from the pivot to the eye (0x0050E816) shortens the distance, which then
|
||||
backs off 1/9 yard (0x0050E871 to 0x0050E8A4). With no actor to follow, it is a plain OrbitCamera.
|
||||
|
||||
Create it beside the scene's own camera and give it that camera's settings:
|
||||
local _, cameraIDs = C_ModelInfo.GetModelSceneInfoByID(modelSceneID);
|
||||
local camera = scene:AddCamera(CameraRegistry:CreateCameraByType("FollowCamera"));
|
||||
camera:ApplyFromModelSceneCameraInfo(C_ModelInfo.GetModelSceneCameraInfoByID(cameraIDs[1]), CAMERA_TRANSITION_TYPE_IMMEDIATE, CAMERA_MODIFICATION_TYPE_DISCARD);
|
||||
camera:SetFollowActor(actor);
|
||||
scene:SetActiveCamera(camera);
|
||||
]]
|
||||
|
||||
FollowCameraMixin = CreateFromMixins(OrbitCameraMixin);
|
||||
|
||||
local CAMERA_NAME = "FollowCamera";
|
||||
CameraRegistry:AddCameraFactoryFromMixin(CAMERA_NAME, FollowCameraMixin);
|
||||
|
||||
-- The solver skips the sweep when the distance is not past the near clip by
|
||||
-- more than this (0x008026BC, 2^-20).
|
||||
local SWEEP_MIN_DISTANCE = 9.5367431640625e-07;
|
||||
-- A distance shortened by more than this (0x008029D4, 2^-22) backs off by
|
||||
-- BOOM_BACKOFF (0x00808A00, 1/9 yard), to no less than 0.
|
||||
local SHORTENED_EPSILON = 2.384185791015625e-07;
|
||||
local BOOM_BACKOFF = 1 / 9;
|
||||
-- A height within this of the armed or the live one arms nothing (0x00801360).
|
||||
local ARM_EPSILON = 0.001;
|
||||
-- The ease runs while the live height is this far or more from its target
|
||||
-- (0x008029D4, 2^-22).
|
||||
local EASE_EPSILON = 2.384185791015625e-07;
|
||||
|
||||
-- "public" functions
|
||||
function FollowCameraMixin:GetCameraType() -- override
|
||||
return CAMERA_NAME;
|
||||
end
|
||||
|
||||
-- The target becomes the actor's pivot. Nil stops following.
|
||||
function FollowCameraMixin:SetFollowActor(actor)
|
||||
self.followActor = actor;
|
||||
end
|
||||
|
||||
function FollowCameraMixin:GetFollowActor()
|
||||
return self.followActor;
|
||||
end
|
||||
|
||||
-- "private" functions
|
||||
|
||||
-- ArmHeight (0x005126B0) with no delay and a factor of 1, and its inner
|
||||
-- BeginHeightTransition (0x00512790).
|
||||
function FollowCameraMixin:ArmPivotHeight(height, now)
|
||||
local live = self.pivotHeight or 0;
|
||||
if self.pivotEasing and math.abs(self.pivotTarget - height) < ARM_EPSILON then
|
||||
return;
|
||||
end
|
||||
if math.abs(live - height) < ARM_EPSILON then
|
||||
return;
|
||||
end
|
||||
self.pivotTarget = height;
|
||||
if not self.pivotSnapped then
|
||||
self.pivotSnapped = true;
|
||||
self.pivotHeight = height;
|
||||
self.pivotEasing = false;
|
||||
return;
|
||||
end
|
||||
self.pivotFrom = live;
|
||||
self.pivotStart = now;
|
||||
self.pivotDuration = math.abs(height - live) / tonumber(GetCVar("cameraHeightSmoothSpeed"));
|
||||
self.pivotEasing = true;
|
||||
end
|
||||
|
||||
-- The height channel of the camera's ease (0x0050F160).
|
||||
function FollowCameraMixin:EasePivotHeight(now)
|
||||
if math.abs(self.pivotTarget - self.pivotHeight) < EASE_EPSILON then
|
||||
self.pivotEasing = false;
|
||||
return;
|
||||
end
|
||||
if not self.pivotEasing or self.pivotStart > now then
|
||||
return;
|
||||
end
|
||||
local t = (now - self.pivotStart) / self.pivotDuration;
|
||||
if t >= 1 then
|
||||
self.pivotHeight = self.pivotTarget;
|
||||
else
|
||||
self.pivotHeight = self.pivotFrom + (self.pivotTarget - self.pivotFrom) * (1 - math.cos(math.pi * t)) * 0.5;
|
||||
end
|
||||
end
|
||||
|
||||
function FollowCameraMixin:OnUpdate(elapsed) -- override
|
||||
local actor = self.followActor;
|
||||
if actor then
|
||||
local height = actor:GetCameraPivotHeight(self:GetZoomDistance());
|
||||
if not height then
|
||||
return;
|
||||
end
|
||||
local now = GetTime();
|
||||
self:ArmPivotHeight(height, now);
|
||||
self:EasePivotHeight(now);
|
||||
local x, y, z = actor:GetPosition();
|
||||
self:SetTarget(x, y, z + self.pivotHeight);
|
||||
end
|
||||
OrbitCameraMixin.OnUpdate(self, elapsed);
|
||||
end
|
||||
|
||||
function FollowCameraMixin:CalculatePositionByDistanceFromTarget(targetX, targetY, targetZ, zoomDistance, axisAngleX, axisAngleY, axisAngleZ) -- override
|
||||
if not self.followActor then
|
||||
return OrbitCameraMixin.CalculatePositionByDistanceFromTarget(self, targetX, targetY, targetZ, zoomDistance, axisAngleX, axisAngleY, axisAngleZ);
|
||||
end
|
||||
|
||||
local scene = self:GetOwningScene();
|
||||
local distance = zoomDistance;
|
||||
if distance - scene:GetCameraNearClip() > SWEEP_MIN_DISTANCE then
|
||||
local eyeX, eyeY, eyeZ = OrbitCameraMixin.CalculatePositionByDistanceFromTarget(self, targetX, targetY, targetZ, distance, axisAngleX, axisAngleY, axisAngleZ);
|
||||
local _, _, _, _, _, _, fraction = scene:TraceSegment(targetX, targetY, targetZ, eyeX, eyeY, eyeZ, true);
|
||||
if fraction then
|
||||
distance = distance * fraction;
|
||||
end
|
||||
if math.abs(zoomDistance - distance) > SHORTENED_EPSILON then
|
||||
distance = math.max(distance - BOOM_BACKOFF, 0);
|
||||
end
|
||||
end
|
||||
return OrbitCameraMixin.CalculatePositionByDistanceFromTarget(self, targetX, targetY, targetZ, distance, axisAngleX, axisAngleY, axisAngleZ);
|
||||
end
|
||||
@@ -0,0 +1,144 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/ModelSceneActorMixin.lua.
|
||||
|
||||
|
||||
ModelSceneActorMixin = {};
|
||||
|
||||
-- "public" methods
|
||||
function ModelSceneActorMixin:ApplyFromModelSceneActorInfo(actorInfo)
|
||||
self:SetUseCenterForOrigin(actorInfo.useCenterForOriginX, actorInfo.useCenterForOriginY, actorInfo.useCenterForOriginZ);
|
||||
|
||||
self:SetPosition(actorInfo.position:GetXYZ());
|
||||
self:SetYaw(actorInfo.yaw);
|
||||
self:SetPitch(actorInfo.pitch);
|
||||
self:SetRoll(actorInfo.roll);
|
||||
|
||||
self.requestedScale = nil;
|
||||
|
||||
local actorDisplayInfo = actorInfo.modelActorDisplayID and C_ModelInfo.GetModelSceneActorDisplayInfoByID(actorInfo.modelActorDisplayID);
|
||||
if actorDisplayInfo then
|
||||
if actorDisplayInfo.animationKitID then
|
||||
self:PlayAnimationKit(actorDisplayInfo.animationKitID);
|
||||
else
|
||||
self:SetAnimation(actorDisplayInfo.animation, actorDisplayInfo.animationVariation, actorDisplayInfo.animSpeed);
|
||||
end
|
||||
|
||||
self:SetSpellVisualKit(actorDisplayInfo.spellVisualKitID);
|
||||
|
||||
self:SetAlpha(actorDisplayInfo.alpha);
|
||||
self:SetRequestedScale(actorDisplayInfo.scale);
|
||||
else
|
||||
self:StopAnimationKit();
|
||||
self:SetSpellVisualKit(nil);
|
||||
self:SetAnimation(0, 0, 1.0);
|
||||
self:SetAlpha(1.0);
|
||||
self:SetRequestedScale(1.0);
|
||||
end
|
||||
|
||||
self:SetNormalizedScaleAggressiveness(actorInfo.normalizeScaleAggressiveness or 0.0);
|
||||
|
||||
self:MarkScaleDirty();
|
||||
self:UpdateScale();
|
||||
|
||||
C_ModelInfo.AddActiveModelSceneActor(self, actorInfo.modelActorID);
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:OnModelCleared()
|
||||
C_ModelInfo.ClearActiveModelSceneActor(self);
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:SetOnSizeChangedCallback(onSizeChangedCallback)
|
||||
self.onSizeChangedCallback = onSizeChangedCallback;
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:GetOnSizeChangedCallback()
|
||||
return self.onSizeChangedCallback;
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:SetOnModelLoadedCallback(onModelLoadedCallback)
|
||||
self.onModelLoadedCallback = onModelLoadedCallback;
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:GetOnModelLoadedCallback()
|
||||
return self.onModelLoadedCallback;
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:SetNormalizedScaleAggressiveness(normalizedScaleAggressiveness)
|
||||
if self.normalizedScaleAggressiveness ~= normalizedScaleAggressiveness then
|
||||
self.normalizedScaleAggressiveness = normalizedScaleAggressiveness;
|
||||
self:MarkScaleDirty();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:GetNormalizedScaleAggressiveness()
|
||||
return self.normalizedScaleAggressiveness or 0;
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:SetRequestedScale(requestedScale)
|
||||
if self.requestedScale ~= requestedScale then
|
||||
self.requestedScale = requestedScale;
|
||||
self:MarkScaleDirty();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:GetRequestedScale()
|
||||
return self.requestedScale or self:GetScale();
|
||||
end
|
||||
|
||||
MODEL_SCENE_ACTOR_DIMENSIONS_FOR_NORMALIZATION = { width = .9984, depth = 1.121, height = 2.118 }; -- Roughly the dimensions of a human male model (but why male models?)
|
||||
function ModelSceneActorMixin:CalculateNormalizedScale(normalizedScaleAggressiveness)
|
||||
if self:IsLoaded() and normalizedScaleAggressiveness > 0.0 then
|
||||
local bottomX, bottomY, bottomZ, topX, topY, topZ = self:GetActiveBoundingBox(); -- Could be nil for invisible models
|
||||
if bottomX and bottomY and bottomZ and topX and topY and topZ then
|
||||
local width = topX - bottomX;
|
||||
local depth = topY - bottomY;
|
||||
local height = topZ - bottomZ;
|
||||
|
||||
local widthScale = width / MODEL_SCENE_ACTOR_DIMENSIONS_FOR_NORMALIZATION.width;
|
||||
local depthScale = depth / MODEL_SCENE_ACTOR_DIMENSIONS_FOR_NORMALIZATION.depth;
|
||||
local heightScale = height / MODEL_SCENE_ACTOR_DIMENSIONS_FOR_NORMALIZATION.height;
|
||||
|
||||
local maxScale = math.max(widthScale, math.max(depthScale, heightScale));
|
||||
if maxScale == 0 then
|
||||
return 1.0;
|
||||
end
|
||||
|
||||
return 1 / Lerp(1.0, maxScale, normalizedScaleAggressiveness);
|
||||
end
|
||||
end
|
||||
|
||||
return 1.0;
|
||||
end
|
||||
|
||||
-- "private" methods
|
||||
function ModelSceneActorMixin:OnReleased()
|
||||
self:SetOnSizeChangedCallback(nil);
|
||||
self:SetOnModelLoadedCallback(nil);
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:OnUpdate()
|
||||
self:UpdateScale();
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:OnModelLoaded()
|
||||
self:MarkScaleDirty();
|
||||
|
||||
if self.onModelLoadedCallback then
|
||||
self.onModelLoadedCallback(self);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:UpdateScale()
|
||||
if self.scaleDirty and self:IsLoaded() then
|
||||
self.scaleDirty = nil;
|
||||
local effectiveScale = self:GetRequestedScale() * self:CalculateNormalizedScale(self:GetNormalizedScaleAggressiveness());
|
||||
self:SetScale(effectiveScale);
|
||||
|
||||
if self.onSizeChangedCallback then
|
||||
self.onSizeChangedCallback(self);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:MarkScaleDirty()
|
||||
self.scaleDirty = true;
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
UI.xsd">
|
||||
<Actor name="ModelSceneActorTemplate" mixin="ModelSceneActorMixin" virtual="true">
|
||||
<Scripts>
|
||||
<OnModelCleared method="OnModelCleared" />
|
||||
<OnModelLoaded method="OnModelLoaded" />
|
||||
<OnUpdate method="OnUpdate" />
|
||||
</Scripts>
|
||||
</Actor>
|
||||
</Ui>
|
||||
@@ -0,0 +1,282 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/ModelSceneControlFrame.lua.
|
||||
|
||||
|
||||
local DEFAULT_ZOOM_INCREMENT = 1;
|
||||
local DEFAULT_ROTATE_INCREMENT = 0.05;
|
||||
|
||||
--------------------------------------------------
|
||||
-- MODEL SCENE CONTROL FRAME MIXIN
|
||||
ModelSceneControlFrameMixin = {};
|
||||
function ModelSceneControlFrameMixin:OnLoad()
|
||||
if self.enableZoom then
|
||||
local increment = self:GetZoomIncrement();
|
||||
self.zoomInButton:SetZoomAmount(increment);
|
||||
self.zoomInButton:Init();
|
||||
|
||||
self.zoomOutButton:SetZoomAmount(-increment);
|
||||
self.zoomOutButton:Init();
|
||||
end
|
||||
self.zoomInButton:SetShown(self.enableZoom);
|
||||
self.zoomOutButton:SetShown(self.enableZoom);
|
||||
|
||||
if self.enableRotate then
|
||||
local increment = self:GetRotateIncrement();
|
||||
self.rotateLeftButton:SetRotation("left");
|
||||
self.rotateLeftButton:SetRotationIncrement(increment);
|
||||
self.rotateLeftButton:Init();
|
||||
|
||||
self.rotateRightButton:SetRotation("right");
|
||||
self.rotateRightButton:SetRotationIncrement(increment);
|
||||
self.rotateRightButton:Init();
|
||||
end
|
||||
self.rotateLeftButton:SetShown(self.enableRotate);
|
||||
self.rotateRightButton:SetShown(self.enableRotate);
|
||||
|
||||
if self.enableReset then
|
||||
self.resetButton:Init();
|
||||
end
|
||||
self.resetButton:SetShown(self.enableReset);
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:OnShow()
|
||||
self:UpdateLayout();
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:OnEnter()
|
||||
self:SetAlpha(1);
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:OnLeave()
|
||||
self:SetAlpha(0.5);
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:SetZoomIncrement(increment)
|
||||
self.zoomIncrement = increment or DEFAULT_ZOOM_INCREMENT;
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:GetZoomIncrement()
|
||||
return self.zoomIncrement or DEFAULT_ZOOM_INCREMENT;
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:SetRotateIncrement(increment)
|
||||
self.rotationIncrement = increment or DEFAULT_ROTATE_INCREMENT;
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:GetRotateIncrement()
|
||||
return self.rotationIncrement or DEFAULT_ROTATE_INCREMENT;
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:SetModelScene(modelScene)
|
||||
self.modelScene = modelScene;
|
||||
|
||||
self.zoomInButton:SetModelScene(modelScene);
|
||||
self.zoomOutButton:SetModelScene(modelScene);
|
||||
self.rotateLeftButton:SetModelScene(modelScene);
|
||||
self.rotateRightButton:SetModelScene(modelScene);
|
||||
self.resetButton:SetModelScene(modelScene);
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:UpdateLayout()
|
||||
local lastButton;
|
||||
local hPadding = self.buttonHorizontalPadding or 0;
|
||||
local vPadding = 0;
|
||||
local buttonSize = 32;
|
||||
local totalHeight = buttonSize;
|
||||
local totalWidth = 2; -- Initial non-zero total width to prevent flickering from negataive padding
|
||||
|
||||
local function LayoutButton(button)
|
||||
button:ClearAllPoints();
|
||||
if lastButton then
|
||||
button:SetPoint("LEFT", lastButton, "RIGHT", hPadding, vPadding);
|
||||
else
|
||||
button:SetPoint("LEFT", 0, vPadding);
|
||||
end
|
||||
totalWidth = totalWidth + buttonSize + hPadding;
|
||||
lastButton = button;
|
||||
end
|
||||
|
||||
if self.enableZoom then
|
||||
-- Make sure that zoom is supported on the model scene.
|
||||
if self.modelScene then
|
||||
local camera = self.modelScene:GetActiveCamera();
|
||||
self.canZoom = camera ~= nil and camera:GetZoomAvailable();
|
||||
self.zoomInButton:SetShown(self.canZoom);
|
||||
self.zoomOutButton:SetShown(self.canZoom);
|
||||
end
|
||||
|
||||
if self.canZoom then
|
||||
LayoutButton(self.zoomInButton);
|
||||
LayoutButton(self.zoomOutButton);
|
||||
end
|
||||
end
|
||||
|
||||
if self.enableRotate then
|
||||
LayoutButton(self.rotateLeftButton);
|
||||
LayoutButton(self.rotateRightButton);
|
||||
end
|
||||
|
||||
if self.enableReset then
|
||||
LayoutButton(self.resetButton);
|
||||
end
|
||||
self:SetSize(totalWidth, totalHeight);
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- MODEL SCENE CONTROL BUTTON MIXIN
|
||||
ModelSceneControlButtonMixin = {};
|
||||
function ModelSceneControlButtonMixin:Init(clickTypes, atlas, tooltip, tooltipText)
|
||||
self:RegisterForClicks(clickTypes);
|
||||
if atlas then
|
||||
self.Icon:SetAtlas(atlas);
|
||||
end
|
||||
self.tooltip = tooltip;
|
||||
self.tooltipText = tooltipText;
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:SetModelScene(modelScene)
|
||||
self.modelScene = modelScene;
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:OnMouseDown()
|
||||
self.Icon:AdjustPointsOffset(1, -1);
|
||||
self:GetParent().buttonDown = self;
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:OnMouseUp()
|
||||
self.Icon:AdjustPointsOffset(-1, 1);
|
||||
self:GetParent().buttonDown = nil;
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:OnClick()
|
||||
--override for your mouse click
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:OnEnter()
|
||||
self:GetParent():SetAlpha(1);
|
||||
if ( GetCVar("UberTooltips") == "1" ) then
|
||||
local tooltip = GetAppropriateTooltip();
|
||||
local topLevelParent = GetAppropriateTopLevelParent();
|
||||
GameTooltip_SetDefaultAnchor(tooltip, topLevelParent);
|
||||
GameTooltip_SetTitle(tooltip, self.tooltip);
|
||||
if ( self.tooltipText ) then
|
||||
GameTooltip_AddBodyLine(tooltip, self.tooltipText);
|
||||
end
|
||||
tooltip:Show();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:OnLeave()
|
||||
self:GetParent():SetAlpha(0.5);
|
||||
|
||||
local tooltip = GetAppropriateTooltip();
|
||||
tooltip:Hide();
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- MODEL ZOOM BUTTON MIXIN
|
||||
ModelSceneZoomButtonMixin = CreateFromMixins(ModelSceneControlButtonMixin);
|
||||
function ModelSceneZoomButtonMixin:OnLoad()
|
||||
self.zoomAmount = 0;
|
||||
end
|
||||
|
||||
function ModelSceneZoomButtonMixin:Init()
|
||||
if self.zoomAmount < 0 then
|
||||
ModelSceneControlButtonMixin.Init(self, "AnyUp", "common-icon-zoomout", ZOOM_OUT, KEY_MOUSEWHEELDOWN);
|
||||
else
|
||||
ModelSceneControlButtonMixin.Init(self, "AnyUp", "common-icon-zoomin", ZOOM_IN, KEY_MOUSEWHEELUP);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneZoomButtonMixin:SetZoomAmount(amountToZoom)
|
||||
self.zoomAmount = amountToZoom;
|
||||
end
|
||||
|
||||
function ModelSceneZoomButtonMixin:OnClick()
|
||||
if self.modelScene then
|
||||
self.modelScene:OnMouseWheel(self.zoomAmount);
|
||||
else
|
||||
assertsafe(fale, "Model Scene not specified");
|
||||
end
|
||||
PlaySound(SOUNDKIT.IG_INVENTORY_ROTATE_CHARACTER);
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- MODEL SCENE ROTATE BUTTON MIXIN
|
||||
ModelScenelRotateButtonMixin = CreateFromMixins(ModelSceneControlButtonMixin);
|
||||
function ModelScenelRotateButtonMixin:OnLoad()
|
||||
ModelSceneControlButtonMixin.OnLoad(self);
|
||||
self.rotateDirection = "none";
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:Init()
|
||||
if (self.rotateDirection == "left") then
|
||||
ModelSceneControlButtonMixin.Init(self, "AnyUp", "common-icon-rotateleft", ROTATE_LEFT, ROTATE_TOOLTIP);
|
||||
elseif (self.rotateDirection == "right") then
|
||||
ModelSceneControlButtonMixin.Init(self, "AnyUp", "common-icon-rotateright", ROTATE_RIGHT, ROTATE_TOOLTIP);
|
||||
else
|
||||
assertsafe(false, "Invalid rotation specified: "..tostring(self.rotateDirection));
|
||||
end
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:SetRotation(rotation)
|
||||
self.rotateDirection = rotation;
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:SetRotationIncrement(increment)
|
||||
self.rotationIncrement = increment or 0.05;
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:OnMouseDown()
|
||||
ModelSceneControlButtonMixin.OnMouseDown(self);
|
||||
if self.modelScene then
|
||||
self.modelScene:AdjustCameraYaw(self.rotateDirection, self.rotationIncrement);
|
||||
else
|
||||
assertsafe(false, "Model Scene not specified");
|
||||
end
|
||||
PlaySound(SOUNDKIT.IG_INVENTORY_ROTATE_CHARACTER);
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:OnMouseUp()
|
||||
ModelSceneControlButtonMixin.OnMouseUp(self);
|
||||
if self.modelScene then
|
||||
self.modelScene:StopCameraYaw();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:OnSmartNavClick()
|
||||
self:RotateStep();
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:RotateStep()
|
||||
local camera = self.modelScene and self.modelScene:GetActiveCamera();
|
||||
if not camera then
|
||||
return;
|
||||
end
|
||||
|
||||
if self.rotateDirection == "left" then
|
||||
camera:AdjustYaw(-1, -1, 0.25);
|
||||
else
|
||||
camera:AdjustYaw(1, 1, 0.25);
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- MODEL SCENE RESET BUTTON MIXIN
|
||||
ModelSceneResetButtonMixin = CreateFromMixins(ModelSceneControlButtonMixin);
|
||||
function ModelSceneResetButtonMixin:OnLoad()
|
||||
ModelSceneControlButtonMixin.OnLoad(self);
|
||||
end
|
||||
|
||||
function ModelSceneResetButtonMixin:Init()
|
||||
local tooltipText = nil;
|
||||
ModelSceneControlButtonMixin.Init(self, "AnyUp", "common-icon-undo", RESET_POSITION, tooltipText);
|
||||
end
|
||||
|
||||
function ModelSceneResetButtonMixin:OnClick()
|
||||
if self.modelScene then
|
||||
self.modelScene:Reset();
|
||||
else
|
||||
assertsafe(false, "Model Scene not specified");
|
||||
end
|
||||
PlaySound(SOUNDKIT.IG_INVENTORY_ROTATE_CHARACTER);
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
UI.xsd">
|
||||
<!-- Retail's Blizzard_SharedXML/ModelSceneControlFrame.xml. The one change:
|
||||
HitRectInsets takes this client's <AbsInset> child. The atlases are
|
||||
built in (src/texture/AtlasData.h, EMBEDDED ART). -->
|
||||
<Button name="ModelSceneControlButtonTemplate" mixin="ModelSceneControlButtonMixin" virtual="true">
|
||||
<Size x="32" y="32"/>
|
||||
<HitRectInsets>
|
||||
<AbsInset left="4" right="4" top="4" bottom="4"/>
|
||||
</HitRectInsets>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture parentKey="Icon" >
|
||||
<Size x="16" y="16"/>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
<!--<Texture parentKey="Icon" setAllPoints="true" />-->
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnMouseDown method="OnMouseDown" />
|
||||
<OnMouseUp method="OnMouseUp" />
|
||||
<OnClick method="OnClick" />
|
||||
<OnEnter method="OnEnter" />
|
||||
<OnLeave method="OnLeave" />
|
||||
</Scripts>
|
||||
<NormalTexture parentKey="NormalTexture" atlas="common-button-square-gray-up"/>
|
||||
<HighlightTexture parentKey="HighlightTexture" alphaMode="ADD" alpha="0.4">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeKey="$parent.Icon" relativePoint="TOPLEFT"/>
|
||||
<Anchor point="BOTTOMRIGHT" relativeKey="$parent.Icon" relativePoint="BOTTOMRIGHT"/>
|
||||
</Anchors>
|
||||
</HighlightTexture>
|
||||
<PushedTexture parentKey="PushedTexture" atlas="common-button-square-gray-down">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="1" y="-1"/>
|
||||
<Anchor point="BOTTOMRIGHT" x="1" y="-1"/>
|
||||
</Anchors>
|
||||
</PushedTexture>
|
||||
</Button>
|
||||
<Frame name="ModelSceneControlFrameTemplate" virtual="true" mixin="ModelSceneControlFrameMixin" alpha="0.5" hidden="true">
|
||||
<Size x="32" y="32"/>
|
||||
<KeyValues>
|
||||
<KeyValue key="enableZoom" value="true" type="boolean" />
|
||||
<KeyValue key="enableRotate" value="true" type="boolean" />
|
||||
<KeyValue key="enableReset" value="true" type="boolean" />
|
||||
<KeyValue key="rotationIncrement" value="0.05" type="number" />
|
||||
<KeyValue key="zoomIncrement" value="1" type="number" />
|
||||
<KeyValue key="buttonHorizontalPadding" value="-6" type="number" />
|
||||
</KeyValues>
|
||||
<Frames>
|
||||
<Button parentKey="zoomInButton" hidden="true" mixin="ModelSceneZoomButtonMixin" inherits="ModelSceneControlButtonTemplate"/>
|
||||
<Button parentKey="zoomOutButton" hidden="true" mixin="ModelSceneZoomButtonMixin" inherits="ModelSceneControlButtonTemplate"/>
|
||||
<Button parentKey="rotateLeftButton" hidden="true" mixin="ModelScenelRotateButtonMixin" inherits="ModelSceneControlButtonTemplate"/>
|
||||
<Button parentKey="rotateRightButton" hidden="true" mixin="ModelScenelRotateButtonMixin" inherits="ModelSceneControlButtonTemplate"/>
|
||||
<Button parentKey="resetButton" hidden="true" mixin="ModelSceneResetButtonMixin" inherits="ModelSceneControlButtonTemplate"/>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad" />
|
||||
<OnShow method="OnShow" />
|
||||
<OnEnter method="OnEnter" />
|
||||
<OnLeave method="OnLeave" />
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,571 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/ModelSceneMixin.lua, verbatim except where
|
||||
-- marked "ClassicAPI:".
|
||||
|
||||
|
||||
ModelSceneMixin = {}
|
||||
|
||||
-- "public" functions
|
||||
function ModelSceneMixin:OnLoad()
|
||||
self.cameras = {};
|
||||
self.actorTemplate = "ModelSceneActorTemplate";
|
||||
self.tagToActor = {};
|
||||
self.tagToCamera = {};
|
||||
|
||||
if self.allowOverlappedModels then
|
||||
self:SetAllowOverlappedModels(true);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:ClearScene()
|
||||
self.modelSceneID = nil;
|
||||
|
||||
self:ReleaseAllActors();
|
||||
self:ReleaseAllCameras();
|
||||
|
||||
C_ModelInfo.ClearActiveModelScene(self);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetModelSceneID()
|
||||
return self.modelSceneID;
|
||||
end
|
||||
|
||||
-- Adjusts this scene to mirror a model scene from static data without transition
|
||||
function ModelSceneMixin:SetFromModelSceneID(modelSceneID, forceEvenIfSame, noAutoCreateActors)
|
||||
local modelSceneType, cameraIDs, actorIDs = C_ModelInfo.GetModelSceneInfoByID(modelSceneID);
|
||||
if not modelSceneType then
|
||||
return;
|
||||
end
|
||||
|
||||
if self.modelSceneID ~= modelSceneID or forceEvenIfSame then
|
||||
self.modelSceneID = modelSceneID;
|
||||
self:ReleaseAllActors();
|
||||
self:ReleaseAllCameras();
|
||||
|
||||
if not noAutoCreateActors then
|
||||
for actorIndex, actorID in ipairs(actorIDs) do
|
||||
self:CreateActorFromScene(actorID);
|
||||
end
|
||||
end
|
||||
|
||||
for cameraIndex, cameraID in ipairs(cameraIDs) do
|
||||
self:CreateCameraFromScene(cameraID);
|
||||
end
|
||||
end
|
||||
|
||||
C_ModelInfo.AddActiveModelScene(self, self.modelSceneID);
|
||||
EventRegistry:TriggerEvent("ModelScene.SetFromModelSceneID", self, self.modelSceneID);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:SetResetCallback(callback)
|
||||
self.resetCallback = callback;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:Reset()
|
||||
if self.modelSceneID then
|
||||
self:TransitionToModelSceneID(self.modelSceneID, self.cameraTransitionType, self.cameraModificationType, self.forceEvenIfSame);
|
||||
if self.resetCallback then
|
||||
self.resetCallback(self);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Adjusts this scene to mirror a model scene from static data but with transition effects
|
||||
-- Only actors/cameras with script tags will be transitioned
|
||||
function ModelSceneMixin:TransitionToModelSceneID(modelSceneID, cameraTransitionType, cameraModificationType, forceEvenIfSame)
|
||||
local modelSceneType, cameraIDs, actorIDs = C_ModelInfo.GetModelSceneInfoByID(modelSceneID);
|
||||
if not modelSceneType or #cameraIDs == 0 or #actorIDs == 0 then
|
||||
return;
|
||||
end
|
||||
|
||||
if self.modelSceneID ~= modelSceneID or forceEvenIfSame then
|
||||
self.modelSceneID = modelSceneID;
|
||||
self.cameraTransitionType = cameraTransitionType;
|
||||
self.cameraModificationType = cameraModificationType;
|
||||
self.forceEvenIfSame = forceEvenIfSame;
|
||||
|
||||
local actorsToRelease = {};
|
||||
for actor in self:EnumerateActiveActors() do
|
||||
actorsToRelease[actor] = true;
|
||||
end
|
||||
|
||||
local oldTagToActor = self.tagToActor;
|
||||
self.tagToActor = {};
|
||||
|
||||
for actorIndex, actorID in ipairs(actorIDs) do
|
||||
local actor = self:CreateOrTransitionActorFromScene(oldTagToActor, actorID);
|
||||
if actor then
|
||||
actorsToRelease[actor] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
for actor in pairs(actorsToRelease) do
|
||||
self.actorPool:Release(actor);
|
||||
end
|
||||
|
||||
local oldTagToCamera = self.tagToCamera;
|
||||
self.tagToCamera = {};
|
||||
|
||||
self.cameras = {};
|
||||
|
||||
local needsNewCamera = true;
|
||||
for cameraIndex, cameraID in ipairs(cameraIDs) do
|
||||
local camera = self:CreateOrTransitionCameraFromScene(oldTagToCamera, cameraTransitionType, cameraModificationType, cameraID);
|
||||
if camera == self.activeCamera then
|
||||
needsNewCamera = false;
|
||||
end
|
||||
end
|
||||
|
||||
if needsNewCamera then
|
||||
self:SetActiveCamera(self.cameras[1]);
|
||||
end
|
||||
end
|
||||
|
||||
C_ModelInfo.AddActiveModelScene(self, self.modelSceneID);
|
||||
EventRegistry:TriggerEvent("ModelScene.TransitionToModelSceneID", self, self.modelSceneID);
|
||||
end
|
||||
|
||||
-- There may be inactive (pooled) actors maintained by this scene, these function only returns the active actors
|
||||
function ModelSceneMixin:GetNumActiveActors()
|
||||
return self.actorPool and self.actorPool:GetNumActive() or 0;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:EnumerateActiveActors()
|
||||
if self.actorPool then
|
||||
return self.actorPool:EnumerateActive();
|
||||
end
|
||||
return nop;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetActorByTag(tag)
|
||||
return self.tagToActor[tag];
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AttachPlayerToMount(mountActor, animID, isSelfMount, disablePlayerMountPreview, spellVisualKitID, usePlayerNativeForm, optionalPlayerRiderTag)
|
||||
local playerRiderTag = optionalPlayerRiderTag or "player-rider";
|
||||
local playerActor = self:GetPlayerActor(playerRiderTag);
|
||||
if (playerActor) then
|
||||
if disablePlayerMountPreview or isSelfMount then
|
||||
playerActor:ClearModel();
|
||||
else
|
||||
local sheatheWeapons = true;
|
||||
local autoDress = true;
|
||||
local hideWeapons = true;
|
||||
|
||||
if C_Glue.IsOnGlueScreen() then
|
||||
local characterIndex = nil; -- defaults to selected character.
|
||||
playerActor:SetPlayerModelFromGlues(characterIndex, sheatheWeapons, autoDress, hideWeapons, usePlayerNativeForm);
|
||||
else
|
||||
playerActor:SetModelByUnit("player", sheatheWeapons, autoDress, hideWeapons, usePlayerNativeForm);
|
||||
end
|
||||
local calcMountScale = mountActor:CalculateMountScale(playerActor);
|
||||
local inverseScale = 1 / calcMountScale;
|
||||
playerActor:SetRequestedScale( inverseScale );
|
||||
mountActor:AttachToMount(playerActor, animID, spellVisualKitID);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function GetPlayerActorLabelTag(forceAlternateForm, overrideRaceFilename, overrideGender)
|
||||
local playerRaceName = overrideRaceFilename;
|
||||
local playerGender = overrideGender;
|
||||
local playerRaceNameActorTag = nil;
|
||||
local hasAlternateForm, inAlternateForm = false, false;
|
||||
if not overrideRaceFilename or not overrideGender then
|
||||
if C_Glue.IsOnGlueScreen() then
|
||||
local characterGuid = GetCharacterGUID(GetCharacterSelection());
|
||||
if characterGuid then
|
||||
local basicCharacterInfo = GetBasicCharacterInfo(characterGuid);
|
||||
playerRaceName = overrideRaceFilename or basicCharacterInfo.raceFilename;
|
||||
playerGender = overrideGender or basicCharacterInfo.genderEnum;
|
||||
end
|
||||
else
|
||||
hasAlternateForm, inAlternateForm = C_PlayerInfo.GetAlternateFormInfo();
|
||||
local _, raceFilename = UnitRace("player");
|
||||
playerRaceName = overrideRaceFilename or raceFilename;
|
||||
playerGender = overrideGender or UnitSex("player");
|
||||
end
|
||||
end
|
||||
if not playerRaceName or not playerGender then
|
||||
return playerRaceName, playerRaceNameActorTag;
|
||||
end
|
||||
playerRaceName = playerRaceName:lower();
|
||||
if hasAlternateForm and (inAlternateForm or forceAlternateForm) then
|
||||
playerRaceName = playerRaceName.."-alt";
|
||||
end
|
||||
playerGender = (playerGender == 2) and "male" or "female";
|
||||
playerRaceNameActorTag = playerRaceName.."-"..playerGender;
|
||||
return playerRaceName, playerRaceNameActorTag;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetPlayerActor(overrideActorName, forceAlternateForm, overrideRaceFilename, overrideGender)
|
||||
local actor;
|
||||
if overrideActorName then
|
||||
actor = self:GetActorByTag(overrideActorName);
|
||||
else
|
||||
local playerRaceName, playerRaceNameActorTag = GetPlayerActorLabelTag(forceAlternateForm, overrideRaceFilename, overrideGender);
|
||||
actor = self:GetActorByTag(playerRaceNameActorTag);
|
||||
if not actor then
|
||||
actor = self:GetActorByTag(playerRaceName);
|
||||
if not actor then
|
||||
actor = self:GetActorByTag("player");
|
||||
if not actor then
|
||||
actor = self:GetActorByTag("player-rider");
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return actor;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:ReleaseAllActors()
|
||||
if self.actorPool then
|
||||
self.actorPool:ReleaseAll();
|
||||
|
||||
if self.dropShadowPool then
|
||||
self.dropShadowPool:ReleaseAll();
|
||||
end
|
||||
|
||||
self.tagToActor = {};
|
||||
end
|
||||
end
|
||||
|
||||
local function OnReleased(actorPool, actor)
|
||||
actor:OnReleased();
|
||||
ActorPool_HideAndClearModel(actorPool, actor);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AcquireActor()
|
||||
if not self.actorPool then
|
||||
self.actorPool = CreateActorPool(self, self.actorTemplate, OnReleased);
|
||||
end
|
||||
return self.actorPool:Acquire();
|
||||
end
|
||||
|
||||
function ModelSceneMixin:ReleaseActor(actor)
|
||||
if not actor or not self.actorPool then
|
||||
return;
|
||||
end
|
||||
|
||||
for tag, taggedActor in pairs(self.tagToActor) do
|
||||
if actor == taggedActor then
|
||||
self.tagToActor[tag] = nil;
|
||||
break;
|
||||
end
|
||||
end
|
||||
|
||||
if actor.dropShadowTexture then
|
||||
self.dropShadowPool:Release(actor.dropShadowTexture);
|
||||
end
|
||||
|
||||
return self.actorPool:Release(actor);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AcquireAndInitializeActor(actorInfo)
|
||||
local actor = self:AcquireActor();
|
||||
self:InitializeActor(actor, actorInfo);
|
||||
return actor;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:ReleaseAllCameras()
|
||||
self:SetActiveCamera(nil);
|
||||
for i = #self.cameras, 1, -1 do
|
||||
self.cameras[i]:SetOwningScene(nil);
|
||||
self.cameras[i] = nil;
|
||||
end
|
||||
self.tagToCamera = {};
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetCameraByTag(tag)
|
||||
return self.tagToCamera[tag];
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AddCamera(camera)
|
||||
table.insert(self.cameras, camera);
|
||||
|
||||
camera:SetOwningScene(self);
|
||||
|
||||
if not self:HasActiveCamera() then
|
||||
self:SetActiveCamera(camera);
|
||||
end
|
||||
|
||||
return camera;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:HasActiveCamera()
|
||||
return self.activeCamera ~= nil;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetActiveCamera()
|
||||
return self.activeCamera;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:SetActiveCamera(camera)
|
||||
if camera ~= self.activeCamera then
|
||||
if self.activeCamera then
|
||||
self.activeCamera:OnDeactivated();
|
||||
end
|
||||
|
||||
self.activeCamera = camera;
|
||||
|
||||
if self.activeCamera then
|
||||
self.activeCamera:OnActivated();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:IsLeftMouseButtonDown()
|
||||
return self.isLeftButtonDown;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:IsRightMouseButtonDown()
|
||||
return self.isRightButtonDown;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:Transform3DPointTo2D(x, y, z)
|
||||
self:SynchronizeActiveCamera();
|
||||
return self:Project3DPointTo2D(x, y, z);
|
||||
end
|
||||
|
||||
-- "private" functions
|
||||
function ModelSceneMixin:OnUpdate(elapsed)
|
||||
if self.activeCamera then
|
||||
local yawDirection = self.yawDirection;
|
||||
-- ClassicAPI: retail turns by the increment every frame, which spins
|
||||
-- several times faster at this client's uncapped frame rates. The
|
||||
-- increment counts per 1/60 s here: 0.05 is 3 rad/s, the speed of
|
||||
-- 1.12's own rotate buttons (ROTATIONS_PER_SECOND, pi rad/s).
|
||||
local increment = self.increment and self.increment * elapsed * 60;
|
||||
if yawDirection == "left" then
|
||||
self.activeCamera:AdjustYaw(-1, -1, increment);
|
||||
elseif yawDirection == "right" then
|
||||
self.activeCamera:AdjustYaw(1, 1, increment);
|
||||
end
|
||||
|
||||
self.activeCamera:OnUpdate(elapsed);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:SynchronizeActiveCamera()
|
||||
if self.activeCamera then
|
||||
self.activeCamera:SynchronizeCamera();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:OnEnter(button)
|
||||
if self.ControlFrame then
|
||||
self.ControlFrame:Show();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:OnLeave(button)
|
||||
if self.ControlFrame then
|
||||
if not self.ControlFrame:IsMouseOver()then
|
||||
self.ControlFrame:Hide();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:OnMouseDown(button)
|
||||
if button == "LeftButton" then
|
||||
self.isLeftButtonDown = true;
|
||||
elseif button == "RightButton" then
|
||||
self.isRightButtonDown = true;
|
||||
end
|
||||
|
||||
if self.activeCamera then
|
||||
self.activeCamera:OnMouseDown(button);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:OnMouseUp(button)
|
||||
if button == "LeftButton" then
|
||||
self.isLeftButtonDown = false;
|
||||
elseif button == "RightButton" then
|
||||
self.isRightButtonDown = false;
|
||||
end
|
||||
|
||||
if self.activeCamera then
|
||||
self.activeCamera:OnMouseUp(button);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:OnMouseWheel(delta)
|
||||
if self.activeCamera then
|
||||
self.activeCamera:OnMouseWheel(delta);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AdjustCameraYaw(direction, increment)
|
||||
if self.activeCamera then
|
||||
self.yawDirection = direction;
|
||||
self.increment = increment;
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:StopCameraYaw()
|
||||
self.yawDirection = nil;
|
||||
self.increment = nil;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:InitializeActor(actor, actorInfo)
|
||||
if actorInfo.scriptTag then
|
||||
self.tagToActor[actorInfo.scriptTag] = actor;
|
||||
end
|
||||
|
||||
actor:ApplyFromModelSceneActorInfo(actorInfo);
|
||||
|
||||
actor:Show();
|
||||
end
|
||||
|
||||
function ModelSceneMixin:CreateActorFromScene(actorID)
|
||||
local actorInfo = C_ModelInfo.GetModelSceneActorInfoByID(actorID);
|
||||
return self:AcquireAndInitializeActor(actorInfo);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:CreateOrTransitionActorFromScene(oldTagToActor, actorID)
|
||||
local actorInfo = C_ModelInfo.GetModelSceneActorInfoByID(actorID);
|
||||
local existingActor = oldTagToActor[actorInfo.scriptTag];
|
||||
if existingActor then
|
||||
self:InitializeActor(existingActor, actorInfo);
|
||||
return existingActor;
|
||||
end
|
||||
|
||||
return self:AcquireAndInitializeActor(actorInfo);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:CreateCameraFromScene(modelSceneCameraID)
|
||||
local modelSceneCameraInfo = C_ModelInfo.GetModelSceneCameraInfoByID(modelSceneCameraID);
|
||||
if modelSceneCameraInfo then
|
||||
local camera = CameraRegistry:CreateCameraByType(modelSceneCameraInfo.cameraType);
|
||||
if camera then
|
||||
if modelSceneCameraInfo.scriptTag then
|
||||
self.tagToCamera[modelSceneCameraInfo.scriptTag] = camera;
|
||||
end
|
||||
self:AddCamera(camera);
|
||||
camera:ApplyFromModelSceneCameraInfo(modelSceneCameraInfo, CAMERA_TRANSITION_TYPE_IMMEDIATE, CAMERA_MODIFICATION_TYPE_DISCARD);
|
||||
return camera;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:CreateOrTransitionCameraFromScene(oldTagToCamera, cameraTransitionType, cameraModificationType, modelSceneCameraID)
|
||||
local modelSceneCameraInfo = C_ModelInfo.GetModelSceneCameraInfoByID(modelSceneCameraID);
|
||||
if modelSceneCameraInfo then
|
||||
local existingCamera = oldTagToCamera[modelSceneCameraInfo.scriptTag];
|
||||
if existingCamera and existingCamera:GetCameraType() == modelSceneCameraInfo.cameraType then
|
||||
self.tagToCamera[modelSceneCameraInfo.scriptTag] = existingCamera;
|
||||
|
||||
self:AddCamera(existingCamera);
|
||||
existingCamera:ApplyFromModelSceneCameraInfo(modelSceneCameraInfo, cameraTransitionType, cameraModificationType);
|
||||
return existingCamera;
|
||||
end
|
||||
|
||||
return self:CreateCameraFromScene(modelSceneCameraID);
|
||||
end
|
||||
end
|
||||
|
||||
-- actorSettings = {
|
||||
-- "actorTag" = { startDelay = 0, duration = 0, speed = 1 } -- duration 0 means no automatic stoppage
|
||||
-- }
|
||||
function ModelSceneMixin:ShowAndAnimateActors(actorSettings, onFinishedCallback)
|
||||
self:Show();
|
||||
local totalTime = 0;
|
||||
for actorTag, actorInfo in pairs(actorSettings) do
|
||||
local actor = self:GetActorByTag(actorTag);
|
||||
if actor then
|
||||
local runningTime = actorInfo.startDelay + actorInfo.duration;
|
||||
if actorInfo.startDelay > 0 then
|
||||
actor:SetAnimation(0, 0, 0, 0);
|
||||
C_Timer.After(actorInfo.startDelay,
|
||||
function()
|
||||
actor:SetAnimation(0, 0, actorInfo.speed, 0);
|
||||
end
|
||||
);
|
||||
else
|
||||
actor:SetAnimation(0, 0, actorInfo.speed, 0);
|
||||
end
|
||||
if actorInfo.duration > 0 then
|
||||
C_Timer.After(runningTime,
|
||||
function()
|
||||
actor:SetAnimation(0, 0, 0, 0);
|
||||
end
|
||||
);
|
||||
end
|
||||
if runningTime > totalTime then
|
||||
totalTime = runningTime;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if onFinishedCallback and totalTime > 0 then
|
||||
C_Timer.After(totalTime, onFinishedCallback);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetOrAcquireDropShadow(actor)
|
||||
if actor.dropShadowTexture then
|
||||
return actor.dropShadowTexture;
|
||||
end
|
||||
|
||||
if not self.dropShadowPool then
|
||||
self.dropShadowPool = CreateTexturePool(self, "BORDER", 1, "ModelSceneDropShadowTemplate");
|
||||
end
|
||||
|
||||
actor.dropShadowTexture = self.dropShadowPool:Acquire();
|
||||
return actor.dropShadowTexture;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AddOrUpdateDropShadow(actor, baseShadowScale)
|
||||
baseShadowScale = baseShadowScale or 1.0;
|
||||
|
||||
local dropShadowTexture = self:GetOrAcquireDropShadow(actor);
|
||||
local positionVector = CreateVector3D(actor:GetPosition());
|
||||
positionVector:ScaleBy(actor:GetScale());
|
||||
local x, y, depthScale = self:Transform3DPointTo2D(positionVector:GetXYZ());
|
||||
|
||||
if not x or not y or not depthScale then
|
||||
dropShadowTexture:Hide();
|
||||
return;
|
||||
end
|
||||
|
||||
dropShadowTexture:ClearAllPoints();
|
||||
depthScale = baseShadowScale * Lerp(.05, 1, ClampedPercentageBetween(depthScale, .8, 1))
|
||||
-- Scales down the texture depending on it's depthScale.
|
||||
dropShadowTexture:SetScale(depthScale);
|
||||
|
||||
-- Need to apply the effective scale to account for UI Scaling.
|
||||
local inverseScale = self:GetEffectiveScale() * depthScale;
|
||||
-- The position of the character can be found by the offset on the screen.
|
||||
dropShadowTexture:SetPoint("CENTER", self, "BOTTOMLEFT", (x / inverseScale) + 2, (y / inverseScale) - 4);
|
||||
dropShadowTexture:Show();
|
||||
end
|
||||
|
||||
|
||||
PanningModelSceneMixin = CreateFromMixins(ModelSceneMixin);
|
||||
function PanningModelSceneMixin:TransitionToModelSceneID(modelSceneID, cameraTransitionType, cameraModificationType, forceEvenIfSame)
|
||||
ModelSceneMixin.TransitionToModelSceneID(self, modelSceneID, cameraTransitionType, cameraModificationType, forceEvenIfSame);
|
||||
|
||||
local camera = self:GetActiveCamera();
|
||||
if camera then
|
||||
camera:SetRightMouseButtonXMode(ORBIT_CAMERA_MOUSE_PAN_HORIZONTAL, true);
|
||||
camera:SetRightMouseButtonYMode(ORBIT_CAMERA_MOUSE_PAN_VERTICAL, true);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
NoCameraControlModelSceneMixin = CreateFromMixins(ModelSceneMixin);
|
||||
function NoCameraControlModelSceneMixin:OnMouseDown(button)
|
||||
self.isLeftButtonDown = false;
|
||||
self.isRightButtonDown = false;
|
||||
end
|
||||
|
||||
function NoCameraControlModelSceneMixin:OnMouseUp(button)
|
||||
self.isLeftButtonDown = false;
|
||||
self.isRightButtonDown = false;
|
||||
end
|
||||
|
||||
function NoCameraControlModelSceneMixin:OnMouseWheel(delta)
|
||||
end
|
||||
NoZoomModelSceneMixin = CreateFromMixins(ModelSceneMixin);
|
||||
function NoZoomModelSceneMixin:OnMouseWheel(delta)
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
UI.xsd">
|
||||
<ModelScene name="NonInteractableModelSceneMixinTemplate" mixin="ModelSceneMixin" virtual="true">
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad" />
|
||||
<OnUpdate method="OnUpdate" />
|
||||
</Scripts>
|
||||
</ModelScene>
|
||||
<ModelScene name="ModelSceneMixinTemplate" inherits="NonInteractableModelSceneMixinTemplate" virtual="true">
|
||||
<Scripts>
|
||||
<OnEnter method="OnEnter"/>
|
||||
<OnLeave method="OnLeave"/>
|
||||
<OnMouseUp method="OnMouseUp"/>
|
||||
<OnMouseDown method="OnMouseDown"/>
|
||||
<OnMouseWheel method="OnMouseWheel" />
|
||||
</Scripts>
|
||||
</ModelScene>
|
||||
<ModelScene name="StoreModelSceneMixinTemplate" inherits="ModelSceneMixinTemplate" virtual="true">
|
||||
<KeyValues>
|
||||
<!-- CLASS-44103: Automatically allow overlapped models for shop scenes -->
|
||||
<KeyValue key="allowOverlappedModels" value="true" type="boolean"/>
|
||||
</KeyValues>
|
||||
</ModelScene>
|
||||
<ModelScene name="NoZoomModelSceneMixinTemplate" inherits="ModelSceneMixinTemplate" mixin="NoZoomModelSceneMixin" virtual="true"/>
|
||||
<ModelScene name="PanningModelSceneMixinTemplate" inherits="ModelSceneMixinTemplate" mixin="PanningModelSceneMixin" virtual="true" />
|
||||
<ModelScene name="NoCameraControlModelSceneMixinTemplate" inherits="ModelSceneMixinTemplate" mixin="NoCameraControlModelSceneMixin" virtual="true" />
|
||||
<Texture name="ModelSceneDropShadowTemplate" atlas="scoreboard-charactermodels-shadow" useAtlasSize="true" virtual="true"/>
|
||||
</Ui>
|
||||
@@ -0,0 +1,640 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/ModelSceneCameras/OrbitCameraMixin.lua.
|
||||
|
||||
--[[
|
||||
OrbitCameraMixin - A simple camera that can orbit a target point at a fixed distance.
|
||||
|
||||
The orbit is represented using a distance from a target point in world space and yaw, pitch and roll.
|
||||
The yaw, pitch and roll represent the camera's orientation, or inversely the direction of the camera from the target point.
|
||||
|
||||
Optionally, the target, orientation and zoom can have splines that are added to the respective axes. The splines are fed the zoom percentage to calculate the point on the spline.
|
||||
This is ideal for changing view points based on zoom distance from the target. For example, target model's face on zoom in and center on the model on zoom out.
|
||||
]]
|
||||
|
||||
OrbitCameraMixin = CreateFromMixins(CameraBaseMixin);
|
||||
|
||||
local CAMERA_NAME = "OrbitCamera";
|
||||
CameraRegistry:AddCameraFactoryFromMixin(CAMERA_NAME, OrbitCameraMixin);
|
||||
|
||||
-- "public" functions
|
||||
function OrbitCameraMixin:GetCameraType() -- override
|
||||
return CAMERA_NAME;
|
||||
end
|
||||
|
||||
local function TryCreateZoomSpline(x, y, z, existingSpline)
|
||||
if x and y and z and (x ~= 0 or y ~= 0 or z ~= 0) then
|
||||
local spline = existingSpline or CreateCatmullRomSpline(3);
|
||||
spline:ClearPoints();
|
||||
spline:AddPoint(0, 0, 0);
|
||||
spline:AddPoint(x, y, z);
|
||||
|
||||
return spline;
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:ApplyFromModelSceneCameraInfo(modelSceneCameraInfo, transitionType, modificationType) -- override
|
||||
self.panningXOffset = 0;
|
||||
self.panningYOffset = 0;
|
||||
|
||||
local transitionalCameraInfo = self:CalculateTransitionalValues(self.modelSceneCameraInfo, modelSceneCameraInfo, modificationType);
|
||||
|
||||
self.modelSceneCameraInfo = modelSceneCameraInfo;
|
||||
|
||||
self:SetTarget(transitionalCameraInfo.target:GetXYZ());
|
||||
self:SetTargetSpline(TryCreateZoomSpline(transitionalCameraInfo.zoomedTargetOffset:GetXYZ()), self:GetTargetSpline());
|
||||
self:SetOrientationSpline(TryCreateZoomSpline(transitionalCameraInfo.zoomedYawOffset, transitionalCameraInfo.zoomedPitchOffset, transitionalCameraInfo.zoomedRollOffset), self:GetOrientationSpline());
|
||||
|
||||
self:SetMinZoomDistance(transitionalCameraInfo.minZoomDistance);
|
||||
self:SetMaxZoomDistance(transitionalCameraInfo.maxZoomDistance);
|
||||
|
||||
self:SetZoomDistance(transitionalCameraInfo.zoomDistance);
|
||||
|
||||
self:SetYaw(transitionalCameraInfo.yaw);
|
||||
self:SetPitch(transitionalCameraInfo.pitch);
|
||||
self:SetRoll(transitionalCameraInfo.roll);
|
||||
|
||||
if transitionType == CAMERA_TRANSITION_TYPE_IMMEDIATE then
|
||||
self:SnapAllInterpolatedValues();
|
||||
end
|
||||
self:UpdateCameraOrientationAndPosition();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetTarget(x, y, z)
|
||||
self.targetX, self.targetY, self.targetZ = x, y, z;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetTarget(x, y, z)
|
||||
return self.targetX, self.targetY, self.targetZ;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetYaw(yaw)
|
||||
self.yaw = yaw;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetYaw()
|
||||
return self.yaw;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetPitch(pitch)
|
||||
self.pitch = pitch;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetPitch()
|
||||
return self.pitch;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetRoll(roll)
|
||||
self.roll = roll;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetRoll()
|
||||
return self.roll;
|
||||
end
|
||||
|
||||
-- where 100% is fully zoomed out
|
||||
function OrbitCameraMixin:GetZoomPercent()
|
||||
return self.zoomPercent;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetZoomPercent(zoomPercent)
|
||||
self.zoomPercent = Saturate(zoomPercent);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:ZoomByPercent(percent)
|
||||
self:SetZoomPercent(self:GetZoomPercent() - percent);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetMaxZoomDistance(distance)
|
||||
self.maxZoomDistance = distance;
|
||||
if self:GetZoomPercent() then
|
||||
self:SetZoomDistance(self:GetZoomDistance());
|
||||
end
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetMaxZoomDistance()
|
||||
return self.maxZoomDistance;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetMinZoomDistance(distance)
|
||||
self.minZoomDistance = distance;
|
||||
if self:GetZoomPercent() then
|
||||
self:SetZoomDistance(self:GetZoomDistance());
|
||||
end
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetMinZoomDistance()
|
||||
return self.minZoomDistance;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetZoomDistance(distance)
|
||||
self:SetZoomPercent(self:CalculateZoomPercentFromDistance(distance));
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:CalculateZoomPercentFromDistance(distance)
|
||||
if self:GetMinZoomDistance() and self:GetMaxZoomDistance() then
|
||||
return PercentageBetween(distance, self:GetMinZoomDistance(), self:GetMaxZoomDistance());
|
||||
end
|
||||
return 0.0;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:CalculateZoomDistanceFromPercent(percent)
|
||||
if self:GetMinZoomDistance() and self:GetMaxZoomDistance() then
|
||||
return Lerp(self:GetMinZoomDistance(), self:GetMaxZoomDistance(), percent);
|
||||
end
|
||||
return 0.0;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetZoomDistance()
|
||||
return self:CalculateZoomDistanceFromPercent(self:GetZoomPercent());
|
||||
end
|
||||
|
||||
-- If min and max values are the same, zoom cannot really happen.
|
||||
function OrbitCameraMixin:GetZoomAvailable()
|
||||
if self:GetMinZoomDistance() and self:GetMaxZoomDistance() then
|
||||
return self:GetMinZoomDistance() ~= self:GetMaxZoomDistance();
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:ZoomBy(distance)
|
||||
self:SetZoomDistance(self:GetZoomDistance() - distance);
|
||||
end
|
||||
|
||||
-- Expects a three dimensional spline where a point of the curve represents a target position
|
||||
function OrbitCameraMixin:SetTargetSpline(targetSpline)
|
||||
self.targetSpline = targetSpline;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetTargetSpline()
|
||||
return self.targetSpline;
|
||||
end
|
||||
|
||||
-- Expects a three dimensional spline where a point of the curve represents the camera yaw, pitch, roll
|
||||
function OrbitCameraMixin:SetOrientationSpline(orientationSpline)
|
||||
self.orientationSpline = orientationSpline;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetOrientationSpline()
|
||||
return self.orientationSpline;
|
||||
end
|
||||
|
||||
-- Expects an one dimensional spline where a point of the curve represents the camera's distance from the target
|
||||
function OrbitCameraMixin:SetZoomSpline(zoomSpline)
|
||||
self.zoomSpline = zoomSpline;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetZoomSpline()
|
||||
return self.zoomSpline;
|
||||
end
|
||||
|
||||
--[[ These return the simple + spline values ]]--
|
||||
function OrbitCameraMixin:GetDerivedTarget()
|
||||
local targetX, targetY, targetZ = self:GetTarget();
|
||||
|
||||
local targetSpline = self:GetTargetSpline();
|
||||
if targetSpline then
|
||||
return Vector3D_Add(targetX, targetY, targetZ, targetSpline:CalculatePointOnGlobalCurve(1.0 - self:GetZoomPercent()));
|
||||
end
|
||||
|
||||
return targetX, targetY, targetZ;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetDerivedOrientation()
|
||||
local yaw, pitch, roll = self:GetYaw(), self:GetPitch(), self:GetRoll();
|
||||
|
||||
local orientationSpline = self:GetOrientationSpline();
|
||||
if orientationSpline then
|
||||
return Vector3D_Add(yaw, pitch, roll, orientationSpline:CalculatePointOnGlobalCurve(1.0 - self:GetZoomPercent()));
|
||||
end
|
||||
|
||||
return yaw, pitch, roll;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetDerivedZoomDistance()
|
||||
local zoomDistance = self:GetZoomDistance();
|
||||
|
||||
local zoomSpline = self:GetZoomSpline();
|
||||
if zoomSpline then
|
||||
return zoomDistance + zoomSpline:CalculatePointOnGlobalCurve(1.0 - self:GetZoomPercent());
|
||||
end
|
||||
|
||||
return zoomDistance;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetAlignLightToOrbitDelta(alignLightToOrbitDelta)
|
||||
if alignLightToOrbitDelta then
|
||||
self.modelSceneCameraInfo.flags = bit.bor(self.modelSceneCameraInfo.flags, Enum.ModelSceneSetting.AlignLightToOrbitDelta);
|
||||
else
|
||||
self.modelSceneCameraInfo.flags = bit.band(self.modelSceneCameraInfo.flags, bit.bnot(Enum.ModelSceneSetting.AlignLightToOrbitDelta));
|
||||
end
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:ShouldAlignLightToOrbitDelta()
|
||||
return bit.band(self.modelSceneCameraInfo.flags, Enum.ModelSceneSetting.AlignLightToOrbitDelta) == Enum.ModelSceneSetting.AlignLightToOrbitDelta;
|
||||
end
|
||||
|
||||
--[[
|
||||
Interpolation API
|
||||
For each API, "amount" is the percentage to approach in an "ideal" frame (60 fps), such that setting the value to >= 1 would snap to the desired target and <= 0 would freeze the interpolation.
|
||||
Setting to nil will disable the interpolation.
|
||||
]]
|
||||
|
||||
function OrbitCameraMixin:SetYawInterpolationAmount(yawInterpolationAmount)
|
||||
self.yawInterpolationAmount = yawInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetYawInterpolationAmount()
|
||||
return self.yawInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetPitchInterpolationAmount(pitchInterpolationAmount)
|
||||
self.pitchInterpolationAmount = pitchInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetPitchInterpolationAmount()
|
||||
return self.pitchInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetRollInterpolationAmount(rollInterpolationAmount)
|
||||
self.rollInterpolationAmount = rollInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetRollInterpolationAmount()
|
||||
return self.rollInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetTargetInterpolationAmount(targetInterpolationAmount)
|
||||
self.targetInterpolationAmount = targetInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetTargetInterpolationAmount()
|
||||
return self.targetInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetZoomInterpolationAmount(zoomInterpolationAmount)
|
||||
self.zoomInterpolationAmount = zoomInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetZoomInterpolationAmount()
|
||||
return self.zoomInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetInterpolatedTarget()
|
||||
if self.interpolatedTargetX then
|
||||
return self.interpolatedTargetX, self.interpolatedTargetY, self.interpolatedTargetZ;
|
||||
end
|
||||
return self:GetDerivedTarget();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetInterpolatedOrientation()
|
||||
if self.interpolatedYaw then
|
||||
return self.interpolatedYaw, self.interpolatedPitch, self.interpolatedRoll;
|
||||
end
|
||||
return self:GetDerivedOrientation();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetInterpolatedZoomDistance()
|
||||
if self.interpolatedZoomDistance then
|
||||
return self.interpolatedZoomDistance;
|
||||
end
|
||||
return self:GetDerivedZoomDistance();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapToTargetInterpolationYaw()
|
||||
self.interpolatedYaw = nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapToTargetInterpolationPitch()
|
||||
self.interpolatedPitch = nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapToTargetInterpolationRoll()
|
||||
self.interpolatedRoll = nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapToTargetInterpolationZoom()
|
||||
self.interpolatedZoomDistance = nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapToTargetInterpolationTarget()
|
||||
self.interpolatedTargetX = nil;
|
||||
self.interpolatedTargetY = nil;
|
||||
self.interpolatedTargetZ = nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapAllInterpolatedValues()
|
||||
self:SnapToTargetInterpolationYaw();
|
||||
self:SnapToTargetInterpolationPitch();
|
||||
self:SnapToTargetInterpolationRoll();
|
||||
|
||||
self:SnapToTargetInterpolationZoom();
|
||||
|
||||
self:SnapToTargetInterpolationTarget();
|
||||
end
|
||||
|
||||
ORBIT_CAMERA_MOUSE_MODE_NOTHING = 0;
|
||||
ORBIT_CAMERA_MOUSE_MODE_YAW_ROTATION = 1;
|
||||
ORBIT_CAMERA_MOUSE_MODE_PITCH_ROTATION = 2;
|
||||
ORBIT_CAMERA_MOUSE_MODE_ROLL_ROTATION = 3;
|
||||
ORBIT_CAMERA_MOUSE_MODE_TARGET_HORIZONTAL = 4;
|
||||
ORBIT_CAMERA_MOUSE_MODE_TARGET_VERTICAL = 5;
|
||||
ORBIT_CAMERA_MOUSE_MODE_ZOOM = 6;
|
||||
ORBIT_CAMERA_MOUSE_PAN_HORIZONTAL = 7;
|
||||
ORBIT_CAMERA_MOUSE_PAN_VERTICAL = 8;
|
||||
|
||||
function OrbitCameraMixin:SetLeftMouseButtonXMode(mouseMode, snap)
|
||||
self.buttonModes.leftX = mouseMode;
|
||||
self.buttonModes.leftXinterpolate = not snap;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetLeftMouseButtonXMode()
|
||||
return self.buttonModes.leftX, not self.buttonModes.leftXinterpolate;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetLeftMouseButtonYMode(mouseMode, snap)
|
||||
self.buttonModes.leftY = mouseMode;
|
||||
self.buttonModes.leftYinterpolate = not snap;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetLeftYMouseButtonYMode()
|
||||
return self.buttonModes.leftY, not self.buttonModes.leftYinterpolate;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetRightMouseButtonXMode(mouseMode, snap)
|
||||
self.buttonModes.rightX = mouseMode;
|
||||
self.buttonModes.rightXinterpolate = not snap;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetRightMouseButtonXMode()
|
||||
return self.buttonModes.rightX, not self.buttonModes.rightXinterpolate;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetRightMouseButtonYMode(mouseMode, snap)
|
||||
self.buttonModes.rightY = mouseMode;
|
||||
self.buttonModes.rightYinterpolate = not snap;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetRightMouseButtonYMode()
|
||||
return self.buttonModes.rightY, not self.buttonModes.rightYinterpolate;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetMouseWheelMode(mouseMode, snap)
|
||||
self.buttonModes.wheel = mouseMode;
|
||||
self.buttonModes.wheelInterpolate = not snap;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetMouseWheelMode()
|
||||
return self.buttonModes.wheel, not self.buttonModes.wheelInterpolate;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:HandleMouseMovement(mode, delta, snapToValue)
|
||||
if mode == ORBIT_CAMERA_MOUSE_MODE_YAW_ROTATION then
|
||||
self:SetYaw(self:GetYaw() - delta);
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationYaw();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_PITCH_ROTATION then
|
||||
self:SetPitch(self:GetPitch() - delta);
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationPitch();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_ROLL_ROTATION then
|
||||
self:SetRoll(self:GetRoll() - delta);
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationRoll();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_ZOOM then
|
||||
self:ZoomByPercent(delta);
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationZoom();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_TARGET_HORIZONTAL then
|
||||
local rightX, rightY, rightZ = Vector3D_ScaleBy(delta, self:GetRightVector());
|
||||
self:SetTarget(Vector3D_Add(rightX, rightY, rightZ, self:GetTarget()));
|
||||
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationTarget();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_TARGET_VERTICAL then
|
||||
local upX, upY, upZ = Vector3D_ScaleBy(delta, self:GetUpVector());
|
||||
self:SetTarget(Vector3D_Add(upX, upY, upZ, self:GetTarget()));
|
||||
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationTarget();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_PAN_HORIZONTAL then
|
||||
self.panningXOffset = self.panningXOffset + delta;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_PAN_VERTICAL then
|
||||
self.panningYOffset = self.panningYOffset + delta;
|
||||
end
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:ResetDefaultInputModes()
|
||||
self:SetLeftMouseButtonXMode(ORBIT_CAMERA_MOUSE_MODE_YAW_ROTATION, true);
|
||||
self:SetLeftMouseButtonYMode(ORBIT_CAMERA_MOUSE_MODE_NOTHING);
|
||||
self:SetRightMouseButtonXMode(ORBIT_CAMERA_MOUSE_MODE_NOTHING);
|
||||
self:SetRightMouseButtonYMode(ORBIT_CAMERA_MOUSE_MODE_NOTHING);
|
||||
self:SetMouseWheelMode(ORBIT_CAMERA_MOUSE_MODE_ZOOM, false);
|
||||
end
|
||||
|
||||
-- "private" function
|
||||
function OrbitCameraMixin:OnAdded() -- override
|
||||
self.buttonModes = {};
|
||||
|
||||
self:SetTarget(0, 0, 0);
|
||||
|
||||
local targetSpline = CreateCatmullRomSpline(3);
|
||||
targetSpline:AddPoint(0, 0, 0);
|
||||
targetSpline:AddPoint(0, 0, .5);
|
||||
|
||||
self:SetTargetSpline(targetSpline);
|
||||
|
||||
self:SetMinZoomDistance(6);
|
||||
self:SetMaxZoomDistance(10);
|
||||
|
||||
self:SetZoomDistance(8);
|
||||
|
||||
self:SetYaw(math.pi);
|
||||
self:SetPitch(0);
|
||||
self:SetRoll(0);
|
||||
|
||||
self:SetZoomInterpolationAmount(.15);
|
||||
self:SetYawInterpolationAmount(.15);
|
||||
self:SetPitchInterpolationAmount(.15);
|
||||
self:SetRollInterpolationAmount(.15);
|
||||
self:SetTargetInterpolationAmount(.15);
|
||||
|
||||
self:ResetDefaultInputModes();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetDeltaModifierForCameraMode(mode)
|
||||
if mode == ORBIT_CAMERA_MOUSE_MODE_YAW_ROTATION then
|
||||
return .008;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_PITCH_ROTATION then
|
||||
return .008;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_ROLL_ROTATION then
|
||||
return .008;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_ZOOM then
|
||||
return .1;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_TARGET_HORIZONTAL then
|
||||
return .05;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_TARGET_VERTICAL then
|
||||
return .05;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_PAN_HORIZONTAL then
|
||||
return 0.93;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_PAN_VERTICAL then
|
||||
return 0.93;
|
||||
end
|
||||
return 0.0;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:OnUpdate(elapsed) -- override
|
||||
if self:IsLeftMouseButtonDown() then
|
||||
local deltaX, deltaY = InputUtil.GetCursorDelta(GetAppropriateTopLevelParent());
|
||||
self:HandleMouseMovement(self.buttonModes.leftX, deltaX * self:GetDeltaModifierForCameraMode(self.buttonModes.leftX), not self.buttonModes.leftXinterpolate);
|
||||
self:HandleMouseMovement(self.buttonModes.leftY, -deltaY * self:GetDeltaModifierForCameraMode(self.buttonModes.leftY), not self.buttonModes.leftYinterpolate);
|
||||
end
|
||||
|
||||
if self:IsRightMouseButtonDown() then
|
||||
local deltaX, deltaY = InputUtil.GetCursorDelta(GetAppropriateTopLevelParent());
|
||||
self:HandleMouseMovement(self.buttonModes.rightX, deltaX * self:GetDeltaModifierForCameraMode(self.buttonModes.rightX), not self.buttonModes.rightXinterpolate);
|
||||
self:HandleMouseMovement(self.buttonModes.rightY, -deltaY * self:GetDeltaModifierForCameraMode(self.buttonModes.rightY), not self.buttonModes.rightYinterpolate);
|
||||
end
|
||||
|
||||
self:UpdateInterpolationTargets(elapsed);
|
||||
self:SynchronizeCamera();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SynchronizeCamera() -- override
|
||||
self:UpdateCameraOrientationAndPosition();
|
||||
self:UpdateLight();
|
||||
end
|
||||
|
||||
local function InterpolateDimension(lastValue, targetValue, amount, elapsed)
|
||||
return lastValue and amount and DeltaLerp(lastValue, targetValue, amount, elapsed) or targetValue;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:UpdateInterpolationTargets(elapsed)
|
||||
local yaw, pitch, roll = self:GetDerivedOrientation();
|
||||
local targetX, targetY, targetZ = self:GetDerivedTarget();
|
||||
local zoomDistance = self:GetDerivedZoomDistance();
|
||||
|
||||
self.interpolatedYaw = InterpolateDimension(self.interpolatedYaw, yaw, self.yawInterpolationAmount, elapsed);
|
||||
self.interpolatedPitch = InterpolateDimension(self.interpolatedPitch, pitch, self.pitchInterpolationAmount, elapsed);
|
||||
self.interpolatedRoll = InterpolateDimension(self.interpolatedRoll, roll, self.rollInterpolationAmount, elapsed);
|
||||
|
||||
self.interpolatedZoomDistance = InterpolateDimension(self.interpolatedZoomDistance, zoomDistance, self.zoomInterpolationAmount, elapsed);
|
||||
|
||||
self.interpolatedTargetX = InterpolateDimension(self.interpolatedTargetX, targetX, self.targetInterpolationAmount, elapsed);
|
||||
self.interpolatedTargetY = InterpolateDimension(self.interpolatedTargetY, targetY, self.targetInterpolationAmount, elapsed);
|
||||
self.interpolatedTargetZ = InterpolateDimension(self.interpolatedTargetZ, targetZ, self.targetInterpolationAmount, elapsed);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:UpdateCameraOrientationAndPosition()
|
||||
local yaw, pitch, roll = self:GetInterpolatedOrientation();
|
||||
local axisAngleX, axisAngleY, axisAngleZ = Vector3D_CalculateNormalFromYawPitch(yaw, pitch);
|
||||
|
||||
local targetX, targetY, targetZ = self:GetInterpolatedTarget();
|
||||
|
||||
local zoomDistance = self:GetInterpolatedZoomDistance();
|
||||
|
||||
-- Panning start --
|
||||
-- We want the model to move 1-to-1 with the mouse.
|
||||
-- Panning formula: dx / hypotenuse * (zoomDistance - 1 / zoomDistance^3)
|
||||
-- It was experimentally determined that adding the additional fudge factor 1/z^3 resulted in better tracking.
|
||||
|
||||
local width = self:GetOwningScene():GetWidth();
|
||||
local height = self:GetOwningScene():GetHeight();
|
||||
local scaleFactor = (width ~= 0 and height ~= 0) and math.sqrt(width * width + height * height) or 1;
|
||||
local zoomFactor = 1;
|
||||
if zoomDistance > 1 then
|
||||
zoomFactor = zoomDistance - (1 / (zoomDistance * zoomDistance * zoomDistance));
|
||||
if zoomFactor < 1 then
|
||||
zoomFactor = 1;
|
||||
end
|
||||
end
|
||||
|
||||
local rightX, rightY, rightZ = Vector3D_ScaleBy((self.panningXOffset / scaleFactor) * zoomFactor, self:GetRightVector());
|
||||
local upX, upY, upZ = Vector3D_ScaleBy((self.panningYOffset / scaleFactor) * zoomFactor, self:GetUpVector());
|
||||
|
||||
-- Panning end --
|
||||
|
||||
targetX, targetY, targetZ = Vector3D_Add(targetX, targetY, targetZ, rightX, rightY, rightZ);
|
||||
targetX, targetY, targetZ = Vector3D_Add(targetX, targetY, targetZ, upX, upY, upZ);
|
||||
|
||||
self:SetPosition(self:CalculatePositionByDistanceFromTarget(targetX, targetY, targetZ, zoomDistance, axisAngleX, axisAngleY, axisAngleZ));
|
||||
self:GetOwningScene():SetCameraOrientationByYawPitchRoll(yaw, pitch, roll);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:UpdateLight()
|
||||
if self:ShouldAlignLightToOrbitDelta() then
|
||||
local owningScene = self:GetOwningScene();
|
||||
owningScene:SetLightDirection(self:GetForwardVector());
|
||||
end
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:CalculatePositionByDistanceFromTarget(targetX, targetY, targetZ, zoomDistance, axisAngleX, axisAngleY, axisAngleZ)
|
||||
local towardsPosX, towardsPosY, towardsPosZ = Vector3D_ScaleBy(-zoomDistance, axisAngleX, axisAngleY, axisAngleZ);
|
||||
return Vector3D_Add(towardsPosX, towardsPosY, towardsPosZ, targetX, targetY, targetZ);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:CalculateTransitionalValues(fromModelSceneCameraInfo, toModelSceneCameraInfo, modificationType)
|
||||
if fromModelSceneCameraInfo and modificationType == CAMERA_MODIFICATION_TYPE_MAINTAIN then
|
||||
local zoomedTargetOffsetX, zoomedTargetOffsetY, zoomedTargetOffsetZ = 0, 0, 0;
|
||||
|
||||
local targetSpline = self:GetTargetSpline();
|
||||
if targetSpline and targetSpline:GetNumPoints() > 0 then
|
||||
zoomedTargetOffsetX, zoomedTargetOffsetY, zoomedTargetOffsetZ = targetSpline:GetPoint(targetSpline:GetNumPoints());
|
||||
end
|
||||
|
||||
local zoomedYawOffset, zoomedPitchOffset, zoomedRollOffset = 0, 0, 0;
|
||||
local orientationSpline = self:GetOrientationSpline();
|
||||
if orientationSpline and orientationSpline:GetNumPoints() > 0 then
|
||||
zoomedYawOffset, zoomedPitchOffset, zoomedRollOffset = orientationSpline:GetPoint(orientationSpline:GetNumPoints());
|
||||
end
|
||||
|
||||
local fromZoomDistancePercent = PercentageBetween(fromModelSceneCameraInfo.zoomDistance, fromModelSceneCameraInfo.minZoomDistance, fromModelSceneCameraInfo.maxZoomDistance);
|
||||
local toZoomDistancePercent = PercentageBetween(toModelSceneCameraInfo.zoomDistance, toModelSceneCameraInfo.minZoomDistance, toModelSceneCameraInfo.maxZoomDistance);
|
||||
-- Maintain any modifications made by applying the scene delta
|
||||
return {
|
||||
target = CreateVector3D(
|
||||
(self.targetX - fromModelSceneCameraInfo.target.x) + toModelSceneCameraInfo.target.x,
|
||||
(self.targetY - fromModelSceneCameraInfo.target.y) + toModelSceneCameraInfo.target.y,
|
||||
(self.targetZ - fromModelSceneCameraInfo.target.z) + toModelSceneCameraInfo.target.z
|
||||
),
|
||||
|
||||
yaw = (self.yaw - fromModelSceneCameraInfo.yaw) + toModelSceneCameraInfo.yaw,
|
||||
pitch = (self.pitch - fromModelSceneCameraInfo.pitch) + toModelSceneCameraInfo.pitch,
|
||||
roll = (self.roll - fromModelSceneCameraInfo.roll) + toModelSceneCameraInfo.roll,
|
||||
|
||||
zoomDistance = Lerp(toModelSceneCameraInfo.minZoomDistance, toModelSceneCameraInfo.maxZoomDistance, (self.zoomPercent - fromZoomDistancePercent) + toZoomDistancePercent),
|
||||
minZoomDistance = (self.minZoomDistance - fromModelSceneCameraInfo.minZoomDistance) + toModelSceneCameraInfo.minZoomDistance,
|
||||
maxZoomDistance = (self.maxZoomDistance - fromModelSceneCameraInfo.maxZoomDistance) + toModelSceneCameraInfo.maxZoomDistance,
|
||||
|
||||
zoomedTargetOffset = CreateVector3D(
|
||||
(zoomedTargetOffsetX - fromModelSceneCameraInfo.zoomedTargetOffset.x) + toModelSceneCameraInfo.zoomedTargetOffset.x,
|
||||
(zoomedTargetOffsetY - fromModelSceneCameraInfo.zoomedTargetOffset.y) + toModelSceneCameraInfo.zoomedTargetOffset.y,
|
||||
(zoomedTargetOffsetZ - fromModelSceneCameraInfo.zoomedTargetOffset.z) + toModelSceneCameraInfo.zoomedTargetOffset.z
|
||||
),
|
||||
|
||||
zoomedYawOffset = (zoomedYawOffset - fromModelSceneCameraInfo.zoomedYawOffset) + toModelSceneCameraInfo.zoomedYawOffset,
|
||||
zoomedPitchOffset = (zoomedPitchOffset - fromModelSceneCameraInfo.zoomedPitchOffset) + toModelSceneCameraInfo.zoomedPitchOffset,
|
||||
zoomedRollOffset = (zoomedRollOffset - fromModelSceneCameraInfo.zoomedRollOffset) + toModelSceneCameraInfo.zoomedRollOffset;
|
||||
};
|
||||
end
|
||||
|
||||
-- Nothing to transition from, just go directly to the values
|
||||
return toModelSceneCameraInfo;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:OnMouseWheel(delta) -- override
|
||||
self:HandleMouseMovement(self.buttonModes.wheel, delta * self:GetDeltaModifierForCameraMode(self.buttonModes.wheel), not self.buttonModes.wheelInterpolate);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:AdjustYaw(deltaX, deltaY, rotationScalar)
|
||||
local xRotation = rotationScalar or self:GetDeltaModifierForCameraMode(self.buttonModes.leftX);
|
||||
self:HandleMouseMovement(self.buttonModes.leftX, deltaX * xRotation, not self.buttonModes.leftXinterpolate);
|
||||
|
||||
local yRotatation = rotationScalar or self:GetDeltaModifierForCameraMode(self.buttonModes.leftY);
|
||||
self:HandleMouseMovement(self.buttonModes.leftY, -deltaY * yRotatation, not self.buttonModes.leftYinterpolate);
|
||||
end
|
||||
@@ -107,6 +107,12 @@ function GameTooltip_AddNormalLine(tooltip, text, wrap, leftOffset)
|
||||
GameTooltip_AddColoredLine(tooltip, text, NORMAL_FONT_COLOR, wrap, leftOffset);
|
||||
end
|
||||
|
||||
-- Retail's takes `...` and passes it on; named parameters keep Lua 5.0 from
|
||||
-- building an `arg` table per call.
|
||||
function GameTooltip_AddBodyLine(tooltip, text, wrap, leftOffset)
|
||||
GameTooltip_AddNormalLine(tooltip, text, wrap, leftOffset);
|
||||
end
|
||||
|
||||
function GameTooltip_AddHighlightLine(tooltip, text, wrap, leftOffset)
|
||||
GameTooltip_AddColoredLine(tooltip, text, HIGHLIGHT_FONT_COLOR, wrap, leftOffset);
|
||||
end
|
||||
|
||||
@@ -8,6 +8,12 @@ function InputUtil.GetCursorPosition(parent)
|
||||
return x / scale, y / scale;
|
||||
end
|
||||
|
||||
function InputUtil.GetCursorDelta(parent)
|
||||
local x, y = GetCursorDelta();
|
||||
local scale = parent:GetEffectiveScale();
|
||||
return x / scale, y / scale;
|
||||
end
|
||||
|
||||
function InputUtil.AnchorRegionToCursor(region, point)
|
||||
local parent = GetAppropriateTopLevelParent();
|
||||
local x, y = InputUtil.GetCursorPosition(parent);
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
-- (scrapping, runeforging, soulbinds, keystones, item upgrades,
|
||||
-- recrafting, bank tabs), and the stock bag and character frames never
|
||||
-- ask for it.
|
||||
-- - No ItemUtil.CreateItemTransmogInfo or ItemTransmogInfoMixin: there is
|
||||
-- no transmogrification.
|
||||
-- - ItemTransmogInfoMixin's appearanceID is an item ID and its illusionID a
|
||||
-- SpellItemEnchantment ID, as the ModelSceneActor ItemTransmogInfo
|
||||
-- methods read and write them.
|
||||
-- - ItemUtil.GetItemDetails has no currency path: there are no currencies.
|
||||
-- - No ItemUtil.GetCraftingReagentCount: it picks between the reagent bank
|
||||
-- and the account bank, and neither exists.
|
||||
@@ -95,6 +96,10 @@ function ItemUtil.FilterOwnedItems(itemIDs)
|
||||
return filtered;
|
||||
end
|
||||
|
||||
function ItemUtil.CreateItemTransmogInfo(appearanceID, secondaryAppearanceID, illusionID)
|
||||
return CreateAndInitFromMixin(ItemTransmogInfoMixin, appearanceID, secondaryAppearanceID, illusionID);
|
||||
end
|
||||
|
||||
function ItemUtil.TransformItemIDsToItems(itemIDs)
|
||||
local items = {};
|
||||
for index, itemID in ipairs(itemIDs) do
|
||||
@@ -194,3 +199,42 @@ function ItemUtil.GetEquipSlotTexture(equipSlot)
|
||||
|
||||
return nil;
|
||||
end
|
||||
|
||||
ItemTransmogInfoMixin = {};
|
||||
|
||||
function ItemTransmogInfoMixin:Init(appearanceID, secondaryAppearanceID, illusionID)
|
||||
self.appearanceID = appearanceID;
|
||||
self.secondaryAppearanceID = secondaryAppearanceID or Constants.Transmog.NoTransmogID;
|
||||
self.illusionID = illusionID or Constants.Transmog.NoTransmogID;
|
||||
end
|
||||
|
||||
function ItemTransmogInfoMixin:IsEqual(itemTransmogInfo)
|
||||
if not itemTransmogInfo then
|
||||
return false;
|
||||
end
|
||||
return self.appearanceID == itemTransmogInfo.appearanceID and self.secondaryAppearanceID == itemTransmogInfo.secondaryAppearanceID and self.illusionID == itemTransmogInfo.illusionID;
|
||||
end
|
||||
|
||||
function ItemTransmogInfoMixin:Clear()
|
||||
self.appearanceID = Constants.Transmog.NoTransmogID;
|
||||
self.secondaryAppearanceID = Constants.Transmog.NoTransmogID;
|
||||
self.illusionID = Constants.Transmog.NoTransmogID;
|
||||
end
|
||||
|
||||
-- There is no slot info in ItemTransmogInfo so the following 3 MainHand functions must be used with correct itemTransmogInfo at call site
|
||||
function ItemTransmogInfoMixin:ConfigureSecondaryForMainHand(isLegionArtifact)
|
||||
if isLegionArtifact then
|
||||
self.secondaryAppearanceID = Constants.Transmog.MainHandTransmogIsPairedWeapon;
|
||||
else
|
||||
self.secondaryAppearanceID = Constants.Transmog.MainHandTransmogIsIndividualWeapon;
|
||||
end
|
||||
end
|
||||
|
||||
function ItemTransmogInfoMixin:IsMainHandIndividualWeapon()
|
||||
return self.secondaryAppearanceID == Constants.Transmog.MainHandTransmogIsIndividualWeapon;
|
||||
end
|
||||
|
||||
function ItemTransmogInfoMixin:IsMainHandPairedWeapon()
|
||||
-- paired weapon can be value Constants.Transmog.MainHandTransmogIsPairedWeapon or greater
|
||||
return not self:IsMainHandIndividualWeapon();
|
||||
end
|
||||
|
||||
@@ -224,6 +224,9 @@ end
|
||||
-- extended `GameTooltip:SetHyperlink` (src/tooltip/Hyperlink.cpp), which
|
||||
-- builds the same tooltip `GameTooltip:SetSpellByID` builds, so the handler
|
||||
-- passes the payload through instead of rebuilding the tooltip itself.
|
||||
-- Control-click is retail's dress-up click: DressUpLink previews a mount
|
||||
-- spell, and nothing else happens, as retail's modified click shows no
|
||||
-- tooltip.
|
||||
------------------------------------------------------------------------------
|
||||
LinkUtil.RegisterLinkHandler(LinkTypes.Spell, function(link, text, linkData, contextData)
|
||||
local spellID = tonumber(string.match(link, "^spell:(%d+)"));
|
||||
@@ -234,6 +237,10 @@ LinkUtil.RegisterLinkHandler(LinkTypes.Spell, function(link, text, linkData, con
|
||||
ChatFrameEditBox:Insert(text);
|
||||
return LinkProcessorResponse.Handled;
|
||||
end
|
||||
if IsControlKeyDown() then
|
||||
DressUpLink(text);
|
||||
return LinkProcessorResponse.Handled;
|
||||
end
|
||||
local tooltip = ItemRefTooltip or GameTooltip;
|
||||
tooltip:SetOwner(UIParent, "ANCHOR_PRESERVE");
|
||||
tooltip:SetHyperlink(link);
|
||||
@@ -250,13 +257,14 @@ end);
|
||||
-- links. Route spell links Turtle would drop back through SetItemRef so the
|
||||
-- Spell handler above shows them; Turtle-shaped links
|
||||
-- (`spell:<id>:<bookType>:<player>`) pass through to Turtle's handler
|
||||
-- untouched. On a stock client the original already forwards everything to
|
||||
-- SetItemRef, so both branches behave the same there.
|
||||
-- untouched, except a Control-click, which previews the mount here. On a
|
||||
-- stock client the original already forwards everything to SetItemRef, so
|
||||
-- both branches behave the same there.
|
||||
------------------------------------------------------------------------------
|
||||
local originalChatFrameOnHyperlinkShow = ChatFrame_OnHyperlinkShow;
|
||||
function ChatFrame_OnHyperlinkShow(link, text, button)
|
||||
if type(link) == "string" and strsub(link, 1, 6) == "spell:" and
|
||||
not string.find(link, "^spell:(%d+):(%w+):(%w+)") then
|
||||
(IsControlKeyDown() or not string.find(link, "^spell:(%d+):(%w+):(%w+)")) then
|
||||
SetItemRef(link, text, button);
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -74,6 +74,10 @@ OPTION_TOOLTIP_INTERACT_ICONS = "Legt fest, welche Symbole über NSCs und Gegens
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Automatischen Angriff stoppen, sobald das Ziel gewechselt wird.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<Zum Löschen der Belegung rechtsklicken>";
|
||||
RESET_POSITION = "Zurücksetzen";
|
||||
ROTATE_LEFT = "Linksdrehung";
|
||||
ROTATE_RIGHT = "Rechtsdrehung";
|
||||
ROTATE_TOOLTIP = "Linksklicken und ziehen, um den Charakter zu drehen.";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Tastaturbelegung für \"%s\" zuweisen oder mit %s abbrechen";
|
||||
SET_FOCUS = "Fokus setzen";
|
||||
FOCUSTARGET = "Ziel als Fokus festlegen";
|
||||
|
||||
@@ -17,7 +17,7 @@ CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell, use an objec
|
||||
CLASSICAPI_AUTO_STAND = "Auto Stand";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "Stand up when you cast a spell, loot, or open the flight map while you sit.";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "Auto Cancel Form";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it, talk to an NPC, use an object or an item, or open the flight map.";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it, use an item, or open the flight map. Also lets you talk to NPCs and use objects in druid forms and Ghost Wolf.";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "Bags and Items";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "Casting and Attacking";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "Combat Log Retention";
|
||||
@@ -164,6 +164,10 @@ OPTION_TOOLTIP_INTERACT_ICONS = "Controls which icons appear above NPCs and obje
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Stop auto attacking when you switch targets.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<Right click to unbind>";
|
||||
RESET_POSITION = "Reset Position";
|
||||
ROTATE_LEFT = "Rotate Left";
|
||||
ROTATE_RIGHT = "Rotate Right";
|
||||
ROTATE_TOOLTIP = "Left-click on character and drag to rotate.";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Assign Binding for \"%s\" or Press %s to Cancel";
|
||||
SET_FOCUS = "Set Focus";
|
||||
FOCUS_TOKEN_NOT_FOUND = "<no focus>";
|
||||
|
||||
@@ -74,6 +74,10 @@ OPTION_TOOLTIP_INTERACT_ICONS = "Controla los iconos que aparecen sobre los PNJ
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Detiene el ataque automático al cambiar de objetivo.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<Haz clic derecho para quitar la asignación>";
|
||||
RESET_POSITION = "Restablecer posición";
|
||||
ROTATE_LEFT = "Rotar a la izquierda";
|
||||
ROTATE_RIGHT = "Rotar a la derecha";
|
||||
ROTATE_TOOLTIP = "Haz clic izquierdo en el personaje y arrastra para rotar.";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Asignar atajo para \"%s\" o pulsar %s para cancelar";
|
||||
SET_FOCUS = "Establecer foco";
|
||||
FOCUSTARGET = "Establecer objetivo";
|
||||
|
||||
@@ -74,6 +74,10 @@ OPTION_TOOLTIP_INTERACT_ICONS = "Contrôle les icônes qui s’affichent au-dess
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Interrompt l'attaque automatique lorsque vous changez de cible.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<Clic droit pour retirer>";
|
||||
RESET_POSITION = "Position par défaut";
|
||||
ROTATE_LEFT = "Pivoter à gauche";
|
||||
ROTATE_RIGHT = "Pivoter à droite";
|
||||
ROTATE_TOOLTIP = "Cliquez sur le personnage et faites glisser la souris pour le faire pivoter.";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Assigner un raccourci pour « %s » ou appuyer sur %s pour annuler";
|
||||
SET_FOCUS = "Définir la focalisation";
|
||||
FOCUSTARGET = "Définir la cible de focalisation";
|
||||
|
||||
@@ -5,6 +5,10 @@ DECIMAL_SEPERATOR = ".";
|
||||
FIRST_NUMBER = "천";
|
||||
FIRST_NUMBER_CAP = "만";
|
||||
FIRST_NUMBER_CAP_NO_SPACE = "만";
|
||||
RESET_POSITION = "위치 초기화";
|
||||
ROTATE_LEFT = "왼쪽으로 회전";
|
||||
ROTATE_RIGHT = "오른쪽으로 회전";
|
||||
ROTATE_TOOLTIP = "마우스 왼쪽 버튼으로 클릭 후 끌어 옮기면 캐릭터를 회전할 수 있습니다.";
|
||||
SECOND_NUMBER = "만";
|
||||
SECOND_NUMBER_CAP = "억";
|
||||
SECOND_NUMBER_CAP_NO_SPACE = "억";
|
||||
|
||||
@@ -19,7 +19,7 @@ CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Спешиваться при примене
|
||||
CLASSICAPI_AUTO_STAND = "Автоматически вставать";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "Вставать при применении заклинания, сборе добычи или открытии карты полетов, если вы сидите.";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "Автоматически снимать облик";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Покидать свой облик при применении заклинания, которое в нем недоступно, разговоре с NPC, использовании объекта или предмета, а также при открытии карты полетов.";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Покидать свой облик при применении заклинания, которое в нем недоступно, использовании предмета, а также при открытии карты полетов. Также позволяет говорить с NPC и использовать объекты в обликах друида и призрачного волка.";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "Сумки и предметы";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "Заклинания и атаки";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "Хранение журнала боя";
|
||||
@@ -166,6 +166,10 @@ OPTION_TOOLTIP_INTERACT_ICONS = "Определяет, какие значки
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Прекращать автоатаку при смене цели.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<ПКМ, чтобы снять привязку>";
|
||||
RESET_POSITION = "Сброс позиции";
|
||||
ROTATE_LEFT = "Вращение влево";
|
||||
ROTATE_RIGHT = "Вращение вправо";
|
||||
ROTATE_TOOLTIP = "Зажмите ЛКМ и тащите курсор, чтобы вращать изображение.";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Назначьте клавишу для \"%s\" или нажмите %s для отмены";
|
||||
SET_FOCUS = "Запомнить цель";
|
||||
FOCUS_TOKEN_NOT_FOUND = "<фокус отсутствует>";
|
||||
|
||||
@@ -19,7 +19,7 @@ CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "骑乘状态下施放法术、使用物体
|
||||
CLASSICAPI_AUTO_STAND = "自动站起";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "坐下状态下施放法术、拾取或打开飞行地图时自动站起。";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "自动取消形态";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "施放当前形态无法施放的法术、与NPC对话、使用物体或物品、或打开飞行地图时自动取消该形态。";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "施放当前形态无法施放的法术、使用物品或打开飞行地图时自动取消该形态。同时允许在德鲁伊形态和幽魂之狼形态下与NPC对话和使用物体。";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "背包与物品";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "施法与攻击";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "战斗记录保留时间";
|
||||
@@ -163,6 +163,10 @@ OPTION_TOOLTIP_INTERACT_ICONS = "控制交互按键启用后NPC和物体上方
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "当你切换目标时停止自动攻击。";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s(%s)";
|
||||
KEY_BINDING_TOOLTIP = "<右键解除键位>";
|
||||
RESET_POSITION = "重置位置";
|
||||
ROTATE_LEFT = "左转";
|
||||
ROTATE_RIGHT = "右转";
|
||||
ROTATE_TOOLTIP = "左键点击并拖曳以旋转角色";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "设置 \"%s\"的快捷键,或者按 %s 取消";
|
||||
SET_FOCUS = "设置焦点";
|
||||
FOCUSTARGET = "设定焦点目标";
|
||||
|
||||
@@ -74,6 +74,10 @@ OPTION_TOOLTIP_INTERACT_ICONS = "在啟用互動鍵功能後,控制出現在NP
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "當你切換目標時停止自動攻擊。";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s(%s)";
|
||||
KEY_BINDING_TOOLTIP = "<右鍵點擊以解除綁定>";
|
||||
RESET_POSITION = "重置位置";
|
||||
ROTATE_LEFT = "向左旋轉";
|
||||
ROTATE_RIGHT = "向右旋轉";
|
||||
ROTATE_TOOLTIP = "滑鼠左鍵點擊角色並拖曳以旋轉角度。";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "為\"%s\"指配綁定或是按下%s以取消";
|
||||
SET_FOCUS = "設定專注目標";
|
||||
FOCUSTARGET = "設定鎖定目標";
|
||||
|
||||
Binary file not shown.
+88
-38
@@ -19,6 +19,7 @@
|
||||
#include "frame/ScriptArgs.h"
|
||||
#include "frame/UnitEvent.h"
|
||||
#include "profile/ScriptTime.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -55,6 +56,10 @@ struct Reservation {
|
||||
int slot; // -1 until claimed
|
||||
const Game::Doc::Event *doc; // API documentation; nullptr = undocumented
|
||||
int fixedId; // an `EngineName`'s engine id (its slot for good); -1 = claimed
|
||||
// The SStrDup'd name `Claim` wrote into `slot`'s entry; nullptr while
|
||||
// unclaimed. `RetryClaims` compares it against the live entry to notice
|
||||
// that the engine rebuilt the table underneath a cached slot.
|
||||
const char *claimed;
|
||||
};
|
||||
Reservation g_reserved[MAX_RESERVED];
|
||||
int g_reservedCount = 0;
|
||||
@@ -63,7 +68,7 @@ bool g_writesEnabled = false;
|
||||
// claim) is logged once per session instead of silently dropping the
|
||||
// event — the failure mode that would otherwise rot unnoticed. Re-armed on
|
||||
// `/reload` (the table is rebuilt fresh). In practice this never triggers:
|
||||
// the engine leaves ~115 claimable gaps below 549 (see `TryClaim`) against
|
||||
// the engine leaves ~115 claimable gaps below 549 (see `Claim`) against
|
||||
// our ~77 reservations, and `Grow` adds room past that. The log is
|
||||
// purely a backstop if some future install somehow drains the gap pool.
|
||||
bool g_warnedTableFull = false;
|
||||
@@ -90,23 +95,6 @@ char *SStrDup(const char *s) {
|
||||
return fn(s, __FILE__, __LINE__);
|
||||
}
|
||||
|
||||
// Storm allocator/free — the same pair the engine uses for the event-table
|
||||
// buffer (see RebuildEventTable), so its reload teardown `SMemFree`s our
|
||||
// grown buffer exactly as its own. Both `__stdcall`, 4 args (`ret 0x10`).
|
||||
// SMemAlloc flag bit 8 zero-fills (matching SetEventCount's fresh entries).
|
||||
void *SMemAlloc(unsigned size) {
|
||||
using SMemAlloc_t = void *(__stdcall *)(unsigned size, const char *file,
|
||||
int line, int flags);
|
||||
auto fn = reinterpret_cast<SMemAlloc_t>(Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
return fn(size, __FILE__, __LINE__, 8 /* zero-fill */);
|
||||
}
|
||||
void SMemFree(void *ptr) {
|
||||
using SMemFree_t = int(__stdcall *)(void *ptr, const char *file, int line,
|
||||
int flags);
|
||||
auto fn = reinterpret_cast<SMemFree_t>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
fn(ptr, __FILE__, __LINE__, 0);
|
||||
}
|
||||
|
||||
// True when slot `id` of the live table can take one of our names: its name
|
||||
// is NULL (no one owns it) AND the engine never signals it by number. A NULL
|
||||
// name alone is not enough — the glue band (ids below GLUE_EVENT_COUNT) and
|
||||
@@ -147,23 +135,39 @@ int ClaimDemand() {
|
||||
// tail. Below 549 only nampower writes, at 369 and 540, both before any Lua
|
||||
// runs, so skip-occupied steps over them. Claiming low keeps us out of
|
||||
// everyone's way and independent of their clamp / hook order.
|
||||
int TryClaim(const char *eventName) {
|
||||
//
|
||||
// Claims only into the IN-GAME table. `CGGameUI::Initialize` (0x0048FE00)
|
||||
// calls `RegisterScriptFunctions` (our `EnableWrites` hook) and only THEN
|
||||
// `SetScriptEventTable(0xBE1198, 0x225)`, so between the two the live table
|
||||
// is still glue's 26-entry one. A `Frame::RegisterEvent` in that window
|
||||
// (a third-party DLL registering a frame from its own LoadScriptFunctions
|
||||
// hook — issue #134) used to grow the glue table and claim into it; the
|
||||
// engine then freed that buffer and our cached ids silently aliased engine
|
||||
// events in the real table. The in-game build always passes 0x225 names
|
||||
// (clamps by other DLLs only enlarge it), so a smaller count is not it.
|
||||
bool GameTableInstalled(int count) {
|
||||
return count >= Offsets::EVENT_NAME_TABLE_STATIC_COUNT;
|
||||
}
|
||||
|
||||
void Claim(Reservation &r) {
|
||||
if (!g_writesEnabled)
|
||||
return -1;
|
||||
return;
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
if (base == nullptr || count <= 0)
|
||||
return -1;
|
||||
if (base == nullptr || !GameTableInstalled(count))
|
||||
return;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (Claimable(base, i)) {
|
||||
auto **namePtr = Game::Ptr<const char *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE,
|
||||
Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
char *copy = SStrDup(eventName);
|
||||
char *copy = SStrDup(r.name);
|
||||
if (copy == nullptr)
|
||||
return -1;
|
||||
return;
|
||||
*namePtr = copy;
|
||||
return i;
|
||||
r.slot = i;
|
||||
r.claimed = copy;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Writes are enabled and the table is valid, but every slot is taken —
|
||||
@@ -173,9 +177,39 @@ int TryClaim(const char *eventName) {
|
||||
Debug::Log::Printf(
|
||||
"[event] table full (%d slots, no NULL left) — cannot claim '%s'"
|
||||
" and any further custom events; even Grow couldn't make room.",
|
||||
count, eventName ? eventName : "?");
|
||||
count, r.name);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// True when a claimed reservation's slot no longer holds its name: the
|
||||
// engine rebuilt the table (every entry re-`SStrDup`'d at a fresh buffer)
|
||||
// without our `PrepareForReload` seeing it. The pointer test is the cheap
|
||||
// common case; the strcmp covers an allocator handing the engine our freed
|
||||
// block for a different name.
|
||||
bool Stale(const Reservation &r) {
|
||||
if (r.claimed == nullptr)
|
||||
return false;
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
if (base == nullptr || r.slot >= count)
|
||||
return true;
|
||||
const char *live = Game::Read<const char *>(
|
||||
base + r.slot * Offsets::EVENT_ENTRY_STRIDE, Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
if (live == r.claimed)
|
||||
return false;
|
||||
return live == nullptr || std::strcmp(live, r.name) != 0;
|
||||
}
|
||||
|
||||
// Forget every claimed slot: the table they were claimed in is gone. The
|
||||
// engine freed our name copies with its per-entry teardown, so nothing to
|
||||
// release here. Re-arms the per-build one-shots.
|
||||
void ForgetClaims() {
|
||||
for (int i = 0; i < g_reservedCount; ++i) {
|
||||
g_reserved[i].slot = g_reserved[i].fixedId;
|
||||
g_reserved[i].claimed = nullptr;
|
||||
}
|
||||
g_warnedTableFull = false;
|
||||
g_growLatched = false;
|
||||
}
|
||||
|
||||
// On-demand event-table grow. Correctly TIMED: called once per in-game
|
||||
@@ -205,12 +239,14 @@ int TryClaim(const char *eventName) {
|
||||
void Grow() {
|
||||
if (g_growLatched || !g_writesEnabled)
|
||||
return;
|
||||
g_growLatched = true; // one attempt per build, whether or not it grows
|
||||
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
if (base == nullptr || !GameTableInstalled(count))
|
||||
return; // still glue's table — the in-game build hasn't run yet
|
||||
g_growLatched = true; // one attempt per build, whether or not it grows
|
||||
|
||||
const int demand = ClaimDemand();
|
||||
if (base == nullptr || count <= 0 || demand <= 0)
|
||||
if (demand <= 0)
|
||||
return;
|
||||
|
||||
int nulls = 0;
|
||||
@@ -242,8 +278,12 @@ void Grow() {
|
||||
constexpr int kMargin = 16;
|
||||
const int deficit = (nulls < demand) ? (demand - nulls) : 0;
|
||||
const int newCount = count + deficit + kMargin;
|
||||
// Storm's heap, like the engine's own table buffer (see RebuildEventTable),
|
||||
// so its reload teardown SMemFrees ours the same way; zero-filled, like
|
||||
// SetEventCount's fresh entries.
|
||||
auto *newBuf = static_cast<uint8_t *>(
|
||||
SMemAlloc(static_cast<unsigned>(newCount) * Offsets::EVENT_ENTRY_STRIDE));
|
||||
Storm::Alloc(static_cast<size_t>(newCount) * Offsets::EVENT_ENTRY_STRIDE, __FILE__,
|
||||
__LINE__, true));
|
||||
if (newBuf == nullptr)
|
||||
return; // alloc failed — keep the old table; claiming still works
|
||||
|
||||
@@ -271,7 +311,7 @@ void Grow() {
|
||||
Game::Ref<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR) = newBuf;
|
||||
Game::Ref<int>(Offsets::VAR_EVENT_TABLE_CAP) = newCount;
|
||||
Game::Ref<int>(Offsets::VAR_EVENT_TABLE_COUNT) = newCount;
|
||||
SMemFree(base);
|
||||
Storm::Free(base, __FILE__, __LINE__);
|
||||
|
||||
Debug::Log::Printf("[event] grew table %d -> %d (%d NULL, %d reserved%s)",
|
||||
count, newCount, nulls, demand,
|
||||
@@ -320,6 +360,7 @@ AutoReserve::AutoReserve(const char *name, const Game::Doc::Event *doc) {
|
||||
g_reserved[g_reservedCount].slot = -1;
|
||||
g_reserved[g_reservedCount].doc = doc;
|
||||
g_reserved[g_reservedCount].fixedId = -1;
|
||||
g_reserved[g_reservedCount].claimed = nullptr;
|
||||
index_ = g_reservedCount;
|
||||
++g_reservedCount;
|
||||
}
|
||||
@@ -339,7 +380,7 @@ EngineName::EngineName(int id, const char *name, const Game::Doc::Event *doc) {
|
||||
if (*slot != nullptr)
|
||||
return; // the engine names it; never take an engine event's slot
|
||||
*slot = name;
|
||||
g_reserved[g_reservedCount] = {name, id, doc, id};
|
||||
g_reserved[g_reservedCount] = {name, id, doc, id, nullptr};
|
||||
++g_reservedCount;
|
||||
}
|
||||
|
||||
@@ -428,6 +469,18 @@ void FireValues(int slot, void *L, int firstArgIdx, int nargs) {
|
||||
}
|
||||
|
||||
void RetryClaims() {
|
||||
// A table rebuilt behind our back (no FrameScript_Initialize in between)
|
||||
// leaves every cached slot pointing at whatever engine event now sits at
|
||||
// that id. Check the claims against the live table and start over if any
|
||||
// moved — we run before the original registers, so no frame has joined a
|
||||
// chain in the new table yet.
|
||||
for (int i = 0; i < g_reservedCount; ++i) {
|
||||
if (Stale(g_reserved[i])) {
|
||||
Debug::Log::Printf("[event] table rebuilt under cached slots — re-claiming");
|
||||
ForgetClaims();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Grow the table first if the low gap pool is short (dormant valve).
|
||||
// Runs at most once per table build; on the first call every chain is
|
||||
// still empty (this is the first registration), which is the only safe
|
||||
@@ -435,18 +488,15 @@ void RetryClaims() {
|
||||
Grow();
|
||||
for (int i = 0; i < g_reservedCount; ++i) {
|
||||
if (g_reserved[i].slot < 0)
|
||||
g_reserved[i].slot = TryClaim(g_reserved[i].name);
|
||||
Claim(g_reserved[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void EnableWrites() { g_writesEnabled = true; }
|
||||
|
||||
void PrepareForReload() {
|
||||
for (int i = 0; i < g_reservedCount; ++i)
|
||||
g_reserved[i].slot = g_reserved[i].fixedId;
|
||||
ForgetClaims();
|
||||
g_writesEnabled = false;
|
||||
g_warnedTableFull = false; // fresh table after the rebuild — re-arm
|
||||
g_growLatched = false; // re-arm the one-shot grow for the new table
|
||||
}
|
||||
|
||||
static const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ inline void FireIdSuccess(int eventID, int id, bool success) {
|
||||
// catch any reservations that couldn't claim earlier.
|
||||
void RetryClaims();
|
||||
|
||||
// Internal: permit `TryClaim` to write to the event table. Held
|
||||
// Internal: permit `Claim` to write to the event table. Held
|
||||
// closed until `LoadScriptFunctions_h` returns, so the boot phase
|
||||
// (during which the engine and SuperWoWhook fire many internal
|
||||
// `RegisterEvent` calls) can't trigger our writes. Writing during
|
||||
|
||||
@@ -39,19 +39,8 @@ namespace {
|
||||
|
||||
// FUN_FONTSTRING_FONT_HEIGHT — __thiscall(fs, scaled-on-stack); dummy-EDX form.
|
||||
using FsFontHeight_t = float(__fastcall *)(void *fs, void *edx, int scaled);
|
||||
using GetScaledScreenAspect_t = float(__cdecl *)();
|
||||
// Declared double so the quotient is read from ST0 with `fstp qword`, as
|
||||
// GetFont stores it, not narrowed to float.
|
||||
using DivideByScreenScaleX_t = double(__stdcall *)(float);
|
||||
|
||||
// GetFont's push (0x0079D4A5..0x0079D4C4). `aspect * 1024` is exact, so the
|
||||
// float product rounds once, as GetFont's `fstp dword` does.
|
||||
double ToUiHeight(float internal) {
|
||||
const float aspect =
|
||||
reinterpret_cast<GetScaledScreenAspect_t>(Offsets::FUN_COORD_GET_SCALED_SCREEN_ASPECT)();
|
||||
const float scaled = aspect * static_cast<float>(Offsets::UI_COORD_SCALE_UNIT) * internal;
|
||||
return reinterpret_cast<DivideByScreenScaleX_t>(Offsets::FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X)(scaled);
|
||||
}
|
||||
// GetFont's push (0x0079D4A5..0x0079D4C4).
|
||||
double ToUiHeight(float internal) { return Game::InternalToPixel(internal); }
|
||||
|
||||
void *FontStringSelf(void *L) { return Game::Lua::ResolveFontString(L); }
|
||||
void *FontSelf(void *L) { return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_FONT_LUA_TYPE_ID); }
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Layout.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -24,9 +25,6 @@ namespace {
|
||||
|
||||
// FUN_HANDLE_RELEASE — DecRef a ref-counted handle. __fastcall(handle).
|
||||
using HandleRelease_t = void(__fastcall *)(void *handle);
|
||||
// FUN_LAYOUT_RESIZE — region layout resize (0 = invalidate). __thiscall(region,
|
||||
// int); dummy-EDX form.
|
||||
using LayoutInvalidate_t = void(__fastcall *)(void *region, void *edx, int arg);
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -39,8 +37,7 @@ void Invalidate(void *fs) {
|
||||
Game::Ref<void *>(fs, Offsets::OFF_FONTSTRING_TEXT_BLOCK) = nullptr;
|
||||
}
|
||||
Game::Ref<uint32_t>(fs, Offsets::OFF_FONTSTRING_DIRTY_FLAGS) &= ~1u;
|
||||
void *region = reinterpret_cast<void *>(reinterpret_cast<uintptr_t>(fs) + 0x24);
|
||||
reinterpret_cast<LayoutInvalidate_t>(Offsets::FUN_LAYOUT_RESIZE)(region, nullptr, 0);
|
||||
Frame::Layout::Resize(fs, false);
|
||||
}
|
||||
|
||||
} // namespace FontString::Layout
|
||||
|
||||
+20
-34
@@ -54,6 +54,8 @@
|
||||
#include "Offsets.h"
|
||||
#include "frame/ClipChildren.h"
|
||||
#include "frame/Destruction.h"
|
||||
#include "frame/Layout.h"
|
||||
#include "gx/Device.h"
|
||||
#include "texture/Load.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -63,19 +65,8 @@ namespace Frame::AlphaGradient {
|
||||
|
||||
namespace {
|
||||
|
||||
using Matrix_t = void(__fastcall *)(float *m);
|
||||
using SetProjection_t = void(__fastcall *)(const float *m);
|
||||
using GetViewport_t = void(__fastcall *)(float *minX, float *maxX, float *minY, float *maxY, float *minZ,
|
||||
float *maxZ);
|
||||
using SetViewport_t = void(__stdcall *)(float minX, float maxX, float minY, float maxY, float minZ, float maxZ);
|
||||
using Convert_t = void(__fastcall *)(float *outX, float *outY, float x, float y);
|
||||
using GetRect_t = int(__fastcall *)(void *layout, void *edx, float *out);
|
||||
using GetRenderable_t = void *(__fastcall *)(void *hTex, int force, void *);
|
||||
using RsSet_t = void(__fastcall *)(int selector, int value);
|
||||
using RsSetPtr_t = void(__fastcall *)(int selector, const void *value);
|
||||
using StateOp_t = void(__fastcall *)();
|
||||
using TexMtxLoad_t = void(__fastcall *)(int unit, const float *m);
|
||||
using TexMtxOp_t = void(__fastcall *)(int unit);
|
||||
using SetFrameFlag_t = void(__fastcall *)(void *frame, void *edx, uint32_t mask, int on);
|
||||
|
||||
// A rect: {bottom, left, top, right}.
|
||||
@@ -135,7 +126,7 @@ void *RampTexture() {
|
||||
if (g_ramp == nullptr)
|
||||
g_ramp = Texture::CreateFromPixels("ClassicAPI\\AlphaGradient", 2, 2, kRampTexels, Texture::DefaultFlags());
|
||||
return g_ramp != nullptr
|
||||
? reinterpret_cast<GetRenderable_t>(Offsets::FUN_TEXTURE_GET_RENDERABLE)(g_ramp, 1, nullptr)
|
||||
? Texture::DeviceTexture(g_ramp)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
@@ -155,13 +146,12 @@ void RampColumn(const Stage &s, int axis, float vMin, float vMax, float col[4])
|
||||
}
|
||||
|
||||
void Load(const Stage &s) {
|
||||
float p[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
|
||||
reinterpret_cast<Matrix_t>(Offsets::FUN_GX_GET_PROJECTION)(p);
|
||||
float minX, maxX, minY, maxY, minZ, maxZ;
|
||||
reinterpret_cast<GetViewport_t>(Offsets::FUN_GX_GET_VIEWPORT)(&minX, &maxX, &minY, &maxY, &minZ, &maxZ);
|
||||
Gx::Matrix p;
|
||||
Gx::GetProjection(p);
|
||||
const Gx::Viewport vp = Gx::GetViewport();
|
||||
float u[4], v[4];
|
||||
RampColumn(s, 0, minX, maxX, u);
|
||||
RampColumn(s, 1, minY, maxY, v);
|
||||
RampColumn(s, 0, vp.minX, vp.maxX, u);
|
||||
RampColumn(s, 1, vp.minY, vp.maxY, v);
|
||||
float m[16];
|
||||
for (int r = 0; r < 4; ++r) {
|
||||
const float *row = p + r * 4;
|
||||
@@ -170,7 +160,7 @@ void Load(const Stage &s) {
|
||||
m[r * 4 + 2] = row[3];
|
||||
m[r * 4 + 3] = 0.0f;
|
||||
}
|
||||
reinterpret_cast<TexMtxLoad_t>(Offsets::FUN_GX_TEXMTX_LOAD)(s.unit, m);
|
||||
Gx::SetTransform(s.unit, m);
|
||||
}
|
||||
|
||||
// Appends the stages of `frame`'s gradient at g_stages[g_count + n]. Returns
|
||||
@@ -180,11 +170,9 @@ int AddStages(const void *frame, int n, int lastUnit) {
|
||||
if (it == g_gradients.end())
|
||||
return n;
|
||||
float rect[4];
|
||||
void *layout = const_cast<uint8_t *>(static_cast<const uint8_t *>(frame) + Offsets::OFF_REGION_ANCHOR);
|
||||
if (reinterpret_cast<GetRect_t>(Offsets::FUN_REGION_GET_RECT)(layout, nullptr, rect) == 0)
|
||||
if (!Frame::Layout::GetRect(frame, rect))
|
||||
return n;
|
||||
const float scale = Game::Read<float>(frame, Offsets::OFF_REGION_EFFECTIVE_SCALE);
|
||||
const auto toNdc = reinterpret_cast<Convert_t>(Offsets::FUN_COORD_DDC_TO_NDC);
|
||||
for (int index = 0; index < 2; ++index) {
|
||||
const float lx = it->second.x[index], ly = it->second.y[index];
|
||||
if (!(lx > 0.0f) && !(ly > 0.0f))
|
||||
@@ -198,8 +186,8 @@ int AddStages(const void *frame, int n, int lastUnit) {
|
||||
const float dy = Game::PixelToInternal(ly * scale) * (index == 0 ? -1.0f : 1.0f);
|
||||
Stage &s = g_stages[g_count + n];
|
||||
s.unit = 1 + g_count + n;
|
||||
toNdc(&s.p0[0], &s.p0[1], ex, ey);
|
||||
toNdc(&s.p1[0], &s.p1[1], ex + dx, ey + dy);
|
||||
Frame::Layout::DdcToNdc(&s.p0[0], &s.p0[1], ex, ey);
|
||||
Frame::Layout::DdcToNdc(&s.p1[0], &s.p1[1], ex + dx, ey + dy);
|
||||
s.on[0] = lx > 0.0f && s.p1[0] != s.p0[0];
|
||||
s.on[1] = ly > 0.0f && s.p1[1] != s.p0[1];
|
||||
if (s.on[0] || s.on[1])
|
||||
@@ -355,16 +343,14 @@ int Begin(const void *frame, bool ancestors) {
|
||||
void *ramp = RampTexture();
|
||||
if (ramp == nullptr)
|
||||
return 0;
|
||||
const auto rs = reinterpret_cast<RsSet_t>(Offsets::FUN_GX_RS_SET);
|
||||
const auto rsPtr = reinterpret_cast<RsSetPtr_t>(Offsets::FUN_GX_RS_SET_PTR);
|
||||
reinterpret_cast<StateOp_t>(Offsets::FUN_GX_STATE_PUSH)();
|
||||
Gx::PushStates();
|
||||
for (int i = g_count; i < g_count + n; ++i) {
|
||||
const int unit = g_stages[i].unit;
|
||||
rsPtr(Offsets::GXRS_TEXTURE0 + unit, ramp);
|
||||
rs(Offsets::GXRS_COMBINER0 + unit, Offsets::GXRS_COMBINE_MODULATE);
|
||||
rs(Offsets::GXRS_TEXGEN0 + unit, Offsets::GXRS_TEXGEN_CAMERA_POS);
|
||||
rs(Offsets::GXRS_TEXXFORM0 + unit, Offsets::GXRS_TEXXFORM_PROJECTED);
|
||||
reinterpret_cast<TexMtxOp_t>(Offsets::FUN_GX_TEXMTX_PUSH)(unit);
|
||||
Gx::SetState(Offsets::GXRS_TEXTURE0 + unit, ramp);
|
||||
Gx::SetState(Offsets::GXRS_COMBINER0 + unit, Offsets::GXRS_COMBINE_MODULATE);
|
||||
Gx::SetState(Offsets::GXRS_TEXGEN0 + unit, Offsets::GXRS_TEXGEN_CAMERA_POS);
|
||||
Gx::SetState(Offsets::GXRS_TEXXFORM0 + unit, Offsets::GXRS_TEXXFORM_PROJECTED);
|
||||
Gx::PushTransform(unit);
|
||||
Load(g_stages[i]);
|
||||
}
|
||||
g_count += n;
|
||||
@@ -375,9 +361,9 @@ void End(int stages) {
|
||||
if (stages == 0)
|
||||
return;
|
||||
for (int i = g_count - 1; i >= g_count - stages; --i)
|
||||
reinterpret_cast<TexMtxOp_t>(Offsets::FUN_GX_TEXMTX_POP)(g_stages[i].unit);
|
||||
Gx::PopTransform(g_stages[i].unit);
|
||||
g_count -= stages;
|
||||
reinterpret_cast<StateOp_t>(Offsets::FUN_GX_STATE_POP)();
|
||||
Gx::PopStates();
|
||||
}
|
||||
|
||||
int StagesInUse() { return g_count; }
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Layout.h"
|
||||
|
||||
#include <cfloat>
|
||||
#include <cstdint>
|
||||
@@ -43,20 +44,12 @@ struct Rect {
|
||||
float bottom, left, top, right;
|
||||
};
|
||||
|
||||
using GetRect_t = int(__fastcall *)(void *layout, void *edx, Rect *out);
|
||||
using LayoutDirty_t = int(__fastcall *)(void *layout);
|
||||
using Resize_t = void(__fastcall *)(void *layout, void *edx, int force);
|
||||
|
||||
void *LayoutOf(void *region) { return static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR; }
|
||||
|
||||
// Grows `rect` by a region's own rect, resolving a pending layout first.
|
||||
void AddRegionRect(void *region, Rect &rect) {
|
||||
void *layout = LayoutOf(region);
|
||||
if (reinterpret_cast<LayoutDirty_t>(Offsets::FUN_REGION_LAYOUT_DIRTY)(layout) != 0)
|
||||
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(layout, nullptr, 1);
|
||||
Rect own{};
|
||||
if (reinterpret_cast<GetRect_t>(Offsets::FUN_REGION_GET_RECT)(layout, nullptr, &own) == 0)
|
||||
float r[4];
|
||||
if (!Frame::Layout::GetResolvedRect(region, r))
|
||||
return;
|
||||
const Rect own{r[0], r[1], r[2], r[3]};
|
||||
rect.left = own.left < rect.left ? own.left : rect.left;
|
||||
rect.right = own.right > rect.right ? own.right : rect.right;
|
||||
rect.bottom = own.bottom < rect.bottom ? own.bottom : rect.bottom;
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Layout.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
@@ -54,10 +55,7 @@ std::unordered_map<void *, Insets> g_insets;
|
||||
using CalcRect_t = int(__thiscall *)(void *layout, float *rect);
|
||||
CalcRect_t g_origCalcRect = nullptr;
|
||||
|
||||
using Invalidate_t = void(__thiscall *)(void *layout, int arg);
|
||||
|
||||
void *LayoutOf(void *frame) { return static_cast<uint8_t *>(frame) + Offsets::OFF_REGION_ANCHOR; }
|
||||
void *FrameOf(void *layout) { return static_cast<uint8_t *>(layout) - Offsets::OFF_REGION_ANCHOR; }
|
||||
|
||||
// A length in the frame's own units (what GetLeft returns) in layout units:
|
||||
// the inverse of Script GetLeft's conversion.
|
||||
@@ -70,7 +68,7 @@ int __fastcall CalcRect_h(void *layout, void * /*edx*/, float *rect) {
|
||||
const uint32_t flags = Game::Read<uint32_t>(layout, Offsets::OFF_LAYOUT_FLAGS);
|
||||
if ((flags & Offsets::LAYOUT_FLAG_CLAMPED) == 0 || g_insets.empty())
|
||||
return g_origCalcRect(layout, rect);
|
||||
void *frame = FrameOf(layout);
|
||||
void *frame = Frame::Layout::OwnerOf(layout);
|
||||
const auto it = g_insets.find(frame);
|
||||
if (it == g_insets.end())
|
||||
return g_origCalcRect(layout, rect);
|
||||
@@ -144,7 +142,7 @@ int __fastcall Script_SetClampRectInsets(void *L) {
|
||||
else
|
||||
g_insets[frame] = in;
|
||||
// Re-lay the frame out, as SetClampedToScreen does after changing the clamp.
|
||||
reinterpret_cast<Invalidate_t>(Offsets::FUN_LAYOUT_RESIZE)(LayoutOf(frame), 0);
|
||||
Frame::Layout::Resize(frame, false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
+11
-27
@@ -66,10 +66,13 @@
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Layout.h"
|
||||
#include "frame/AlphaGradient.h"
|
||||
#include "frame/ClipChildren.h"
|
||||
#include "frame/Destruction.h"
|
||||
#include "frame/LayerChange.h"
|
||||
#include "frame/ParentChange.h"
|
||||
#include "gx/Device.h"
|
||||
#include "xml/FrameLoad.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -85,14 +88,8 @@ using BatchOp_t = void(__fastcall *)(void *batch);
|
||||
using Callback_t = void(__fastcall *)(void *arg);
|
||||
using AddCallback_t = void(__fastcall *)(void *batch, void *edx, Callback_t fn, void *arg);
|
||||
using SetFrameFlag_t = void(__fastcall *)(void *frame, void *edx, uint32_t mask, int on);
|
||||
using NotifyLayerChanged_t = void(__fastcall *)(void *frame, void *edx, int layer);
|
||||
using HitTest_t = int(__fastcall *)(void *frame, void *edx, const float *point);
|
||||
using Matrix_t = void(__fastcall *)(float *m);
|
||||
using GetViewport_t = void(__fastcall *)(float *minX, float *maxX, float *minY, float *maxY, float *minZ,
|
||||
float *maxZ);
|
||||
using SetViewport_t = void(__stdcall *)(float minX, float maxX, float minY, float maxY, float minZ, float maxZ);
|
||||
using SetupOrtho_t = void(__fastcall *)(const float *rect, const float *point, int unused);
|
||||
using Convert_t = void(__fastcall *)(float *outX, float *outY, float x, float y);
|
||||
|
||||
// A hit rect: {bottom, left, top, right}.
|
||||
enum { BOTTOM, LEFT, TOP, RIGHT };
|
||||
@@ -218,18 +215,11 @@ bool ClipRect(const void *root, float rect[4]) {
|
||||
// The ScrollFrame's draw (0x00787220) with `rect` in place of its hit rect
|
||||
// and `draw(arg)` in place of its scroll child's.
|
||||
void DrawInside(float rect[4], Callback_t draw, void *arg) {
|
||||
float projection[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
|
||||
float view[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
|
||||
reinterpret_cast<Matrix_t>(Offsets::FUN_GX_GET_PROJECTION)(projection);
|
||||
reinterpret_cast<Matrix_t>(Offsets::FUN_GX_GET_VIEW)(view);
|
||||
float vpMinX, vpMaxX, vpMinY, vpMaxY, vpMinZ, vpMaxZ;
|
||||
reinterpret_cast<GetViewport_t>(Offsets::FUN_GX_GET_VIEWPORT)(&vpMinX, &vpMaxX, &vpMinY, &vpMaxY, &vpMinZ,
|
||||
&vpMaxZ);
|
||||
|
||||
const auto toDdc = reinterpret_cast<Convert_t>(Offsets::FUN_COORD_NDC_TO_DDC);
|
||||
const Gx::SavedTransforms saved;
|
||||
const Gx::Viewport &vp = saved.SavedViewport();
|
||||
float minX = 0, minY = 0, maxX = 0, maxY = 0;
|
||||
toDdc(&minX, &minY, vpMinX, vpMinY);
|
||||
toDdc(&maxX, &maxY, vpMaxX, vpMaxY);
|
||||
Frame::Layout::NdcToDdc(&minX, &minY, vp.minX, vp.minY);
|
||||
Frame::Layout::NdcToDdc(&maxX, &maxY, vp.maxX, vp.maxY);
|
||||
if (minX > rect[LEFT])
|
||||
rect[LEFT] = minX;
|
||||
if (maxX < rect[RIGHT])
|
||||
@@ -241,9 +231,8 @@ void DrawInside(float rect[4], Callback_t draw, void *arg) {
|
||||
|
||||
const float point[2] = {0, 0};
|
||||
reinterpret_cast<SetupOrtho_t>(Offsets::FUN_SETUP_ORTHO_TRANSFORMS)(rect, point, 0);
|
||||
const auto toNdc = reinterpret_cast<Convert_t>(Offsets::FUN_COORD_DDC_TO_NDC);
|
||||
toNdc(&rect[LEFT], &rect[BOTTOM], rect[LEFT], rect[BOTTOM]);
|
||||
toNdc(&rect[RIGHT], &rect[TOP], rect[RIGHT], rect[TOP]);
|
||||
Frame::Layout::DdcToNdc(&rect[LEFT], &rect[BOTTOM], rect[LEFT], rect[BOTTOM]);
|
||||
Frame::Layout::DdcToNdc(&rect[RIGHT], &rect[TOP], rect[RIGHT], rect[TOP]);
|
||||
if (rect[LEFT] < 0.0f) {
|
||||
rect[RIGHT] -= rect[LEFT];
|
||||
rect[LEFT] = 0.0f;
|
||||
@@ -261,14 +250,9 @@ void DrawInside(float rect[4], Callback_t draw, void *arg) {
|
||||
rect[TOP] = 1.0f;
|
||||
}
|
||||
if (rect[LEFT] < rect[RIGHT] && rect[BOTTOM] < rect[TOP]) {
|
||||
reinterpret_cast<SetViewport_t>(Offsets::FUN_GX_SET_VIEWPORT)(rect[LEFT], rect[RIGHT], rect[BOTTOM],
|
||||
rect[TOP], 0.0f, 1.0f);
|
||||
Gx::SetViewport({rect[LEFT], rect[RIGHT], rect[BOTTOM], rect[TOP], 0.0f, 1.0f});
|
||||
draw(arg);
|
||||
}
|
||||
|
||||
reinterpret_cast<Matrix_t>(Offsets::FUN_GX_SET_PROJECTION)(projection);
|
||||
reinterpret_cast<Matrix_t>(Offsets::FUN_GX_SET_VIEW)(view);
|
||||
reinterpret_cast<SetViewport_t>(Offsets::FUN_GX_SET_VIEWPORT)(vpMinX, vpMaxX, vpMinY, vpMaxY, vpMinZ, vpMaxZ);
|
||||
}
|
||||
|
||||
void __fastcall DrawClipped(void *arg) {
|
||||
@@ -345,7 +329,7 @@ const Game::HookAutoRegister _hitTestHook{Offsets::FUN_FRAME_HIT_TEST, reinterpr
|
||||
|
||||
void MarkLayers(void *frame) {
|
||||
for (int layer = 0; layer < Offsets::FRAME_LAYER_COUNT; ++layer)
|
||||
reinterpret_cast<NotifyLayerChanged_t>(Offsets::FUN_FRAME_NOTIFY_LAYER_CHANGED)(frame, nullptr, layer);
|
||||
Frame::LayerChange::NotifyFrame(frame, layer);
|
||||
}
|
||||
|
||||
// Marks every layer of each frame under `frame` changed.
|
||||
|
||||
+56
-165
@@ -83,9 +83,12 @@
|
||||
#include "api/Documentation.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "frame/ExtraScripts.h"
|
||||
#include "frame/LayerChange.h"
|
||||
#include "frame/Layout.h"
|
||||
#include "frame/Region.h"
|
||||
#include "frame/RenderBatch.h"
|
||||
#include "frame/ScriptArgs.h"
|
||||
#include "frame/Subclass.h"
|
||||
#include "text/NumericFormatter.h"
|
||||
#include "texture/Load.h"
|
||||
#include "time/Clock.h"
|
||||
@@ -185,9 +188,7 @@ struct Rect {
|
||||
};
|
||||
const Rect &RectOf(void *frame) { return *Game::Ptr<Rect>(frame, Offsets::OFF_REGION_RECT); }
|
||||
|
||||
void *FrameOfLayout(void *layout) {
|
||||
return static_cast<uint8_t *>(layout) - Offsets::OFF_FRAME_LAYOUT_SUBOBJECT;
|
||||
}
|
||||
using Frame::Layout::OwnerOf;
|
||||
|
||||
// The alpha the frame draws with — 4.3.4's m_alpha * m_parentAlpha / 255.
|
||||
// 1.12's alpha setter copies one byte down the tree instead of multiplying.
|
||||
@@ -224,28 +225,29 @@ uint32_t ColorByte(double c) {
|
||||
return static_cast<uint32_t>(c * 255.0 + 0.5);
|
||||
}
|
||||
|
||||
// ---- vtables ----------------------------------------------------------------
|
||||
// ---- the class ----------------------------------------------------------------
|
||||
|
||||
// An MSVC vtable with its RTTI locator in front, as the engine's are laid out.
|
||||
template <size_t N>
|
||||
struct VTable {
|
||||
const void *locator;
|
||||
const void *slots[N];
|
||||
};
|
||||
|
||||
VTable<Offsets::VTBL_SIMPLEFRAME_SLOTS> g_vtable;
|
||||
VTable<Offsets::VTBL_SIMPLEFRAME_LAYOUT_SLOTS> g_layoutVtable;
|
||||
|
||||
// CSimpleFrame's own slot, for the base call of an override.
|
||||
template <typename Fn>
|
||||
Fn Base(uintptr_t slot) {
|
||||
return Game::Read<Fn>(reinterpret_cast<const void *>(Offsets::VTBL_SIMPLEFRAME), slot);
|
||||
}
|
||||
template <typename Fn>
|
||||
Fn LayoutBase(uintptr_t slot) {
|
||||
return Game::Read<Fn>(reinterpret_cast<const void *>(Offsets::VTBL_SIMPLEFRAME_LAYOUT), slot);
|
||||
// CGCooldown::~CGCooldown, before CSimpleFrame's deleting destructor.
|
||||
void Destroy(void *frame) {
|
||||
Fields &f = F(frame);
|
||||
for (Slot &s : f.slots) {
|
||||
Texture::ReleaseHandle(s.texture);
|
||||
s.texture = nullptr;
|
||||
}
|
||||
Text::NumericFormatter::Release(f.countdownFormatter);
|
||||
f.countdownFormatter = nullptr;
|
||||
delete f.countdownText;
|
||||
f.countdownText = nullptr;
|
||||
}
|
||||
|
||||
Frame::Subclass::Class<Offsets::VTBL_SIMPLEFRAME_SLOTS, Offsets::VTBL_SIMPLEFRAME_LAYOUT_SLOTS>
|
||||
g_class{"Cooldown",
|
||||
Offsets::VTBL_SIMPLEFRAME,
|
||||
Offsets::VTBL_SIMPLEFRAME_LAYOUT,
|
||||
Offsets::FUN_SIMPLEFRAME_CONSTRUCTOR,
|
||||
kObjectSize,
|
||||
&Destroy};
|
||||
|
||||
// 1.12's CSimpleFrame::Show / Hide, as Script Show / Hide make them.
|
||||
void Show(void *frame) {
|
||||
using Slot_t = void(__fastcall *)(void *self, void *edx);
|
||||
@@ -259,10 +261,7 @@ void Hide(void *frame) {
|
||||
Game::Read<Slot_t>(Game::Read<void *>(frame, 0), Offsets::VTBL_FRAME_HIDE)(frame, nullptr);
|
||||
}
|
||||
|
||||
void NotifyLayerChanged(void *frame, int layer) {
|
||||
using Fn_t = void(__fastcall *)(void *self, void *edx, int layer);
|
||||
reinterpret_cast<Fn_t>(Offsets::FUN_FRAME_NOTIFY_LAYER_CHANGED)(frame, nullptr, layer);
|
||||
}
|
||||
using Frame::LayerChange::NotifyFrame;
|
||||
|
||||
// ---- textures ---------------------------------------------------------------
|
||||
|
||||
@@ -371,8 +370,6 @@ void MapStarTexCoords(Fields &f, const Rect &r) {
|
||||
void AnimateVerts(void *frame) {
|
||||
Fields &f = F(frame);
|
||||
const Rect &r = RectOf(frame);
|
||||
const float cx = (r.minX + r.maxX) * 0.5f;
|
||||
const float cy = (r.minY + r.maxY) * 0.5f;
|
||||
|
||||
if (!f.flashing) {
|
||||
const float elapsed = static_cast<float>(f.elapsed);
|
||||
@@ -482,7 +479,7 @@ void SetSlotTexture(void *frame, TextureSlot slot, const char *file, const uint3
|
||||
if (base != nullptr)
|
||||
s.base = *base;
|
||||
f.texturesSet |= 1u << slot;
|
||||
NotifyLayerChanged(frame, kSlotLayer[slot]);
|
||||
NotifyFrame(frame, kSlotLayer[slot]);
|
||||
UpdateColors(frame);
|
||||
}
|
||||
|
||||
@@ -633,25 +630,12 @@ void UpdateCountdown(void *frame) {
|
||||
|
||||
// ---- the overrides ------------------------------------------------------------
|
||||
|
||||
using DeletingDestructor_t = void *(__fastcall *)(void *self, void *edx, uint32_t flags);
|
||||
using GetScriptMethod_t = int(__fastcall *)(void *self, void *edx, void *L, const char *name);
|
||||
using IsA_t = bool(__fastcall *)(void *self, void *edx, int typeId);
|
||||
using IsObjectType_t = bool(__fastcall *)(void *self, void *edx, const char *name);
|
||||
using SetFrameAlpha_t = void(__fastcall *)(void *self, void *edx, uint8_t alpha);
|
||||
using OnFrameUpdate_t = void(__fastcall *)(void *self, void *edx, float elapsedSec);
|
||||
using EmitLayer_t = void(__fastcall *)(void *self, void *edx, void *batch, int layer);
|
||||
using Frame::Subclass::EmitLayer_t;
|
||||
using Frame::Subclass::LoadXML_t;
|
||||
using Frame::Subclass::OnFrameUpdate_t;
|
||||
using Frame::Subclass::SetFrameAlpha_t;
|
||||
using OnFrameSizeChanged_t = void(__fastcall *)(void *layout, void *edx, const float *oldRect);
|
||||
using LoadXML_t = uintptr_t(__fastcall *)(void *layout, void *edx, const void *node,
|
||||
void *status);
|
||||
using PushMethod_t = int(__fastcall *)(void *L, const char *name, void *hash);
|
||||
|
||||
// The class's method hash and type id (Offsets.h "Per-class method hashes").
|
||||
uint32_t g_hash[Offsets::METHOD_HASH_SIZE / 4];
|
||||
int g_typeId = 0;
|
||||
|
||||
uintptr_t TypeIdVar() { return reinterpret_cast<uintptr_t>(&g_typeId); }
|
||||
|
||||
const Frame::ExtraScripts::Script g_onCooldownDone{"oncooldowndone", TypeIdVar()};
|
||||
const Frame::ExtraScripts::Script g_onCooldownDone{"oncooldowndone", g_class.TypeIdVar()};
|
||||
|
||||
// From the update walk, where the engine runs OnUpdate.
|
||||
void RunOnCooldownDone(void *frame) {
|
||||
@@ -664,52 +648,15 @@ void RunOnCooldownDone(void *frame) {
|
||||
Frame::ScriptArgs::RunSlotWithValues(L, slot, frame, Game::Lua::GetTop(L) + 1, 0);
|
||||
}
|
||||
|
||||
// CGCooldown::~CGCooldown, then CSimpleFrame's deleting destructor.
|
||||
void *__fastcall DeletingDestructor(void *self, void *, uint32_t flags) {
|
||||
Fields &f = F(self);
|
||||
for (Slot &s : f.slots) {
|
||||
Texture::ReleaseHandle(s.texture);
|
||||
s.texture = nullptr;
|
||||
}
|
||||
Text::NumericFormatter::Release(f.countdownFormatter);
|
||||
f.countdownFormatter = nullptr;
|
||||
delete f.countdownText;
|
||||
f.countdownText = nullptr;
|
||||
return Base<DeletingDestructor_t>(Offsets::VTBL_FRAME_DELETING_DESTRUCTOR)(self, nullptr,
|
||||
flags);
|
||||
}
|
||||
|
||||
void *__fastcall LayoutDeletingDestructor(void *layout, void *, uint32_t flags) {
|
||||
return DeletingDestructor(FrameOfLayout(layout), nullptr, flags);
|
||||
}
|
||||
|
||||
int __fastcall GetScriptMethod(void *self, void *, void *L, const char *name) {
|
||||
if (reinterpret_cast<PushMethod_t>(Offsets::FUN_FRAMESCRIPT_PUSH_METHOD)(L, name, g_hash))
|
||||
return 1;
|
||||
return Base<GetScriptMethod_t>(Offsets::VTBL_FRAME_GET_SCRIPT_METHOD)(self, nullptr, L, name);
|
||||
}
|
||||
|
||||
bool __fastcall IsA(void *self, void *, int typeId) {
|
||||
return (g_typeId != 0 && typeId == g_typeId) ||
|
||||
Base<IsA_t>(Offsets::VTBL_FRAME_IS_A)(self, nullptr, typeId);
|
||||
}
|
||||
|
||||
bool __fastcall IsObjectType(void *self, void *, const char *name) {
|
||||
return (name != nullptr && _stricmp(name, "Cooldown") == 0) ||
|
||||
Base<IsObjectType_t>(Offsets::VTBL_FRAME_IS_OBJECT_TYPE)(self, nullptr, name);
|
||||
}
|
||||
|
||||
const char *__fastcall GetObjectType(void *, void *) { return "Cooldown"; }
|
||||
|
||||
// OFFSET (4.3.4): 0x009FF570 (UpdateAlpha)
|
||||
void __fastcall SetFrameAlpha(void *self, void *, uint8_t alpha) {
|
||||
Base<SetFrameAlpha_t>(Offsets::VTBL_FRAME_SET_FRAME_ALPHA)(self, nullptr, alpha);
|
||||
g_class.Base<SetFrameAlpha_t>(Offsets::VTBL_FRAME_SET_FRAME_ALPHA)(self, nullptr, alpha);
|
||||
UpdateColors(self);
|
||||
}
|
||||
|
||||
// OFFSET (4.3.4): 0x009FF5A0 (OnLayerUpdate)
|
||||
void __fastcall OnFrameUpdate(void *self, void *, float elapsedSec) {
|
||||
Base<OnFrameUpdate_t>(Offsets::VTBL_FRAME_ON_FRAME_UPDATE)(self, nullptr, elapsedSec);
|
||||
g_class.Base<OnFrameUpdate_t>(Offsets::VTBL_FRAME_ON_FRAME_UPDATE)(self, nullptr, elapsedSec);
|
||||
Fields &f = F(self);
|
||||
// 4.3.4: `now <= start ? 0 : now - start`, compared wrap-safe. A paused
|
||||
// frame reads the time it was paused at.
|
||||
@@ -757,7 +704,7 @@ void __fastcall OnFrameUpdate(void *self, void *, float elapsedSec) {
|
||||
|
||||
// OFFSET (4.3.4): 0x009FE830 (OnFrameRender)
|
||||
void __fastcall EmitLayer(void *self, void *, void *batch, int layer) {
|
||||
Base<EmitLayer_t>(Offsets::VTBL_FRAME_EMIT_LAYER)(self, nullptr, batch, layer);
|
||||
g_class.Base<EmitLayer_t>(Offsets::VTBL_FRAME_EMIT_LAYER)(self, nullptr, batch, layer);
|
||||
Fields &f = F(self);
|
||||
using RenderBatch::QueueStrip;
|
||||
void *swipe = f.slots[SLOT_SWIPE].texture;
|
||||
@@ -777,9 +724,9 @@ void __fastcall EmitLayer(void *self, void *, void *batch, int layer) {
|
||||
|
||||
// OFFSET (4.3.4): 0x009FF580
|
||||
void __fastcall OnFrameSizeChanged(void *layout, void *, const float *oldRect) {
|
||||
LayoutBase<OnFrameSizeChanged_t>(Offsets::VTBL_LAYOUT_ON_FRAME_SIZE_CHANGED)(layout, nullptr,
|
||||
g_class.LayoutBase<OnFrameSizeChanged_t>(Offsets::VTBL_LAYOUT_ON_FRAME_SIZE_CHANGED)(layout, nullptr,
|
||||
oldRect);
|
||||
ResetVerts(FrameOfLayout(layout));
|
||||
ResetVerts(OwnerOf(layout));
|
||||
}
|
||||
|
||||
// The stock CooldownFrameTemplate: a Model template (Cooldown.xml, 1.12).
|
||||
@@ -800,16 +747,15 @@ void LoadModernTemplate(void *layout) {
|
||||
nullptr);
|
||||
reinterpret_cast<SetAllPoints_t>(Offsets::FUN_LAYOUT_SET_ALL_POINTS)(layout, nullptr, parent,
|
||||
1);
|
||||
Hide(FrameOfLayout(layout));
|
||||
Hide(OwnerOf(layout));
|
||||
}
|
||||
|
||||
// A texture element's <Color> child into `color`, read as CSimpleTexture::
|
||||
// LoadXML reads one (it passes its load status too); false when it has none.
|
||||
bool XmlTextureColor(const void *node, void *status, uint32_t &color) {
|
||||
using ReadColor_t = int(__fastcall *)(const void *node, uint32_t *bgra, void *status);
|
||||
for (const void *c = Xml::FirstChild(node); c != nullptr; c = Xml::NextSibling(c)) {
|
||||
if (Xml::TagIs(c, "Color")) {
|
||||
reinterpret_cast<ReadColor_t>(Offsets::FUN_XML_READ_COLOR)(c, &color, status);
|
||||
Xml::ReadColor(c, color, status);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -826,10 +772,10 @@ uintptr_t __fastcall LoadXML(void *layout, void *, const void *node, void *statu
|
||||
return 0;
|
||||
}
|
||||
const uintptr_t result =
|
||||
LayoutBase<LoadXML_t>(Offsets::VTBL_XML_NODE_LOADER)(layout, nullptr, node, status);
|
||||
g_class.LayoutBase<LoadXML_t>(Offsets::VTBL_XML_NODE_LOADER)(layout, nullptr, node, status);
|
||||
if (node == nullptr)
|
||||
return result;
|
||||
Fields &f = F(FrameOfLayout(layout));
|
||||
Fields &f = F(OwnerOf(layout));
|
||||
if (const char *reverse = Xml::Attr(node, "reverse"))
|
||||
f.reverse = Game::StringToFlag(reverse);
|
||||
if (const char *drawEdge = Xml::Attr(node, "drawEdge"))
|
||||
@@ -853,7 +799,7 @@ uintptr_t __fastcall LoadXML(void *layout, void *, const void *node, void *statu
|
||||
if (!Xml::TagIs(c, kSlotTag[slot]))
|
||||
continue;
|
||||
uint32_t color = 0;
|
||||
SetSlotTexture(FrameOfLayout(layout), static_cast<TextureSlot>(slot),
|
||||
SetSlotTexture(OwnerOf(layout), static_cast<TextureSlot>(slot),
|
||||
Xml::Attr(c, "file"),
|
||||
XmlTextureColor(c, status, color) ? &color : nullptr);
|
||||
}
|
||||
@@ -862,44 +808,23 @@ uintptr_t __fastcall LoadXML(void *layout, void *, const void *node, void *statu
|
||||
}
|
||||
|
||||
void BuildVTables() {
|
||||
if (g_vtable.slots[0] != nullptr)
|
||||
if (!Frame::Subclass::BuildVTables<g_class>())
|
||||
return;
|
||||
std::memcpy(&g_vtable, reinterpret_cast<const void *>(Offsets::VTBL_SIMPLEFRAME - 4),
|
||||
sizeof(g_vtable));
|
||||
std::memcpy(&g_layoutVtable,
|
||||
reinterpret_cast<const void *>(Offsets::VTBL_SIMPLEFRAME_LAYOUT - 4),
|
||||
sizeof(g_layoutVtable));
|
||||
auto set = [](const void **slots, uintptr_t slot, const void *fn) { slots[slot / 4] = fn; };
|
||||
const void **v = g_vtable.slots;
|
||||
set(v, Offsets::VTBL_FRAME_DELETING_DESTRUCTOR, reinterpret_cast<const void *>(&DeletingDestructor));
|
||||
set(v, Offsets::VTBL_FRAME_GET_SCRIPT_METHOD, reinterpret_cast<const void *>(&GetScriptMethod));
|
||||
set(v, Offsets::VTBL_FRAME_IS_A, reinterpret_cast<const void *>(&IsA));
|
||||
set(v, Offsets::VTBL_FRAME_IS_OBJECT_TYPE, reinterpret_cast<const void *>(&IsObjectType));
|
||||
set(v, Offsets::VTBL_FRAME_GET_OBJECT_TYPE, reinterpret_cast<const void *>(&GetObjectType));
|
||||
set(v, Offsets::VTBL_FRAME_SET_FRAME_ALPHA, reinterpret_cast<const void *>(&SetFrameAlpha));
|
||||
set(v, Offsets::VTBL_FRAME_ON_FRAME_UPDATE, reinterpret_cast<const void *>(&OnFrameUpdate));
|
||||
set(v, Offsets::VTBL_FRAME_EMIT_LAYER, reinterpret_cast<const void *>(&EmitLayer));
|
||||
const void **l = g_layoutVtable.slots;
|
||||
set(l, Offsets::VTBL_LAYOUT_ON_FRAME_SIZE_CHANGED,
|
||||
reinterpret_cast<const void *>(&OnFrameSizeChanged));
|
||||
set(l, Offsets::VTBL_LAYOUT_DELETING_DESTRUCTOR,
|
||||
reinterpret_cast<const void *>(&LayoutDeletingDestructor));
|
||||
set(l, Offsets::VTBL_XML_NODE_LOADER, reinterpret_cast<const void *>(&LoadXML));
|
||||
auto fn = [](auto f) { return reinterpret_cast<const void *>(f); };
|
||||
g_class.vtable.Set(Offsets::VTBL_FRAME_SET_FRAME_ALPHA, fn(&SetFrameAlpha));
|
||||
g_class.vtable.Set(Offsets::VTBL_FRAME_ON_FRAME_UPDATE, fn(&OnFrameUpdate));
|
||||
g_class.vtable.Set(Offsets::VTBL_FRAME_EMIT_LAYER, fn(&EmitLayer));
|
||||
g_class.layoutVtable.Set(Offsets::VTBL_LAYOUT_ON_FRAME_SIZE_CHANGED, fn(&OnFrameSizeChanged));
|
||||
g_class.layoutVtable.Set(Offsets::VTBL_XML_NODE_LOADER, fn(&LoadXML));
|
||||
}
|
||||
|
||||
// OFFSET (4.3.4): 0x008C6390 (CGCooldown::Create) — the "Cooldown" factory.
|
||||
void *__fastcall CreateCooldown(void *parent) {
|
||||
using SMemAlloc_t = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
|
||||
using Constructor_t = void(__fastcall *)(void *self, void *edx, void *parent);
|
||||
BuildVTables();
|
||||
// Zero-filled: 4.3.4's constructor zeroes every CGCooldown field.
|
||||
void *frame = reinterpret_cast<SMemAlloc_t>(Offsets::FUN_STORM_SMEM_ALLOC)(
|
||||
kObjectSize, __FILE__, __LINE__, 0x08);
|
||||
void *frame = Frame::Subclass::New<g_class>(parent);
|
||||
if (frame == nullptr)
|
||||
return nullptr;
|
||||
reinterpret_cast<Constructor_t>(Offsets::FUN_SIMPLEFRAME_CONSTRUCTOR)(frame, nullptr, parent);
|
||||
Game::Ref<const void *>(frame, 0) = g_vtable.slots;
|
||||
Game::Ref<const void *>(frame, Offsets::OFF_FRAME_LAYOUT_SUBOBJECT) = g_layoutVtable.slots;
|
||||
Fields &f = F(frame);
|
||||
f.modRate = 1.0f;
|
||||
f.drawSwipe = 1;
|
||||
@@ -917,7 +842,7 @@ void *__fastcall CreateCooldown(void *parent) {
|
||||
|
||||
// ---- the methods (CGCooldown_Scripts.cpp, then the later clients') -------------------
|
||||
|
||||
void *ResolveCooldown(void *L) { return Game::Lua::ResolveTypedObject(L, 1, TypeIdVar()); }
|
||||
void *ResolveCooldown(void *L) { return Game::Lua::ResolveTypedObject(L, 1, g_class.TypeIdVar()); }
|
||||
|
||||
// Seconds to ms on the GetTime() epoch, wrapping like the tick does.
|
||||
uint32_t Ms(double seconds) { return static_cast<uint32_t>(static_cast<int64_t>(seconds * 1000.0)); }
|
||||
@@ -1046,7 +971,7 @@ int __fastcall Script_SetDrawSwipe(void *L) {
|
||||
const uint32_t drawSwipe = Game::Lua::ToFlag(L, 2, true);
|
||||
if (drawSwipe != f.drawSwipe) {
|
||||
f.drawSwipe = drawSwipe;
|
||||
NotifyLayerChanged(frame, kSlotLayer[SLOT_SWIPE]);
|
||||
NotifyFrame(frame, kSlotLayer[SLOT_SWIPE]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1308,7 +1233,7 @@ int __fastcall Script_SetDrawEdge(void *L) {
|
||||
const uint32_t drawEdge = Game::Lua::ToFlag(L, 2, true);
|
||||
if (drawEdge != f.drawEdge) {
|
||||
f.drawEdge = drawEdge;
|
||||
NotifyLayerChanged(frame, kSlotLayer[SLOT_EDGE]);
|
||||
NotifyFrame(frame, kSlotLayer[SLOT_EDGE]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1561,55 +1486,21 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
{"GetCountdownFormatter", &kGetCountdownFormatter},
|
||||
};
|
||||
|
||||
// The engine's static initializer for a class hash (Model's 0x0076F5F0).
|
||||
void InitMethodHash() {
|
||||
if (g_hash[0] != 0)
|
||||
return;
|
||||
g_hash[0] = Offsets::VTBL_METHOD_HASH_ALLOCATOR;
|
||||
g_hash[1] = 0xC; // node link offset
|
||||
g_hash[2] = reinterpret_cast<uintptr_t>(&g_hash[2]);
|
||||
g_hash[3] = reinterpret_cast<uintptr_t>(&g_hash[2]) | 1;
|
||||
g_hash[9] = 0xFFFFFFFF; // no buckets yet
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
using RegisterFactory_t = int(__fastcall *)(const char *type, void *(__fastcall *)(void *),
|
||||
int unique);
|
||||
// The engine's frame-type table outlives the Lua state, so on a /reload
|
||||
// the name is already there and the registrar keeps the first one. This
|
||||
// pass runs before FrameXML loads (CGGameUI::Initialize calls
|
||||
// This pass runs before FrameXML loads (CGGameUI::Initialize calls
|
||||
// LoadScriptFunctions first), like the engine's own game-UI list
|
||||
// FUN_00495940, where 4.3.4 registers "Cooldown".
|
||||
InitMethodHash();
|
||||
g_countdownForCooldowns = CVar::Factory::Register(
|
||||
"countdownForCooldowns",
|
||||
"Whether to use number countdown instead of radial swipe for action button cooldowns or not.",
|
||||
Offsets::CVAR_CATEGORY_GAME, "0");
|
||||
reinterpret_cast<RegisterFactory_t>(Offsets::FUN_FRAMEXML_REGISTER_FACTORY)(
|
||||
"Cooldown", &CreateCooldown, 0);
|
||||
Api::Documentation::NameWidgetSystem(g_hash, "FrameAPICooldown");
|
||||
Game::Lua::RegisterFrameMethods(g_hash, g_methods, g_methodDocs);
|
||||
Frame::Subclass::RegisterFactory(g_class.name, &CreateCooldown);
|
||||
Api::Documentation::NameWidgetSystem(g_class.hash.Registry(), "FrameAPICooldown");
|
||||
Game::Lua::RegisterFrameMethods(g_class.hash.Registry(), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
// The hash's functions are references into the game Lua state, so they are
|
||||
// released with the engine's own class hashes, at the end of the game UI's
|
||||
// unregister pass while that state is still open.
|
||||
using UnregisterScriptFunctions_t = void(__cdecl *)();
|
||||
using UnregisterMethods_t = void(__fastcall *)(void *hash);
|
||||
UnregisterScriptFunctions_t UnregisterScriptFunctions_o = nullptr;
|
||||
|
||||
void __cdecl UnregisterScriptFunctions_h() {
|
||||
UnregisterScriptFunctions_o();
|
||||
reinterpret_cast<UnregisterMethods_t>(Offsets::FUN_FRAMESCRIPT_UNREGISTER_METHODS)(g_hash);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _unregisterHook{
|
||||
Offsets::FUN_GAMEUI_UNREGISTER_SCRIPT_FUNCTIONS,
|
||||
reinterpret_cast<void *>(&UnregisterScriptFunctions_h),
|
||||
reinterpret_cast<void **>(&UnregisterScriptFunctions_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Frame::Cooldown
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// The engine's two draw-layer invalidates: what to call after a change to
|
||||
// what a frame or a region draws, so the next batch rebuild picks it up.
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Frame::LayerChange {
|
||||
|
||||
// CSimpleFrame::NotifyLayerChanged (FUN_FRAME_NOTIFY_LAYER_CHANGED): one of
|
||||
// the frame's own draw layers.
|
||||
inline void NotifyFrame(void *frame, int layer) {
|
||||
using Fn_t = void(__fastcall *)(void *frame, void *edx, int layer);
|
||||
reinterpret_cast<Fn_t>(Offsets::FUN_FRAME_NOTIFY_LAYER_CHANGED)(frame, nullptr, layer);
|
||||
}
|
||||
|
||||
// CScriptRegion::NotifyLayerChanged (FUN_REGION_NOTIFY_LAYER_CHANGED): the
|
||||
// layer the region draws in.
|
||||
inline void NotifyRegion(void *region) {
|
||||
using Fn_t = void(__fastcall *)(void *region);
|
||||
reinterpret_cast<Fn_t>(Offsets::FUN_REGION_NOTIFY_LAYER_CHANGED)(region);
|
||||
}
|
||||
|
||||
} // namespace Frame::LayerChange
|
||||
@@ -0,0 +1,82 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// The layout sub-object every region and frame has (4.3.4's CLayoutFrame, at
|
||||
// OFF_REGION_ANCHOR, which is OFF_FRAME_LAYOUT_SUBOBJECT for a frame) and the
|
||||
// engine calls on it. Each call here takes the region or frame itself.
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Frame::Layout {
|
||||
|
||||
static_assert(Offsets::OFF_REGION_ANCHOR == Offsets::OFF_FRAME_LAYOUT_SUBOBJECT,
|
||||
"a frame's layout sub-object is where a region's is");
|
||||
|
||||
// The layout sub-object: `this` for SetPoint and the calls below.
|
||||
inline void *Of(const void *object) {
|
||||
return const_cast<uint8_t *>(static_cast<const uint8_t *>(object) + Offsets::OFF_REGION_ANCHOR);
|
||||
}
|
||||
|
||||
// Layout units (DDC, the space rects are stored in) to the device's 0..1
|
||||
// (NDC) and back: each axis scaled by the screen's (VAR_COORD_DDC_*_SCALE).
|
||||
// Either out pointer may be null, and may alias its input.
|
||||
inline void DdcToNdc(float *outX, float *outY, float x, float y) {
|
||||
using Fn = void(__fastcall *)(float *, float *, float, float);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_COORD_DDC_TO_NDC)(outX, outY, x, y);
|
||||
}
|
||||
inline void NdcToDdc(float *outX, float *outY, float x, float y) {
|
||||
using Fn = void(__fastcall *)(float *, float *, float, float);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_COORD_NDC_TO_DDC)(outX, outY, x, y);
|
||||
}
|
||||
|
||||
// The region or frame a layout sub-object belongs to, for the layout
|
||||
// vtable's overrides and hooks.
|
||||
inline void *OwnerOf(void *layout) {
|
||||
return static_cast<uint8_t *>(layout) - Offsets::OFF_REGION_ANCHOR;
|
||||
}
|
||||
|
||||
// CLayoutFrame::GetRect (FUN_REGION_GET_RECT): {bottom, left, top, right} in
|
||||
// layout units. False while the object has no valid rect.
|
||||
inline bool GetRect(const void *object, float (&rect)[4]) {
|
||||
using Fn = int(__thiscall *)(void *, float *);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_REGION_GET_RECT)(Of(object), rect) != 0;
|
||||
}
|
||||
|
||||
// IsResizePending (FUN_REGION_LAYOUT_DIRTY): a layout change waits for the
|
||||
// layout pass.
|
||||
inline bool IsResizePending(const void *object) {
|
||||
using Fn = int(__thiscall *)(void *);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_REGION_LAYOUT_DIRTY)(Of(object)) != 0;
|
||||
}
|
||||
|
||||
// CLayoutFrame::Resize (FUN_LAYOUT_RESIZE): `now` lays the object out at
|
||||
// once; otherwise it is marked to be laid out later, the path SetPoint takes.
|
||||
inline void Resize(void *object, bool now) {
|
||||
using Fn = void(__thiscall *)(void *, int);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_LAYOUT_RESIZE)(Of(object), now ? 1 : 0);
|
||||
}
|
||||
|
||||
// The edge getters' read (Script GetLeft 0x007A1A19): a pending resize run
|
||||
// now, then the rect. A hidden object, which the layout pass skips, gets a
|
||||
// rect this way.
|
||||
inline bool GetResolvedRect(void *object, float (&rect)[4]) {
|
||||
if (IsResizePending(object))
|
||||
Resize(object, true);
|
||||
return GetRect(object, rect);
|
||||
}
|
||||
|
||||
} // namespace Frame::Layout
|
||||
@@ -72,6 +72,7 @@
|
||||
#include "Game.h"
|
||||
#include "HookSecureFunc.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Layout.h"
|
||||
#include "frame/ScriptBindings.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -104,13 +105,10 @@ int __fastcall Script_SetSize(void *L) {
|
||||
}
|
||||
|
||||
using LayoutExtent_t = float(__thiscall *)(void *layout);
|
||||
using LayoutDirty_t = int(__fastcall *)(void *layout);
|
||||
using LayoutResize_t = void(__fastcall *)(void *layout, void *edx, int force);
|
||||
using LayoutGetRect_t = int(__fastcall *)(void *layout, void *edx, float *out);
|
||||
|
||||
// The explicit size of `self` in layout `slot`; 0 when none is set.
|
||||
float ExplicitExtent(void *self, int slot) {
|
||||
void *layout = static_cast<uint8_t *>(self) + Offsets::OFF_REGION_ANCHOR;
|
||||
void *layout = Frame::Layout::Of(self);
|
||||
auto *vtable = Game::Read<uintptr_t *>(layout, 0);
|
||||
return reinterpret_cast<LayoutExtent_t>(vtable[slot / sizeof(uintptr_t)])(layout);
|
||||
}
|
||||
@@ -145,13 +143,10 @@ void PushUnscaledExtent(void *L, bool ignoreRect, int slot, bool vertical) {
|
||||
void *self = Game::Lua::ResolveRegion(L);
|
||||
if (self == nullptr)
|
||||
return;
|
||||
void *layout = static_cast<uint8_t *>(self) + Offsets::OFF_REGION_ANCHOR;
|
||||
float extent = ExplicitExtent(self, slot);
|
||||
if (extent == 0.0f && !ignoreRect) {
|
||||
if (reinterpret_cast<LayoutDirty_t>(Offsets::FUN_REGION_LAYOUT_DIRTY)(layout) != 0)
|
||||
reinterpret_cast<LayoutResize_t>(Offsets::FUN_LAYOUT_RESIZE)(layout, nullptr, 1);
|
||||
float rect[4] = {0.0f, 0.0f, 0.0f, 0.0f}; // {bottom, left, top, right}
|
||||
if (reinterpret_cast<LayoutGetRect_t>(Offsets::FUN_REGION_GET_RECT)(layout, nullptr, rect) != 0)
|
||||
if (Frame::Layout::GetResolvedRect(self, rect))
|
||||
extent = (vertical ? rect[2] - rect[0] : rect[3] - rect[1]) /
|
||||
Game::Read<float>(self, Offsets::OFF_REGION_EFFECTIVE_SCALE);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Layout.h"
|
||||
#include "baselib/Ascii.h"
|
||||
#include "frame/Destruction.h"
|
||||
#include "frame/MotionFocus.h"
|
||||
@@ -63,7 +64,6 @@ using EnableEvent_t = void(__thiscall *)(void *frame, int kind, int priority);
|
||||
using DisableEvent_t = void(__thiscall *)(void *frame, int kind);
|
||||
using Button_t = void(__thiscall *)(void *frame, const void *event);
|
||||
using HitTest_t = int(__thiscall *)(void *frame, const float *point);
|
||||
using NDCToDDC_t = void(__fastcall *)(float *outX, float *outY, float x, float y);
|
||||
|
||||
// Frames with their mouse bit on and that half off.
|
||||
std::unordered_set<void *> g_clickOff;
|
||||
@@ -119,7 +119,7 @@ void ClickTargets(uint8_t *ctx, std::vector<void *> &out) {
|
||||
void *const primary = Game::Read<void *>(ctx, Offsets::OFF_UI_CONTEXT_MOUSE_FOCUS);
|
||||
const float *mouse = Game::Ptr<float>(ctx, Offsets::OFF_UI_CONTEXT_MOUSE_POSITION);
|
||||
float point[2] = {0.0f, 0.0f};
|
||||
reinterpret_cast<NDCToDDC_t>(Offsets::FUN_COORD_NDC_TO_DDC)(&point[0], &point[1], mouse[0], mouse[1]);
|
||||
Frame::Layout::NdcToDdc(&point[0], &point[1], mouse[0], mouse[1]);
|
||||
const auto hitTest = reinterpret_cast<HitTest_t>(Offsets::FUN_FRAME_HIT_TEST);
|
||||
|
||||
bool pastPrimary = primary == nullptr;
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/GetPointGuard.h"
|
||||
#include "frame/Layout.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -55,9 +56,6 @@ using StringToPoint_t = int(__fastcall *)(const char *name, int *point);
|
||||
using RemoveDependent_t = void(__fastcall *)(void *relative, void *edx, void *layout, int mask);
|
||||
using GetRelative_t = void *(__thiscall *)(void *node);
|
||||
using DeleteNode_t = void(__thiscall *)(void *node, int flags);
|
||||
using LayoutDirty_t = int(__fastcall *)(void *layout);
|
||||
using Resize_t = void(__fastcall *)(void *layout, void *edx, int force);
|
||||
using GetRect_t = int(__fastcall *)(void *layout, void *edx, float *out);
|
||||
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint,
|
||||
float x, float y, int flag);
|
||||
|
||||
@@ -154,12 +152,8 @@ int __fastcall Script_IsRectValid(void *L) {
|
||||
void *region = Game::Lua::ResolveRegion(L);
|
||||
if (region == nullptr)
|
||||
return 0;
|
||||
void *layout = static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR;
|
||||
if (reinterpret_cast<LayoutDirty_t>(Offsets::FUN_REGION_LAYOUT_DIRTY)(layout) != 0)
|
||||
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(layout, nullptr, 1);
|
||||
float rect[4];
|
||||
Game::Lua::PushBoolean(
|
||||
L, reinterpret_cast<GetRect_t>(Offsets::FUN_REGION_GET_RECT)(layout, nullptr, rect) != 0);
|
||||
Game::Lua::PushBoolean(L, Frame::Layout::GetResolvedRect(region, rect));
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "texture/Load.h"
|
||||
|
||||
namespace Frame::RenderBatch {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetRenderable_t = void *(__fastcall *)(void *texture, int load, void *callback);
|
||||
using ReserveQuads_t = void(__thiscall *)(void *array, uint32_t extra, int round);
|
||||
|
||||
struct QuadRecord {
|
||||
@@ -40,8 +40,7 @@ void QueueStrip(void *batch, void *texture, Blend blend, const C3Vector *positio
|
||||
const C2Vector *texCoords, const uint32_t *color) {
|
||||
// Loading on demand, as the texture emit does: a texture still streaming
|
||||
// is finished here, so the first rebuild already draws it.
|
||||
void *device = reinterpret_cast<GetRenderable_t>(Offsets::FUN_TEXTURE_GET_RENDERABLE)(
|
||||
texture, 1, nullptr);
|
||||
void *device = Texture::DeviceTexture(texture);
|
||||
if (device == nullptr)
|
||||
return;
|
||||
reinterpret_cast<ReserveQuads_t>(Offsets::FUN_RENDERBATCH_RESERVE_QUADS)(
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "frame/Subclass.h"
|
||||
|
||||
namespace Frame::Subclass {
|
||||
|
||||
namespace {
|
||||
|
||||
// Constant-initialized, so every MethodHash's constructor can link in during
|
||||
// static initialization whatever the order of the translation units.
|
||||
MethodHash *g_hashes = nullptr;
|
||||
|
||||
using RegisterFactory_t = int(__fastcall *)(const char *type, Factory_t factory, int unique);
|
||||
using PushMethod_t = int(__fastcall *)(void *L, const char *name, void *hash);
|
||||
using UnregisterMethods_t = void(__fastcall *)(void *hash);
|
||||
using UnregisterScriptFunctions_t = void(__cdecl *)();
|
||||
UnregisterScriptFunctions_t UnregisterScriptFunctions_o = nullptr;
|
||||
|
||||
// The hashes' functions are references into the game Lua state.
|
||||
void __cdecl UnregisterScriptFunctions_h() {
|
||||
UnregisterScriptFunctions_o();
|
||||
MethodHash::ReleaseAll();
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _unregisterHook{
|
||||
Offsets::FUN_GAMEUI_UNREGISTER_SCRIPT_FUNCTIONS,
|
||||
reinterpret_cast<void *>(&UnregisterScriptFunctions_h),
|
||||
reinterpret_cast<void **>(&UnregisterScriptFunctions_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
void RegisterFactory(const char *type, Factory_t factory) {
|
||||
reinterpret_cast<RegisterFactory_t>(Offsets::FUN_FRAMEXML_REGISTER_FACTORY)(type, factory, 0);
|
||||
}
|
||||
|
||||
MethodHash::MethodHash() : words_{}, next_(g_hashes) {
|
||||
words_[0] = Offsets::VTBL_METHOD_HASH_ALLOCATOR;
|
||||
words_[1] = 0xC; // node link offset
|
||||
words_[2] = reinterpret_cast<uintptr_t>(&words_[2]);
|
||||
words_[3] = reinterpret_cast<uintptr_t>(&words_[2]) | 1;
|
||||
words_[9] = 0xFFFFFFFF; // no buckets yet
|
||||
g_hashes = this;
|
||||
}
|
||||
|
||||
bool MethodHash::PushMethod(void *L, const char *name) {
|
||||
return reinterpret_cast<PushMethod_t>(Offsets::FUN_FRAMESCRIPT_PUSH_METHOD)(L, name, words_) !=
|
||||
0;
|
||||
}
|
||||
|
||||
void MethodHash::ReleaseAll() {
|
||||
for (MethodHash *h = g_hashes; h != nullptr; h = h->next_)
|
||||
reinterpret_cast<UnregisterMethods_t>(Offsets::FUN_FRAMESCRIPT_UNREGISTER_METHODS)(h->words_);
|
||||
}
|
||||
|
||||
} // namespace Frame::Subclass
|
||||
@@ -0,0 +1,214 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// The parts every frame class this DLL adds shares: copied engine vtables with
|
||||
// overrides in place, a method hash of the class's own, its type id and
|
||||
// type-name overrides, and the factory (Offsets.h "Subclassing CSimpleFrame",
|
||||
// "Per-class method hashes"). Frame::Cooldown and Model::Scene are built on it.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Layout.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Frame::Subclass {
|
||||
|
||||
// An MSVC vtable with its RTTI locator in front, as the engine's are laid out.
|
||||
template <size_t N>
|
||||
struct VTable {
|
||||
const void *locator;
|
||||
const void *slots[N];
|
||||
|
||||
bool Built() const { return slots[0] != nullptr; }
|
||||
// A copy of the engine vtable at `address` (its first slot).
|
||||
void CopyFrom(uintptr_t address) {
|
||||
std::memcpy(this, reinterpret_cast<const void *>(address - 4), sizeof(*this));
|
||||
}
|
||||
// `slot` is a byte offset (Offsets::VTBL_*).
|
||||
void Set(uintptr_t slot, const void *fn) { slots[slot / 4] = fn; }
|
||||
};
|
||||
|
||||
// The function in byte offset `slot` of the engine vtable at `address`, for
|
||||
// an override's base call.
|
||||
template <typename Fn>
|
||||
Fn Slot(uintptr_t address, uintptr_t slot) {
|
||||
return Game::Read<Fn>(reinterpret_cast<const void *>(address), slot);
|
||||
}
|
||||
|
||||
// A frame class's method hash, built the way the engine's static initializer
|
||||
// builds one (Model's 0x0076F5F0). Every hash is released where the engine
|
||||
// releases its own class hashes, at the end of CGGameUI::UnregisterScriptFunctions,
|
||||
// while the game Lua state is still open. Declare one at file scope.
|
||||
class MethodHash {
|
||||
public:
|
||||
MethodHash();
|
||||
MethodHash(const MethodHash &) = delete;
|
||||
MethodHash &operator=(const MethodHash &) = delete;
|
||||
|
||||
// What Game::Lua::RegisterFrameMethods and the documentation take.
|
||||
void *Registry() { return words_; }
|
||||
// FrameScriptPushMethod: pushes the method and answers true when the
|
||||
// hash has it.
|
||||
bool PushMethod(void *L, const char *name);
|
||||
|
||||
static void ReleaseAll();
|
||||
|
||||
private:
|
||||
uint32_t words_[Offsets::METHOD_HASH_SIZE / 4];
|
||||
MethodHash *next_;
|
||||
};
|
||||
|
||||
// CSimpleFrame's own slot, for the base call of an override that answers as
|
||||
// a Frame whatever engine class the frame extends.
|
||||
template <typename Fn>
|
||||
Fn FrameBase(uintptr_t slot) {
|
||||
return Slot<Fn>(Offsets::VTBL_SIMPLEFRAME, slot);
|
||||
}
|
||||
|
||||
// A frame class this DLL adds: what it is and what it extends, then the
|
||||
// state the engine and the shared overrides below keep for it. Declare one
|
||||
// at file scope; the overrides take it as their template argument.
|
||||
template <size_t Slots, size_t LayoutSlots>
|
||||
struct Class {
|
||||
const char *name; // what GetObjectType gives and IsObjectType takes
|
||||
uintptr_t engineVtable; // the engine class it extends: its vtable,
|
||||
uintptr_t engineLayoutVtable; // its layout vtable,
|
||||
uintptr_t constructor; // and its constructor, __thiscall (frame, parent)
|
||||
size_t size; // the whole object, the engine class's part and ours
|
||||
void (*destroy)(void *frame); // releases our part, before the engine's destructor
|
||||
|
||||
int typeId = 0; // the engine's type id for the class, once it names one
|
||||
MethodHash hash;
|
||||
VTable<Slots> vtable;
|
||||
VTable<LayoutSlots> layoutVtable;
|
||||
|
||||
// What Game::Lua::ResolveTypedObject and Frame::ExtraScripts take.
|
||||
uintptr_t TypeIdVar() { return reinterpret_cast<uintptr_t>(&typeId); }
|
||||
// The engine class's own slot, for the base call of an override.
|
||||
template <typename Fn>
|
||||
Fn Base(uintptr_t slot) const {
|
||||
return Slot<Fn>(engineVtable, slot);
|
||||
}
|
||||
template <typename Fn>
|
||||
Fn LayoutBase(uintptr_t slot) const {
|
||||
return Slot<Fn>(engineLayoutVtable, slot);
|
||||
}
|
||||
};
|
||||
|
||||
// ---- the overrides every class has --------------------------------------------
|
||||
|
||||
using DeletingDestructor_t = void *(__fastcall *)(void *self, void *edx, uint32_t flags);
|
||||
using GetScriptMethod_t = int(__fastcall *)(void *self, void *edx, void *L, const char *name);
|
||||
using IsA_t = bool(__fastcall *)(void *self, void *edx, int typeId);
|
||||
using IsObjectType_t = bool(__fastcall *)(void *self, void *edx, const char *name);
|
||||
|
||||
// The slots a class overrides for itself, for their base calls.
|
||||
using SetFrameAlpha_t = void(__fastcall *)(void *self, void *edx, uint8_t alpha);
|
||||
using OnFrameUpdate_t = void(__fastcall *)(void *self, void *edx, float elapsedSec);
|
||||
using EmitLayer_t = void(__fastcall *)(void *self, void *edx, void *batch, int layer);
|
||||
// The layout vtable's node loader (Offsets::VTBL_XML_NODE_LOADER).
|
||||
using LoadXML_t = uintptr_t(__fastcall *)(void *layout, void *edx, const void *node, void *status);
|
||||
|
||||
// Our part released, then the engine class's deleting destructor.
|
||||
template <auto &C>
|
||||
void *__fastcall DeletingDestructor(void *self, void *, uint32_t flags) {
|
||||
C.destroy(self);
|
||||
return C.template Base<DeletingDestructor_t>(Offsets::VTBL_FRAME_DELETING_DESTRUCTOR)(
|
||||
self, nullptr, flags);
|
||||
}
|
||||
|
||||
template <auto &C>
|
||||
void *__fastcall LayoutDeletingDestructor(void *layout, void *, uint32_t flags) {
|
||||
return DeletingDestructor<C>(Layout::OwnerOf(layout), nullptr, flags);
|
||||
}
|
||||
|
||||
// The class's methods, then a Frame's: none of the engine class's own
|
||||
// (ModelScene has no Model methods).
|
||||
template <auto &C>
|
||||
int __fastcall GetScriptMethod(void *self, void *, void *L, const char *name) {
|
||||
if (C.hash.PushMethod(L, name))
|
||||
return 1;
|
||||
return FrameBase<GetScriptMethod_t>(Offsets::VTBL_FRAME_GET_SCRIPT_METHOD)(self, nullptr, L,
|
||||
name);
|
||||
}
|
||||
|
||||
// The class, then what a Frame is (retail's ModelScene is not a Model).
|
||||
template <auto &C>
|
||||
bool __fastcall IsA(void *self, void *, int typeId) {
|
||||
return (C.typeId != 0 && typeId == C.typeId) ||
|
||||
FrameBase<IsA_t>(Offsets::VTBL_FRAME_IS_A)(self, nullptr, typeId);
|
||||
}
|
||||
|
||||
template <auto &C>
|
||||
bool __fastcall IsObjectType(void *self, void *, const char *name) {
|
||||
return (name != nullptr && Game::StrCmpI(name, C.name) == 0) ||
|
||||
FrameBase<IsObjectType_t>(Offsets::VTBL_FRAME_IS_OBJECT_TYPE)(self, nullptr, name);
|
||||
}
|
||||
|
||||
template <auto &C>
|
||||
const char *__fastcall GetObjectType(void *, void *) {
|
||||
return C.name;
|
||||
}
|
||||
|
||||
// Copies the engine class's two vtables and puts in the overrides above, with
|
||||
// CSimpleFrame's script names (+0x0C) to match its methods. False when they
|
||||
// are built already; on true the caller puts in its own overrides.
|
||||
template <auto &C>
|
||||
bool BuildVTables() {
|
||||
if (C.vtable.Built())
|
||||
return false;
|
||||
C.vtable.CopyFrom(C.engineVtable);
|
||||
C.layoutVtable.CopyFrom(C.engineLayoutVtable);
|
||||
auto fn = [](auto f) { return reinterpret_cast<const void *>(f); };
|
||||
C.vtable.Set(Offsets::VTBL_FRAME_DELETING_DESTRUCTOR, fn(&DeletingDestructor<C>));
|
||||
C.vtable.Set(Offsets::VTBL_FRAME_GET_SCRIPT_METHOD, fn(&GetScriptMethod<C>));
|
||||
C.vtable.Set(Offsets::VTBL_FRAME_SCRIPT_RESOLVER,
|
||||
FrameBase<const void *>(Offsets::VTBL_FRAME_SCRIPT_RESOLVER));
|
||||
C.vtable.Set(Offsets::VTBL_FRAME_IS_A, fn(&IsA<C>));
|
||||
C.vtable.Set(Offsets::VTBL_FRAME_IS_OBJECT_TYPE, fn(&IsObjectType<C>));
|
||||
C.vtable.Set(Offsets::VTBL_FRAME_GET_OBJECT_TYPE, fn(&GetObjectType<C>));
|
||||
C.layoutVtable.Set(Offsets::VTBL_LAYOUT_DELETING_DESTRUCTOR,
|
||||
fn(&LayoutDeletingDestructor<C>));
|
||||
return true;
|
||||
}
|
||||
|
||||
// A new frame of the class, shaped like the engine's factories (the "Frame"
|
||||
// one, 0x006EEC10): a Storm block for the whole object, zero-filled, the
|
||||
// engine class's constructor, then our vtables, which BuildVTables must have
|
||||
// built. Null when the allocation fails.
|
||||
template <auto &C>
|
||||
void *New(void *parent) {
|
||||
using Constructor_t = void(__fastcall *)(void *self, void *edx, void *parent);
|
||||
void *frame = Storm::Alloc(C.size, __FILE__, __LINE__, true);
|
||||
if (frame == nullptr)
|
||||
return nullptr;
|
||||
reinterpret_cast<Constructor_t>(C.constructor)(frame, nullptr, parent);
|
||||
Game::Ref<const void *>(frame, 0) = C.vtable.slots;
|
||||
Game::Ref<const void *>(frame, Offsets::OFF_FRAME_LAYOUT_SUBOBJECT) = C.layoutVtable.slots;
|
||||
return frame;
|
||||
}
|
||||
|
||||
using Factory_t = void *(__fastcall *)(void *parent);
|
||||
|
||||
// Adds the frame type to CreateFrame and XML (FUN_FRAMEXML_REGISTER_FACTORY).
|
||||
// The engine's type table outlives the Lua state, so on a /reload the name
|
||||
// is there already and the engine keeps the first factory.
|
||||
void RegisterFactory(const char *type, Factory_t factory);
|
||||
|
||||
} // namespace Frame::Subclass
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// The graphics device's transforms: its projection and view matrices and its
|
||||
// viewport (Offsets.h, at FUN_GX_GET_PROJECTION). The engine's own draws that
|
||||
// leave the UI pass's transforms (SimpleModelDraw 0x0076D240, the ScrollFrame
|
||||
// draw 0x00787220, the portrait bake) save all three first and restore them
|
||||
// after; SavedTransforms does that for a scope.
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "gx/Matrix.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Gx {
|
||||
|
||||
inline void GetProjection(Matrix &m) {
|
||||
reinterpret_cast<void(__fastcall *)(float *)>(Offsets::FUN_GX_GET_PROJECTION)(m);
|
||||
}
|
||||
inline void SetProjection(const Matrix &m) {
|
||||
reinterpret_cast<void(__fastcall *)(const float *)>(Offsets::FUN_GX_SET_PROJECTION)(m);
|
||||
}
|
||||
inline void GetView(Matrix &m) {
|
||||
reinterpret_cast<void(__fastcall *)(float *)>(Offsets::FUN_GX_GET_VIEW)(m);
|
||||
}
|
||||
inline void SetView(const Matrix &m) {
|
||||
reinterpret_cast<void(__fastcall *)(const float *)>(Offsets::FUN_GX_SET_VIEW)(m);
|
||||
}
|
||||
|
||||
// In NDC, 0 to 1.
|
||||
struct Viewport {
|
||||
float minX, maxX, minY, maxY, minZ, maxZ;
|
||||
};
|
||||
|
||||
inline Viewport GetViewport() {
|
||||
using Fn = void(__fastcall *)(float *, float *, float *, float *, float *, float *);
|
||||
Viewport v{};
|
||||
reinterpret_cast<Fn>(Offsets::FUN_GX_GET_VIEWPORT)(&v.minX, &v.maxX, &v.minY, &v.maxY, &v.minZ,
|
||||
&v.maxZ);
|
||||
return v;
|
||||
}
|
||||
inline void SetViewport(const Viewport &v) {
|
||||
using Fn = void(__stdcall *)(float, float, float, float, float, float);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_GX_SET_VIEWPORT)(v.minX, v.maxX, v.minY, v.maxY, v.minZ,
|
||||
v.maxZ);
|
||||
}
|
||||
|
||||
// ---- render states (GXRS_*) -----------------------------------------------------
|
||||
// The device journals each state written after a push; the pop puts every one
|
||||
// back. The engine brackets each region-layer draw in a pair.
|
||||
|
||||
inline void PushStates() { reinterpret_cast<void(__fastcall *)()>(Offsets::FUN_GX_STATE_PUSH)(); }
|
||||
inline void PopStates() { reinterpret_cast<void(__fastcall *)()>(Offsets::FUN_GX_STATE_POP)(); }
|
||||
|
||||
// The render states written in a scope, put back when it ends.
|
||||
class SavedStates {
|
||||
public:
|
||||
SavedStates() { PushStates(); }
|
||||
~SavedStates() { PopStates(); }
|
||||
SavedStates(const SavedStates &) = delete;
|
||||
SavedStates &operator=(const SavedStates &) = delete;
|
||||
};
|
||||
|
||||
inline void SetState(int state, int value) {
|
||||
reinterpret_cast<void(__fastcall *)(int, int)>(Offsets::FUN_GX_RS_SET)(state, value);
|
||||
}
|
||||
// A pointer state: a texture bind.
|
||||
inline void SetState(int state, const void *value) {
|
||||
reinterpret_cast<void(__fastcall *)(int, const void *)>(Offsets::FUN_GX_RS_SET_PTR)(state, value);
|
||||
}
|
||||
// A colour state (fog colour, material).
|
||||
inline void SetStateWord(int state, uint32_t value) {
|
||||
reinterpret_cast<void(__fastcall *)(int, int, uint32_t)>(Offsets::FUN_GX_RS_SET_WORD)(state, 0, value);
|
||||
}
|
||||
inline void SetStateFloat(int state, float value) {
|
||||
reinterpret_cast<void(__fastcall *)(int, int, float)>(Offsets::FUN_GX_RS_SET_FLOAT)(state, 0, value);
|
||||
}
|
||||
inline int GetState(int state) {
|
||||
int value = 0;
|
||||
reinterpret_cast<void(__fastcall *)(int, int *)>(Offsets::FUN_GX_RS_GET_INT)(state, &value);
|
||||
return value;
|
||||
}
|
||||
|
||||
// ---- transform stacks -----------------------------------------------------------
|
||||
// Each texture unit has a matrix stack, and stack 8 is the world transform.
|
||||
|
||||
inline void PushTransform(int unit) {
|
||||
reinterpret_cast<void(__fastcall *)(int)>(Offsets::FUN_GX_TEXMTX_PUSH)(unit);
|
||||
}
|
||||
inline void PushTransform(int unit, const float *m) {
|
||||
reinterpret_cast<void(__fastcall *)(int, const float *)>(Offsets::FUN_GX_TEXMTX_PUSH_LOAD)(unit, m);
|
||||
}
|
||||
inline void PopTransform(int unit) {
|
||||
reinterpret_cast<void(__fastcall *)(int)>(Offsets::FUN_GX_TEXMTX_POP)(unit);
|
||||
}
|
||||
// Over the top of the unit's stack.
|
||||
inline void SetTransform(int unit, const float *m) {
|
||||
reinterpret_cast<void(__fastcall *)(int, const float *)>(Offsets::FUN_GX_TEXMTX_LOAD)(unit, m);
|
||||
}
|
||||
|
||||
// ---- clear ----------------------------------------------------------------------
|
||||
|
||||
inline uint32_t GetClearColor() {
|
||||
uint32_t argb = 0;
|
||||
reinterpret_cast<uint32_t *(__fastcall *)(uint32_t *)>(Offsets::FUN_GX_GET_CLEAR_COLOR)(&argb);
|
||||
return argb;
|
||||
}
|
||||
inline void SetClearColor(uint32_t argb) {
|
||||
reinterpret_cast<void(__stdcall *)(uint32_t)>(Offsets::FUN_GX_SET_CLEAR_COLOR)(argb);
|
||||
}
|
||||
// `mask` 1 the colour, 2 the depth.
|
||||
inline void Clear(uint32_t mask) { reinterpret_cast<void(__fastcall *)(uint32_t)>(Offsets::FUN_GX_CLEAR)(mask); }
|
||||
|
||||
// The near and far plane points at the viewport fractions `a` (across) and
|
||||
// `b` (up), through the device view and projection: relative to the eye when
|
||||
// the view leaves it out, as a camera's does.
|
||||
inline void UnprojectScreenPoint(float (&nearPoint)[3], float (&farPoint)[3], float a, float b) {
|
||||
using Fn = void(__fastcall *)(float *, float *, float, float);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_UNPROJECT_SCREEN_POINT)(nearPoint, farPoint, a, b);
|
||||
}
|
||||
|
||||
// The projection, view and viewport, put back when the scope ends.
|
||||
class SavedTransforms {
|
||||
public:
|
||||
SavedTransforms() : viewport_(GetViewport()) {
|
||||
GetProjection(projection_);
|
||||
GetView(view_);
|
||||
}
|
||||
~SavedTransforms() {
|
||||
SetProjection(projection_);
|
||||
SetView(view_);
|
||||
SetViewport(viewport_);
|
||||
}
|
||||
SavedTransforms(const SavedTransforms &) = delete;
|
||||
SavedTransforms &operator=(const SavedTransforms &) = delete;
|
||||
|
||||
const Viewport &SavedViewport() const { return viewport_; }
|
||||
|
||||
private:
|
||||
Matrix projection_;
|
||||
Matrix view_;
|
||||
Viewport viewport_;
|
||||
};
|
||||
|
||||
} // namespace Gx
|
||||
@@ -0,0 +1,86 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// The engine's 4x4 matrix helpers (Offsets.h FUN_MATRIX_*, FUN_TRANSFORM_*).
|
||||
// Matrices are row-vector: a point is a row times the matrix, rows 0..2 are
|
||||
// the scaled axes and row 3 the translation. Each transform below
|
||||
// pre-multiplies, so the one called last applies to a point first.
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace Gx {
|
||||
|
||||
using Matrix = float[16];
|
||||
|
||||
inline void Identity(float *m) {
|
||||
std::memset(m, 0, sizeof(Matrix));
|
||||
m[0] = m[5] = m[10] = m[15] = 1.0f;
|
||||
}
|
||||
|
||||
// TranslateMatrix: row 3 += t · rows 0..2.
|
||||
inline void Translate(float *m, const float (&t)[3]) {
|
||||
reinterpret_cast<void(__thiscall *)(float *, const float *)>(Offsets::FUN_MATRIX_TRANSLATE)(m, t);
|
||||
}
|
||||
|
||||
// RotateMatrix: by `angle` radians about the unit `axis`.
|
||||
inline void Rotate(float *m, float angle, const float (&axis)[3]) {
|
||||
reinterpret_cast<void(__thiscall *)(float *, float, const float *, int)>(Offsets::FUN_MATRIX_ROTATE)(
|
||||
m, angle, axis, 1);
|
||||
}
|
||||
|
||||
// By the quaternion {x, y, z, w}.
|
||||
inline void RotateQuaternion(float *m, const float *q) {
|
||||
reinterpret_cast<void(__thiscall *)(float *, const float *)>(Offsets::FUN_MATRIX_ROTATE_QUATERNION)(m, q);
|
||||
}
|
||||
|
||||
inline void Scale(float *m, float s) {
|
||||
reinterpret_cast<void(__thiscall *)(float *, float)>(Offsets::FUN_MATRIX_SCALE)(m, s);
|
||||
}
|
||||
|
||||
// MultiplyMatrix4: out = a × b.
|
||||
inline void Multiply(float *out, const float *a, const float *b) {
|
||||
reinterpret_cast<float *(__fastcall *)(float *, const float *, const float *)>(Offsets::FUN_MATRIX_MULTIPLY)(
|
||||
out, a, b);
|
||||
}
|
||||
|
||||
// m = m × b.
|
||||
inline void MultiplyInPlace(float *m, const float *b) {
|
||||
reinterpret_cast<float *(__thiscall *)(float *, const float *)>(Offsets::FUN_MATRIX_MULTIPLY_IN_PLACE)(m, b);
|
||||
}
|
||||
|
||||
// InvertAffineMatrix: the inverse of a rotation, uniform `scale` and
|
||||
// translation.
|
||||
inline void InvertAffine(const float *m, float *out, float scale) {
|
||||
reinterpret_cast<void(__thiscall *)(const float *, float *, float)>(Offsets::FUN_MATRIX_INVERT_AFFINE)(
|
||||
m, out, scale);
|
||||
}
|
||||
|
||||
// The point through the matrix (`in` and `out` may be the same).
|
||||
inline void TransformPoint(const float (&in)[3], const float *m, float (&out)[3]) {
|
||||
float v[3] = {in[0], in[1], in[2]};
|
||||
reinterpret_cast<float *(__fastcall *)(float *, float *, const float *)>(Offsets::FUN_TRANSFORM_POINT_IN_PLACE)(
|
||||
out, v, m);
|
||||
}
|
||||
|
||||
// The box {min xyz, max xyz} through the matrix, as the box around its
|
||||
// transformed corners.
|
||||
inline void TransformBox(const float *m, const float (&box)[6], float (&out)[6]) {
|
||||
reinterpret_cast<void(__fastcall *)(const float *, const float *, float *)>(Offsets::FUN_TRANSFORM_BOX)(m, box,
|
||||
out);
|
||||
}
|
||||
|
||||
} // namespace Gx
|
||||
@@ -0,0 +1,97 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetCursorDelta()` -> deltaX, deltaY
|
||||
//
|
||||
// Retail's: how far the cursor moved since the last frame, zero when it did
|
||||
// not, in GetCursorPosition's units (independent of the UI scale; InputUtil.
|
||||
// GetCursorDelta divides by a frame's effective scale). The orbit camera of
|
||||
// a ModelScene drags with it.
|
||||
//
|
||||
// The cursor is sampled once a frame, at the head of the UI render callback,
|
||||
// before any OnUpdate runs, the way the in-game GetCursorPosition (0x0048B820)
|
||||
// reads it: the top's NDC position (OFF_UI_CONTEXT_MOUSE_POSITION) through
|
||||
// NDCToDDC, then the engine's script push (Game::InternalToPixel). The delta is
|
||||
// this frame's sample less the last frame's, so every OnUpdate of a frame sees
|
||||
// the same value.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Layout.h"
|
||||
#include "tick/FrameTick.h"
|
||||
|
||||
namespace Input::CursorDelta {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Position {
|
||||
double x = 0.0;
|
||||
double y = 0.0;
|
||||
};
|
||||
|
||||
Position g_previous;
|
||||
Position g_current;
|
||||
bool g_sampled = false;
|
||||
|
||||
// What GetCursorPosition pushes now, or false before the UI top exists.
|
||||
bool ReadCursor(Position &out) {
|
||||
const void *top = Game::Read<const void *>(Offsets::VAR_UI_CONTEXT_PTR);
|
||||
if (top == nullptr)
|
||||
return false;
|
||||
const float ndcX = Game::Read<float>(top, Offsets::OFF_UI_CONTEXT_MOUSE_POSITION);
|
||||
const float ndcY = Game::Read<float>(top, Offsets::OFF_UI_CONTEXT_MOUSE_POSITION + 4);
|
||||
float ddcX = 0.0f;
|
||||
float ddcY = 0.0f;
|
||||
Frame::Layout::NdcToDdc(&ddcX, &ddcY, ndcX, ndcY);
|
||||
out = {Game::InternalToPixel(ddcX), Game::InternalToPixel(ddcY)};
|
||||
return true;
|
||||
}
|
||||
|
||||
void Sample() {
|
||||
Position now;
|
||||
if (!ReadCursor(now))
|
||||
return;
|
||||
g_previous = g_sampled ? g_current : now;
|
||||
g_current = now;
|
||||
g_sampled = true;
|
||||
}
|
||||
|
||||
const Tick::FrameTick::AutoSubscribeBefore _tick{&Sample};
|
||||
|
||||
int __fastcall Script_GetCursorDelta(void *L) {
|
||||
Game::Lua::PushNumber(L, g_current.x - g_previous.x);
|
||||
Game::Lua::PushNumber(L, g_current.y - g_previous.y);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("deltaX", "number"),
|
||||
Game::Doc::Req("deltaY", "number"),
|
||||
};
|
||||
const Game::Doc::Function kDoc{"Returns how far the cursor moved since the last frame.", {},
|
||||
kRets, "Input"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("GetCursorDelta", &Script_GetCursorDelta, &kDoc);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("GetCursorDelta", &Script_GetCursorDelta, &kDoc);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Input::CursorDelta
|
||||
@@ -93,6 +93,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "debug/Log.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
@@ -114,7 +115,6 @@ namespace {
|
||||
using FileRead_t = int(__stdcall *)(int unused, const char *path, void **outBuf,
|
||||
unsigned int *outSize, unsigned int extraBytes,
|
||||
int flag1, int flag2);
|
||||
using SMemFree_t = void(__stdcall *)(void *ptr, const char *file, int line, int flags);
|
||||
|
||||
// MPQ enumerator (see Offsets::FUN_MPQ_ENUM_FILES). The callback
|
||||
// receives each listfile entry's full path in ecx; returning 0 STOPS
|
||||
@@ -301,7 +301,6 @@ void ScanPathGetters(Collector &c) {
|
||||
int WriteFiles(const std::vector<std::string> &files, size_t prefixLen,
|
||||
const char *dstRoot, int *skipped) {
|
||||
auto FileRead = reinterpret_cast<FileRead_t>(Offsets::FUN_FILE_READ);
|
||||
auto SMemFree = reinterpret_cast<SMemFree_t>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
|
||||
int written = 0;
|
||||
int missed = 0;
|
||||
@@ -327,7 +326,7 @@ int WriteFiles(const std::vector<std::string> &files, size_t prefixLen,
|
||||
static_cast<unsigned long>(GetLastError()), dst.c_str());
|
||||
}
|
||||
|
||||
SMemFree(buf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(buf, __FILE__, __LINE__);
|
||||
}
|
||||
if (skipped != nullptr)
|
||||
*skipped = missed;
|
||||
|
||||
+61
-4
@@ -34,6 +34,15 @@
|
||||
// state machine — we no longer need `WaitingOpen` / `WaitingClose` /
|
||||
// per-tick `VAR_LOOT_GUID` reads, because the controller hook *is* the
|
||||
// state transition.
|
||||
//
|
||||
// Loot mode remembers corpses whose window opened EMPTY but which stay
|
||||
// LOOTABLE: the server keeps the flag while any loot is left (another
|
||||
// member's quest item, condition-gated loot) yet never sends this player a
|
||||
// slot it can't take, so an empty window is the server's own "nothing here
|
||||
// for you". Later walks skip such a corpse. A corpse whose window held
|
||||
// anything (bags full, a roll or master-loot assignment in progress) is not
|
||||
// remembered and is retried, because those refusals clear on their own.
|
||||
// See `s_emptyCorpses` for when an entry is forgotten.
|
||||
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
@@ -45,6 +54,7 @@
|
||||
#include "../time/Clock.h"
|
||||
#include "Request.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -68,6 +78,8 @@ constexpr uint32_t kStepTimeoutMs = 3000;
|
||||
// fire unmodified.
|
||||
constexpr int EVENT_LOOT_OPENED = 0x10B;
|
||||
constexpr int EVENT_LOOT_CLOSED = 0x10D;
|
||||
// Observed, never swallowed (see `s_emptyCorpses`).
|
||||
constexpr int EVENT_QUEST_LOG_UPDATE = 0x134;
|
||||
|
||||
enum class State {
|
||||
Idle,
|
||||
@@ -100,6 +112,21 @@ struct ScanCtx {
|
||||
|
||||
ScanCtx s_scan;
|
||||
|
||||
// Loot-mode corpses whose window opened empty while the unit stayed
|
||||
// LOOTABLE (see the file comment). Outlives a walk. An entry is forgotten:
|
||||
// - on the first world tick where its unit is gone (despawned, out of
|
||||
// view) or no longer LOOTABLE (alive again after a respawn
|
||||
// with the same GUID, or emptied by someone else), so a corpse back in
|
||||
// view or a fresh corpse is opened again;
|
||||
// - all at once on QUEST_LOG_UPDATE: quest items are per-player loot, so
|
||||
// a quest change can turn an empty window into one with an item in it.
|
||||
std::vector<uint64_t> s_emptyCorpses;
|
||||
|
||||
bool IsKnownEmpty(uint64_t guid) {
|
||||
return std::find(s_emptyCorpses.begin(), s_emptyCorpses.end(), guid) !=
|
||||
s_emptyCorpses.end();
|
||||
}
|
||||
|
||||
// === Typedefs for engine helpers ===
|
||||
|
||||
using ClntObjMgrEnumVisibleObjectsCallback_t = int(__fastcall *)(void *ctx,
|
||||
@@ -130,6 +157,19 @@ using LootController_t = void(__fastcall *)(int lootStructPtr,
|
||||
int coin,
|
||||
int unk);
|
||||
|
||||
// Run every world tick, so no gone-and-back or respawn-and-die-again
|
||||
// can happen between two checks.
|
||||
void PruneEmptyCorpses() {
|
||||
s_emptyCorpses.erase(
|
||||
std::remove_if(s_emptyCorpses.begin(), s_emptyCorpses.end(),
|
||||
[](uint64_t guid) {
|
||||
void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT,
|
||||
guid, nullptr, 0);
|
||||
return unit == nullptr || !Request::IsLootable(unit);
|
||||
}),
|
||||
s_emptyCorpses.end());
|
||||
}
|
||||
|
||||
// === Scrape ===
|
||||
|
||||
// Reads the post-population `VAR_LOOT_SLOTS` table into a LootEntry.
|
||||
@@ -325,6 +365,10 @@ void __fastcall LootController_h(int lootStructPtr, int coin, int unk) {
|
||||
LootEntry entry;
|
||||
entry.guid = s_scan.currentGuid;
|
||||
ScrapeCurrentLoot(&entry);
|
||||
// Each corpse is queued once per walk and known-empty ones are not
|
||||
// queued in loot mode, so this never adds a duplicate.
|
||||
if (s_scan.lootMode && entry.coin == 0 && entry.items.empty())
|
||||
s_emptyCorpses.push_back(entry.guid);
|
||||
s_scan.results.push_back(std::move(entry));
|
||||
|
||||
// Loot mode: actually take everything (coin + all slots) before the
|
||||
@@ -355,9 +399,19 @@ const Game::HookAutoRegister _hookController{
|
||||
|
||||
const Event::SignalHook::AutoSubscribe _lootIntercept{&SuppressLootEvents};
|
||||
|
||||
// === Tick (refusals and timeouts) ===
|
||||
// Observes only: the event still fires.
|
||||
bool ForgetEmptyCorpsesOnQuestChange(int eventID) {
|
||||
if (eventID == EVENT_QUEST_LOG_UPDATE)
|
||||
s_emptyCorpses.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
const Event::SignalHook::AutoSubscribe _questIntercept{&ForgetEmptyCorpsesOnQuestChange};
|
||||
|
||||
// === Tick (memory upkeep, refusals and timeouts) ===
|
||||
|
||||
void OnTick() {
|
||||
PruneEmptyCorpses();
|
||||
if (s_scan.state == State::Idle)
|
||||
return;
|
||||
const bool timedOut = Time::Clock::Elapsed(s_scan.stepStartMs) >= kStepTimeoutMs;
|
||||
@@ -396,6 +450,7 @@ const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
|
||||
struct EnumCtx {
|
||||
void *player;
|
||||
std::vector<uint64_t> *out;
|
||||
bool skipKnownEmpty;
|
||||
};
|
||||
|
||||
int __fastcall EnumCallback(EnumCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
@@ -404,17 +459,19 @@ int __fastcall EnumCallback(EnumCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
return 1;
|
||||
if (!Request::IsLootable(obj))
|
||||
return 1;
|
||||
if (ctx->skipKnownEmpty && IsKnownEmpty(guid))
|
||||
return 1;
|
||||
if (!Request::InReach(ctx->player, obj))
|
||||
return 1;
|
||||
ctx->out->push_back(guid);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void BuildQueue(void *player, size_t maxCount) {
|
||||
void BuildQueue(void *player, size_t maxCount, bool skipKnownEmpty) {
|
||||
s_scan.queue.clear();
|
||||
auto Enum = reinterpret_cast<ClntObjMgrEnumVisibleObjects_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS);
|
||||
EnumCtx ctx{player, &s_scan.queue};
|
||||
EnumCtx ctx{player, &s_scan.queue, skipKnownEmpty};
|
||||
Enum(reinterpret_cast<ClntObjMgrEnumVisibleObjectsCallback_t>(&EnumCallback),
|
||||
&ctx);
|
||||
// Cap to `maxCount` corpses (0 = unlimited). Order is engine
|
||||
@@ -440,7 +497,7 @@ bool BeginWalk(bool lootMode, size_t maxCount) {
|
||||
s_scan.results.clear();
|
||||
s_scan.player = player;
|
||||
s_scan.lootMode = lootMode;
|
||||
BuildQueue(player, maxCount);
|
||||
BuildQueue(player, maxCount, /*skipKnownEmpty=*/lootMode);
|
||||
|
||||
if (s_scan.queue.empty()) {
|
||||
Complete(); // nothing nearby — fire LOOT_SCAN_COMPLETED immediately
|
||||
|
||||
@@ -84,8 +84,6 @@ Vec3 GetVector(void *camera, uint32_t id) {
|
||||
return v;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void SetVector(void *camera, uint32_t id, const Vec3 &v) {
|
||||
reinterpret_cast<SetVector_t>(Offsets::FUN_CAMERA_SET_VECTOR)(camera, id, &v.x, 0);
|
||||
}
|
||||
@@ -98,6 +96,13 @@ void SetScalar(void *camera, uint32_t id, float value) {
|
||||
reinterpret_cast<SetScalar_t>(Offsets::FUN_CAMERA_SET_SCALAR)(camera, id, value);
|
||||
}
|
||||
|
||||
void SetDeviceTransforms(void *camera, const float (&rect)[4]) {
|
||||
using Fn = void(__fastcall *)(void *camera, const float *rect, uint32_t unused);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_CAMERA_SET_DEVICE_TRANSFORMS)(camera, rect, 0);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// ConfigureCamera's own evaluation pass: facing 0, rebuild the root
|
||||
// transform, animate the scene, restore the facing.
|
||||
void AnimateUnrotated(void *pane, void *model) {
|
||||
|
||||
+10
-1
@@ -21,7 +21,16 @@ struct Vec3 {
|
||||
// The M2 model's camera with id `id` (0 = portrait), or null when it has none.
|
||||
void *CameraById(void *model, uint32_t id);
|
||||
|
||||
// A camera's vector property (Offsets::CAMERA_PROP_EYE / _TARGET).
|
||||
// A camera's properties (Offsets::CAMERA_PROP_*): vectors (eye, target) and
|
||||
// scalars (clips, field of view, roll, yaw).
|
||||
Vec3 GetVector(void *camera, uint32_t id);
|
||||
void SetVector(void *camera, uint32_t id, const Vec3 &v);
|
||||
float GetScalar(void *camera, uint32_t id);
|
||||
void SetScalar(void *camera, uint32_t id, float value);
|
||||
|
||||
// CameraSetDeviceTransforms (FUN_CAMERA_SET_DEVICE_TRANSFORMS): the device's
|
||||
// projection and view from the camera, framed over `rect` {bottom, left,
|
||||
// top, right}.
|
||||
void SetDeviceTransforms(void *camera, const float (&rect)[4]);
|
||||
|
||||
} // namespace Model::Camera
|
||||
|
||||
+53
-13
@@ -96,8 +96,8 @@ constexpr uint32_t kGeosetGroupDefaults[16] = {1, 101, 201, 301, 401, 501,
|
||||
using CharCompCreate_t = void *(__cdecl *)();
|
||||
using CharCompDestroy_t = void(__fastcall *)(void *builder);
|
||||
using CharCompSetInfo_t = char(__thiscall *)(void *builder, const CharCompInfo *info);
|
||||
using CharCompSetItem_t = void(__thiscall *)(void *builder, int slot, int itemDisplayID);
|
||||
using CharCompPump_t = char(__thiscall *)(void *builder, int *unused);
|
||||
using CharCompSetItem_t = void(__thiscall *)(void *builder, uint32_t slot, uint32_t itemDisplayID);
|
||||
using CharCompPump_t = char(__thiscall *)(void *builder, int force);
|
||||
|
||||
// A pending / finished dress for one Model frame. The model loads async, and
|
||||
// the engine only dresses a LOADED model (its poll gate FUN_00607da0 checks
|
||||
@@ -301,10 +301,32 @@ const uint8_t *DressRecord(uint32_t displayID) {
|
||||
return bake != nullptr && bake[0] != '\0' ? extraRec : nullptr;
|
||||
}
|
||||
|
||||
void *CreateDisplayModel(void *scene, uint32_t displayID, const char **path) {
|
||||
using ApplyDisplay_t = void(__fastcall *)(void *model, const uint8_t *display,
|
||||
const uint8_t *modelData);
|
||||
const uint8_t *display = DBC::Record(Offsets::VAR_CREATUREDISPLAYINFO_RECORDS,
|
||||
Offsets::VAR_CREATUREDISPLAYINFO_COUNT, displayID);
|
||||
if (display == nullptr)
|
||||
return nullptr;
|
||||
const uint8_t *modelData = DBC::Record(
|
||||
Offsets::VAR_CREATUREMODELDATA_RECORDS, Offsets::VAR_CREATUREMODELDATA_COUNT,
|
||||
Game::Read<uint32_t>(display, Offsets::OFF_CREATUREDISPLAYINFO_MODEL_ID));
|
||||
if (modelData == nullptr)
|
||||
return nullptr;
|
||||
const char *file = Game::Read<const char *>(modelData, Offsets::OFF_CREATUREMODELDATA_MODEL_PATH);
|
||||
if (path != nullptr)
|
||||
*path = file;
|
||||
void *model = M2::CreateModel(scene, file);
|
||||
if (model != nullptr)
|
||||
reinterpret_cast<ApplyDisplay_t>(Offsets::FUN_APPLY_CREATURE_DISPLAY)(model, display,
|
||||
modelData);
|
||||
return model;
|
||||
}
|
||||
|
||||
// Configure + feed equipment, the engine's order: SetInfo, then the 10 Extra
|
||||
// equipment slots. Field-for-field mirror of FUN_005fb200's NPC branch.
|
||||
void *CreateDresser(void *instance, const uint8_t *extraRec) {
|
||||
void *builder = reinterpret_cast<CharCompCreate_t>(Offsets::FUN_CHARCOMP_CREATE)();
|
||||
void *builder = NewDresser();
|
||||
if (builder == nullptr)
|
||||
return nullptr;
|
||||
|
||||
@@ -333,12 +355,10 @@ void *CreateDresser(void *instance, const uint8_t *extraRec) {
|
||||
DestroyDresser(builder); // releases the model ref already copied in
|
||||
return nullptr;
|
||||
}
|
||||
auto setItem = reinterpret_cast<CharCompSetItem_t>(Offsets::FUN_CHARCOMP_SET_ITEM);
|
||||
for (int slot = 0; slot < 10; ++slot) {
|
||||
const auto displayID =
|
||||
static_cast<int>(field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_EQUIP + slot * 4));
|
||||
for (uint32_t slot = 0; slot < 10; ++slot) {
|
||||
const uint32_t displayID = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_EQUIP + slot * 4);
|
||||
if (displayID != 0)
|
||||
setItem(builder, slot, displayID);
|
||||
SetDresserItem(builder, slot, displayID);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
@@ -348,16 +368,20 @@ void *CreateDresser(void *instance, const uint8_t *extraRec) {
|
||||
// black. Each child gets the frame's own light/fog callback; it reads the
|
||||
// frame's live light struct, so later SetLight calls reach them too.
|
||||
void LightChildren(void *frame, void *instance) {
|
||||
const void *lightFogCb =
|
||||
reinterpret_cast<const void *>(Offsets::FUN_SIMPLEMODELFFX_LIGHT_FOG_CB);
|
||||
const void *callback = Game::Read<const void *>(instance, Offsets::OFF_MODEL_INSTANCE_RENDER_CB);
|
||||
void *user = Game::Read<void *>(instance, Offsets::OFF_MODEL_INSTANCE_RENDER_CB_USER);
|
||||
if (callback == nullptr) {
|
||||
callback = reinterpret_cast<const void *>(Offsets::FUN_SIMPLEMODELFFX_LIGHT_FOG_CB);
|
||||
user = frame;
|
||||
}
|
||||
for (auto *child = Game::Read<uint8_t *>(instance, Offsets::OFF_MODEL_INSTANCE_CHILD_HEAD);
|
||||
child != nullptr;
|
||||
child = Game::Read<uint8_t *>(child, Offsets::OFF_MODEL_INSTANCE_CHILD_NEXT))
|
||||
M2::SetRenderCallback(child, lightFogCb, frame);
|
||||
M2::SetRenderCallback(child, callback, user);
|
||||
}
|
||||
|
||||
bool PumpDresser(void *dresser) {
|
||||
return reinterpret_cast<CharCompPump_t>(Offsets::FUN_CHARCOMP_PUMP)(dresser, nullptr) != 0;
|
||||
bool PumpDresser(void *dresser, bool force) {
|
||||
return reinterpret_cast<CharCompPump_t>(Offsets::FUN_CHARCOMP_PUMP)(dresser, force ? 1 : 0) != 0;
|
||||
}
|
||||
|
||||
void DestroyDresser(void *dresser) {
|
||||
@@ -365,4 +389,20 @@ void DestroyDresser(void *dresser) {
|
||||
reinterpret_cast<CharCompDestroy_t>(Offsets::FUN_CHARCOMP_DESTROY)(dresser);
|
||||
}
|
||||
|
||||
void *NewDresser() { return reinterpret_cast<CharCompCreate_t>(Offsets::FUN_CHARCOMP_CREATE)(); }
|
||||
|
||||
void CloneDresser(void *dresser, void *model, void *source) {
|
||||
using Clone_t = bool(__thiscall *)(void *dresser, void *model, void *source);
|
||||
reinterpret_cast<Clone_t>(Offsets::FUN_CHARCOMP_CLONE)(dresser, model, source);
|
||||
}
|
||||
|
||||
void UnequipDresserSlot(void *dresser, uint32_t slot) {
|
||||
using Unequip_t = void(__thiscall *)(void *dresser, uint32_t slot);
|
||||
reinterpret_cast<Unequip_t>(Offsets::FUN_CHARCOMP_UNEQUIP_SLOT)(dresser, slot);
|
||||
}
|
||||
|
||||
void SetDresserItem(void *dresser, uint32_t slot, uint32_t displayID) {
|
||||
reinterpret_cast<CharCompSetItem_t>(Offsets::FUN_CHARCOMP_SET_ITEM)(dresser, slot, displayID);
|
||||
}
|
||||
|
||||
} // namespace Model::DisplayInfo
|
||||
|
||||
+24
-4
@@ -18,17 +18,37 @@ namespace Model::DisplayInfo {
|
||||
// null when the display gets no character customization.
|
||||
const uint8_t *DressRecord(uint32_t displayID);
|
||||
|
||||
// A new model of the display in `scene`, skinned as AttachDisplayModel skins
|
||||
// one (CreateModelFromPath on the CreatureModelData path, then
|
||||
// ApplyCreatureDisplay), that the caller owns one reference on. Null for a
|
||||
// display with no CreatureDisplayInfo or CreatureModelData row. Undressed: a
|
||||
// DressRecord display still needs CreateDresser once it has loaded. `path`,
|
||||
// when given, receives the model's file path (DBC memory).
|
||||
void *CreateDisplayModel(void *scene, uint32_t displayID, const char **path = nullptr);
|
||||
|
||||
// The engine character compositor, configured to dress the LOADED model
|
||||
// `instance` from `extraRec` (a DressRecord) and fed its equipment, or null
|
||||
// when the engine pool is empty or rejects the info. It holds a reference on
|
||||
// the model. Pump it with PumpDresser until that returns true; DestroyDresser
|
||||
// releases it (null is a no-op).
|
||||
// releases it (null is a no-op). `force` loads every texture at once,
|
||||
// ignoring the engine's per-frame load budget.
|
||||
void *CreateDresser(void *instance, const uint8_t *extraRec);
|
||||
bool PumpDresser(void *dresser);
|
||||
bool PumpDresser(void *dresser, bool force = false);
|
||||
void DestroyDresser(void *dresser);
|
||||
|
||||
// Gives every model attached to `instance` the light and fog callback that
|
||||
// `frame` gives its own model, so the children are lit like it.
|
||||
// The compositor's own calls, for a dresser built another way. A new one
|
||||
// (FUN_CHARCOMP_CREATE), null when the pool is empty.
|
||||
void *NewDresser();
|
||||
// FUN_CHARCOMP_CLONE: `dresser` dresses `model` as `source` dresses its own;
|
||||
// the caller gives the dresser its reference on `model` first.
|
||||
void CloneDresser(void *dresser, void *model, void *source);
|
||||
// One component slot cleared, or set to an ItemDisplayInfo ID.
|
||||
void UnequipDresserSlot(void *dresser, uint32_t slot);
|
||||
void SetDresserItem(void *dresser, uint32_t slot, uint32_t displayID);
|
||||
|
||||
// Gives every model attached to `instance` the light and fog callback of
|
||||
// `instance` itself, else the one `frame` gives its own model, so the
|
||||
// children are lit like it.
|
||||
void LightChildren(void *frame, void *instance);
|
||||
|
||||
} // namespace Model::DisplayInfo
|
||||
|
||||
+237
-316
@@ -13,25 +13,23 @@
|
||||
|
||||
// `DressUpModel:UndressSlot(slot)`, the 4.3.4 `DressUpModel:TryOn(item)`
|
||||
// (DressUpModelFrame_Scripts.cpp), and retail's TryOn result, IsGeoReady,
|
||||
// SetSheathed / GetSheathed and SetAutoDress / GetAutoDress.
|
||||
// SetSheathed / GetSheathed, SetAutoDress / GetAutoDress, IsSlotAllowed /
|
||||
// IsSlotVisible and the ItemTransmogInfo methods.
|
||||
//
|
||||
// UndressSlot is one step of vanilla's own Undress (DressUpModel::ClearModel
|
||||
// 0x00504490), which 4.3.4 split per slot: a component slot is cleared with
|
||||
// UnequipCharacterSlot, a held lane is detached with DetachHeldItemSlot and
|
||||
// zeroed. The slot map is 4.3.4's, from inventory slot to component slot.
|
||||
// That step, the lanes and the weapon hanging below are Model::Outfit's,
|
||||
// which the ModelScene actor shares.
|
||||
//
|
||||
// TryOn keeps vanilla's worker (TryOnItem, FUN_DRESSUPMODEL_TRY_ON_ITEM) and
|
||||
// takes 4.3.4's argument forms in front of it: an item ID, an item link or
|
||||
// `item:` string, a trade-skill `enchant:` link (the item its recipe makes),
|
||||
// or the name of an item the player carries. Anything else shows
|
||||
// takes the arguments Model::TryOn reads in front of it. No item shows
|
||||
// ERR_NOT_EQUIPPABLE, as 4.3.4 does.
|
||||
//
|
||||
// Retail's hand argument is an inventory slot name, matched the way
|
||||
// GetInventorySlotInfo matches it (SStrCmpI over the PaperDollItemFrame
|
||||
// rows): MainHandSlot or SecondaryHandSlot. The worker picks a hand at one
|
||||
// site (SITE_TRY_ON_HAND_CHOICE); a named hand sends a held item down that
|
||||
// hand's path, so the worker's own eviction of an incompatible other lane
|
||||
// runs, as 4.3.4's does for a forced hand.
|
||||
// The worker picks a hand at one site (SITE_TRY_ON_HAND_CHOICE); a named hand
|
||||
// sends a held item down that hand's path, so the worker's own eviction of an
|
||||
// incompatible other lane runs, as 4.3.4's does for a forced hand.
|
||||
//
|
||||
// The enchant glow is the player's AttachEnchantVisual step on the item the
|
||||
// worker hangs: FindChild at the attachment AttachItemModel used, then
|
||||
@@ -41,11 +39,10 @@
|
||||
// the link's enchant field, as 4.3.4 passes it. The worker's cache callback
|
||||
// re-runs with neither hand nor enchant, as 4.3.4's does.
|
||||
//
|
||||
// 4.3.4's worker (`TryOnWithVisual`, 0x00A05010) tries on what a recipe makes:
|
||||
// an item with no inventory type whose spell teaches a CREATE_ITEM spell is
|
||||
// swapped for the made item. Vanilla's worker shows ERR_NOT_EQUIPPABLE
|
||||
// instead. `TryOnItem` adds the swap in front of the worker, for our TryOn
|
||||
// and for the worker's cache-load callback, so an uncached pattern works too.
|
||||
// Vanilla's worker shows ERR_NOT_EQUIPPABLE for a recipe, where 4.3.4's
|
||||
// tries on what it makes. `TryOnItem` adds that swap (Model::TryOn::WornItem)
|
||||
// in front of the worker, for our TryOn and for the worker's cache-load
|
||||
// callback, so an uncached pattern works too.
|
||||
//
|
||||
// TryOn's result (retail's ItemTryOnReason) is read from the same gates the
|
||||
// worker passes: an uncached record is DataPending (the worker has queued the
|
||||
@@ -63,30 +60,34 @@
|
||||
//
|
||||
// Auto-dress off undresses the pane with ClearModel right after
|
||||
// SetModelInstance clones the unit, unless Dress() is the one rebuilding it.
|
||||
//
|
||||
// The ItemTransmogInfo methods are the ModelScene actor's (Outfit::Worn, the
|
||||
// item ID as the appearance, the glowing enchant as the illusion) on what the
|
||||
// pane wears: the unit's visible items when SetModelInstance builds it, then
|
||||
// each slot the worker fills (an armor arm's component slot, or the lane
|
||||
// TryOnAttach hangs) and each lane it evicts. A read first drops every slot
|
||||
// the pane no longer shows (Outfit::TrimWorn), which covers the engine's own
|
||||
// Undress. SetItemTransmogInfo is TryOn without the worker's
|
||||
// ERR_NOT_EQUIPPABLE: the worker shows it only in its default arm, once the
|
||||
// record is cached, so a quiet try-on of such an item skips the worker, then
|
||||
// or when its cache callback runs.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "Common.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Dressable.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "model/ItemDisplay.h"
|
||||
#include "model/M2.h"
|
||||
#include "model/Outfit.h"
|
||||
#include "model/TryOn.h"
|
||||
#include "object/Resolve.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace Model::DressUp {
|
||||
namespace {
|
||||
|
||||
using UnequipSlot_t = void(__thiscall *)(void *component, uint32_t slot);
|
||||
using DetachHeldItem_t = void(__fastcall *)(void *model, uint32_t bodySlot, uint32_t sheatheType,
|
||||
bool shield);
|
||||
using AttachItemModel_t = int(__fastcall *)(void *model, const void *record, uint32_t bodySlot,
|
||||
@@ -95,116 +96,38 @@ using AttachItemModel_t = int(__fastcall *)(void *model, const void *record, uin
|
||||
using TryOnItem_t = void(__thiscall *)(void *pane, uint32_t itemID);
|
||||
using SetModelInstance_t = void(__thiscall *)(void *pane, void *model);
|
||||
using ClearModel_t = void(__thiscall *)(void *pane);
|
||||
using GetHeldItemDisplay_t = const void *(__thiscall *)(void *unit, uint32_t hand);
|
||||
using FindChild_t = void *(__thiscall *)(void *model, int attachId);
|
||||
using GetVisibleItem_t = const void *(__thiscall *)(void *player, uint32_t slot);
|
||||
using GetDescVisual_t = int(__thiscall *)(const void *visibleItem);
|
||||
using DisplayError_t = void(__cdecl *)(uint32_t id, ...);
|
||||
|
||||
// Body slots of the two held lanes.
|
||||
constexpr uint32_t kBodySlotMainHand = 0xF;
|
||||
constexpr uint32_t kBodySlotOffHand = 0x10;
|
||||
|
||||
// Retail's Enum.ItemTryOnReason.
|
||||
constexpr int kTryOnSuccess = 0;
|
||||
constexpr int kTryOnWrongRace = 1;
|
||||
constexpr int kTryOnNotEquippable = 2;
|
||||
constexpr int kTryOnDataPending = 3;
|
||||
|
||||
// A held lane (OFF_DRESSUPMODEL_*_HAND_LANE).
|
||||
struct HeldLane {
|
||||
uint8_t itemClass; // 0 = empty
|
||||
uint8_t unknown1[2];
|
||||
uint8_t inventoryType;
|
||||
uint8_t sheatheType;
|
||||
uint8_t unknown5[3];
|
||||
};
|
||||
static_assert(sizeof(HeldLane) == 8, "ClearModel zeroes 8 bytes per lane");
|
||||
|
||||
struct Lane {
|
||||
uintptr_t offset;
|
||||
uint32_t bodySlot;
|
||||
};
|
||||
constexpr Lane kLanes[2] = {
|
||||
{Offsets::OFF_DRESSUPMODEL_MAIN_HAND_LANE, kBodySlotMainHand},
|
||||
{Offsets::OFF_DRESSUPMODEL_OFF_HAND_LANE, kBodySlotOffHand},
|
||||
};
|
||||
static_assert(Offsets::OFF_DRESSUPMODEL_OFF_HAND_LANE ==
|
||||
Offsets::OFF_DRESSUPMODEL_MAIN_HAND_LANE + sizeof(Outfit::HeldLane),
|
||||
"the pane's two lanes are one Outfit::Lanes");
|
||||
|
||||
struct PaneState {
|
||||
bool autoDress = true;
|
||||
bool sheathSet = false; // SetSheathed was called; weapons follow the two below
|
||||
bool sheathed = false;
|
||||
bool hideWeapons = false;
|
||||
const void *laneDisplay[2] = {}; // ItemDisplayInfo row on each lane, or null
|
||||
uint32_t laneVisual[2] = {}; // enchant ItemVisuals row on each lane, or 0
|
||||
Outfit::Weapons weapons;
|
||||
Outfit::Worn worn = {};
|
||||
std::unordered_set<uint32_t> quietPending; // quiet try-ons waiting for their record
|
||||
};
|
||||
std::unordered_map<void *, PaneState> g_panes;
|
||||
|
||||
void *g_tryOnPane = nullptr; // the pane whose TryOnItem worker is running
|
||||
uint32_t g_tryOnBodySlot = 0; // the hand TryOn named (kBodySlot*), or 0
|
||||
uint32_t g_tryOnEnchant = 0; // the SpellItemEnchantment TryOn gave, or 0
|
||||
bool g_tryOnQuiet = false; // the try-on shows no ERR_NOT_EQUIPPABLE
|
||||
uint32_t g_tryOnItem = 0; // the item the worker is putting on
|
||||
bool g_dressing = false; // Dress() is rebuilding the pane
|
||||
|
||||
uintptr_t g_mainHandPath = Offsets::TRY_ON_MAIN_HAND_PATH;
|
||||
uintptr_t g_offHandPath = Offsets::TRY_ON_OFF_HAND_PATH;
|
||||
|
||||
HeldLane &LaneOf(void *pane, int i) {
|
||||
return Game::Ref<HeldLane>(pane, kLanes[i].offset);
|
||||
Outfit::Lanes &LanesOf(void *pane) {
|
||||
return Game::Ref<Outfit::Lanes>(pane, Offsets::OFF_DRESSUPMODEL_MAIN_HAND_LANE);
|
||||
}
|
||||
|
||||
int LaneIndex(uint32_t bodySlot) {
|
||||
return bodySlot == kBodySlotOffHand ? 1 : 0;
|
||||
}
|
||||
|
||||
// The glow `enchantID` gives an item shown with `display`: none when the
|
||||
// display has its own.
|
||||
uint32_t EnchantVisual(const void *display, uint32_t enchantID) {
|
||||
if (display == nullptr ||
|
||||
Model::ItemDisplay::OwnVisual(static_cast<const uint8_t *>(display)) != 0)
|
||||
return 0;
|
||||
const uint8_t *enchant = DBC::Record(Offsets::VAR_SPELLITEMENCHANT_RECORDS,
|
||||
Offsets::VAR_SPELLITEMENCHANT_COUNT, enchantID);
|
||||
return enchant != nullptr
|
||||
? Game::Read<uint32_t>(enchant, Offsets::OFF_SPELLITEMENCHANT_ITEM_VISUAL)
|
||||
: 0;
|
||||
}
|
||||
|
||||
// AttachEnchantVisual's step (0x005EED50): the glow on the item hung at
|
||||
// `attachId`.
|
||||
void HangVisual(void *model, int attachId, uint32_t visual) {
|
||||
if (visual == 0 || attachId < 0)
|
||||
return;
|
||||
if (void *item = reinterpret_cast<FindChild_t>(Offsets::FUN_M2MODEL_FIND_CHILD)(model,
|
||||
attachId))
|
||||
M2::AttachItemVisuals(item, visual);
|
||||
}
|
||||
|
||||
// DetachHeldItemSlot on lane `i`'s item.
|
||||
void DetachLane(void *model, int i, const HeldLane &lane) {
|
||||
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(
|
||||
model, kLanes[i].bodySlot, lane.sheatheType,
|
||||
lane.inventoryType == Offsets::INVTYPE_SHIELD);
|
||||
}
|
||||
|
||||
// The unit's AttachHandItem step on each occupied lane: detach it, then hang
|
||||
// it in the hand or the sheath, or leave it off.
|
||||
void ApplyWeapons(void *pane, const PaneState &s) {
|
||||
void *model = Pane::Model(pane);
|
||||
if (model == nullptr)
|
||||
return;
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const HeldLane &lane = LaneOf(pane, i);
|
||||
if (lane.itemClass == 0 || s.laneDisplay[i] == nullptr)
|
||||
continue;
|
||||
DetachLane(model, i, lane);
|
||||
if (s.hideWeapons)
|
||||
continue;
|
||||
const int attachId = reinterpret_cast<AttachItemModel_t>(Offsets::FUN_ATTACH_ITEM_MODEL)(
|
||||
model, s.laneDisplay[i], kLanes[i].bodySlot, lane.sheatheType, s.sheathed,
|
||||
lane.inventoryType == Offsets::INVTYPE_SHIELD,
|
||||
lane.inventoryType == Offsets::INVTYPE_RANGEDRIGHT);
|
||||
HangVisual(model, attachId, s.laneVisual[i]);
|
||||
}
|
||||
Outfit::HangWeapons(Pane::Model(pane), LanesOf(pane), s.weapons, s.sheathed, s.hideWeapons);
|
||||
}
|
||||
|
||||
// Slot 36, after the original has cloned the unit and copied its lanes.
|
||||
@@ -214,30 +137,11 @@ void __fastcall SetModelInstance(void *pane, void * /*edx*/, void *model) {
|
||||
if (model == nullptr)
|
||||
return;
|
||||
PaneState &s = g_panes[pane];
|
||||
s.laneDisplay[0] = s.laneDisplay[1] = nullptr;
|
||||
s.laneVisual[0] = s.laneVisual[1] = 0;
|
||||
// The original filled the lanes from this unit, resolved the same way.
|
||||
const uint64_t guid = Game::Read<uint64_t>(pane, Offsets::OFF_PLAYERMODEL_UNIT_GUID);
|
||||
if (void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid)) {
|
||||
const auto get = Game::Read<GetHeldItemDisplay_t>(
|
||||
Game::Read<const void *>(unit, 0), Offsets::VTBL_UNIT_GET_HELD_ITEM_DISPLAY);
|
||||
s.laneDisplay[0] = get(unit, 0);
|
||||
s.laneDisplay[1] = get(unit, 1);
|
||||
}
|
||||
// AttachHandEnchant's glow (0x005EED00), for players only: the visible
|
||||
// item in the lane's slot (0xF / 0x10, the same numbers as the body
|
||||
// slots), and its visual unless the display hangs its own (-1).
|
||||
if (void *player = Object::ByGuid(Offsets::TYPEMASK_PLAYER, guid)) {
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const void *item = reinterpret_cast<GetVisibleItem_t>(
|
||||
Offsets::FUN_UNIT_GET_VISIBLE_ITEM)(player, kLanes[i].bodySlot);
|
||||
if (item == nullptr || Game::Read<uint32_t>(item, Offsets::OFF_VISIBLE_ITEM_ITEM_ID) == 0)
|
||||
continue;
|
||||
const int visual =
|
||||
reinterpret_cast<GetDescVisual_t>(Offsets::FUN_VISIBLE_ITEM_GET_DESC_VISUAL)(item);
|
||||
s.laneVisual[i] = visual > 0 ? static_cast<uint32_t>(visual) : 0;
|
||||
}
|
||||
}
|
||||
const auto guid = Game::Read<uint64_t>(pane, Offsets::OFF_PLAYERMODEL_UNIT_GUID);
|
||||
Outfit::ReadWeapons(guid, s.weapons);
|
||||
Outfit::ReadWorn(guid != 0 ? ::Object::ByGuid(Offsets::TYPEMASK_UNIT, guid) : nullptr, guid,
|
||||
s.weapons, s.worn);
|
||||
if (!s.autoDress && !g_dressing) {
|
||||
reinterpret_cast<ClearModel_t>(Offsets::FUN_DRESSUPMODEL_CLEAR_MODEL)(pane);
|
||||
return;
|
||||
@@ -246,24 +150,26 @@ void __fastcall SetModelInstance(void *pane, void * /*edx*/, void *model) {
|
||||
ApplyWeapons(pane, s);
|
||||
}
|
||||
|
||||
// TryOnItem's install: record the lane's row and glow, honor SetSheathed,
|
||||
// then hang the glow.
|
||||
// TryOnItem's install: record the lane's row, glow and item, honor
|
||||
// SetSheathed, then hang the glow.
|
||||
int __fastcall TryOnAttach(void *model, const void *record, uint32_t bodySlot,
|
||||
uint32_t sheatheType, bool sheathed, bool shield, bool ranged) {
|
||||
const auto attach = reinterpret_cast<AttachItemModel_t>(Offsets::FUN_ATTACH_ITEM_MODEL);
|
||||
if (g_tryOnPane == nullptr)
|
||||
return attach(model, record, bodySlot, sheatheType, sheathed, shield, ranged);
|
||||
PaneState &s = g_panes[g_tryOnPane];
|
||||
const int lane = LaneIndex(bodySlot);
|
||||
s.laneDisplay[lane] = record;
|
||||
s.laneVisual[lane] = EnchantVisual(record, g_tryOnEnchant);
|
||||
const int lane = Outfit::LaneIndex(bodySlot);
|
||||
s.weapons.display[lane] = record;
|
||||
s.weapons.visual[lane] = Outfit::EnchantVisual(record, g_tryOnEnchant);
|
||||
s.worn[(lane == 1 ? Offsets::INVSLOT_OFFHAND : Offsets::INVSLOT_MAINHAND) - 1] = {
|
||||
g_tryOnItem, s.weapons.visual[lane] != 0 ? g_tryOnEnchant : 0};
|
||||
if (s.sheathSet) {
|
||||
if (s.hideWeapons)
|
||||
return -1;
|
||||
sheathed = s.sheathed;
|
||||
}
|
||||
const int attachId = attach(model, record, bodySlot, sheatheType, sheathed, shield, ranged);
|
||||
HangVisual(model, attachId, s.laneVisual[lane]);
|
||||
Outfit::HangVisual(model, attachId, s.weapons.visual[lane]);
|
||||
return attachId;
|
||||
}
|
||||
|
||||
@@ -273,9 +179,10 @@ int __fastcall TryOnAttach(void *model, const void *record, uint32_t bodySlot,
|
||||
void __fastcall TryOnEvict(void *model, uint32_t bodySlot, uint32_t sheatheType, bool shield) {
|
||||
if (g_tryOnPane != nullptr) {
|
||||
PaneState &s = g_panes[g_tryOnPane];
|
||||
const int lane = LaneIndex(bodySlot);
|
||||
s.laneDisplay[lane] = nullptr;
|
||||
s.laneVisual[lane] = 0;
|
||||
const int lane = Outfit::LaneIndex(bodySlot);
|
||||
s.weapons.display[lane] = nullptr;
|
||||
s.weapons.visual[lane] = 0;
|
||||
s.worn[(lane == 1 ? Offsets::INVSLOT_OFFHAND : Offsets::INVSLOT_MAINHAND) - 1] = {};
|
||||
}
|
||||
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(model, bodySlot,
|
||||
sheatheType, shield);
|
||||
@@ -302,33 +209,6 @@ __declspec(naked) void HandChoiceThunk() {
|
||||
}
|
||||
}
|
||||
|
||||
// ClearModel's lane step: detach the held item and zero the lane.
|
||||
void ClearLane(void *pane, int i) {
|
||||
HeldLane &lane = LaneOf(pane, i);
|
||||
if (lane.itemClass == 0)
|
||||
return;
|
||||
DetachLane(Pane::Model(pane), i, lane);
|
||||
lane = HeldLane{};
|
||||
}
|
||||
|
||||
// Component slot for a 0-based inventory slot, or -1 (4.3.4 UndressSlot).
|
||||
int ComponentSlot(int slot0) {
|
||||
switch (slot0) {
|
||||
case 0: return 0; // head
|
||||
case 2: return 1; // shoulder
|
||||
case 3: return 2; // shirt
|
||||
case 4: return 3; // chest
|
||||
case 5: return 4; // waist
|
||||
case 6: return 5; // legs
|
||||
case 7: return 6; // feet
|
||||
case 8: return 7; // wrist
|
||||
case 9: return 8; // hands
|
||||
case 14: return 10; // back
|
||||
case 18: return 9; // tabard
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int __fastcall Script_UndressSlot(void *L) {
|
||||
void *pane = Game::Lua::ResolveDressUpModel(L);
|
||||
if (pane == nullptr)
|
||||
@@ -337,166 +217,87 @@ int __fastcall Script_UndressSlot(void *L) {
|
||||
Game::Lua::Error(L, "Usage: %s:UndressSlot(slotIndex)", Game::Lua::ObjectDisplayName(pane));
|
||||
return 0;
|
||||
}
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 2)) - 1;
|
||||
void *component = Game::Read<void *>(pane, Offsets::OFF_DRESSUPMODEL_COMPONENT);
|
||||
if (Pane::Model(pane) == nullptr || component == nullptr)
|
||||
return 0;
|
||||
if (slot0 + 1 == Offsets::INVSLOT_MAINHAND) {
|
||||
ClearLane(pane, 0);
|
||||
} else if (slot0 + 1 == Offsets::INVSLOT_OFFHAND) {
|
||||
ClearLane(pane, 1);
|
||||
} else {
|
||||
const int compSlot = ComponentSlot(slot0);
|
||||
if (compSlot >= 0)
|
||||
reinterpret_cast<UnequipSlot_t>(Offsets::FUN_CHARCOMP_UNEQUIP_SLOT)(
|
||||
component, static_cast<uint32_t>(compSlot));
|
||||
}
|
||||
Outfit::UndressSlot(Pane::Model(pane),
|
||||
Game::Read<void *>(pane, Offsets::OFF_DRESSUPMODEL_COMPONENT),
|
||||
LanesOf(pane), static_cast<int>(Game::Lua::ToNumber(L, 2)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The item a trade-skill recipe makes, from an `enchant:<spellID>` link: a
|
||||
// TRADESPELL whose first effect makes an item (4.3.4's test).
|
||||
uint32_t RecipeItem(const char *link) {
|
||||
const char *enchant = std::strstr(link, "enchant:");
|
||||
if (enchant == nullptr)
|
||||
return 0;
|
||||
const long spellID = std::strtol(enchant + 8, nullptr, 10);
|
||||
if (spellID <= 0)
|
||||
return 0;
|
||||
const uint8_t *spell = DBC::Record(Offsets::VAR_SPELL_RECORDS, Offsets::VAR_SPELL_RECORD_COUNT,
|
||||
static_cast<uint32_t>(spellID));
|
||||
if (spell == nullptr ||
|
||||
(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
|
||||
Offsets::SPELL_ATTR_TRADESPELL) == 0)
|
||||
return 0;
|
||||
return Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_EFFECT_ITEM_TYPE);
|
||||
}
|
||||
|
||||
// The item the worker is given: a cached recipe item becomes what it makes.
|
||||
uint32_t WornItem(uint32_t itemID) {
|
||||
const uint8_t *record = Item::PeekRecord(itemID);
|
||||
if (record != nullptr &&
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE) == 0) {
|
||||
const uint32_t made = Item::Dressable::RecipeItem(record);
|
||||
if (made != 0)
|
||||
return made;
|
||||
}
|
||||
return itemID;
|
||||
}
|
||||
|
||||
// `__thiscall(pane, itemID)` as `__fastcall` with the EDX slot unused, so it
|
||||
// can stand at the worker's call sites.
|
||||
void __fastcall TryOnItem(void *pane, void *, uint32_t itemID) {
|
||||
g_tryOnPane = pane;
|
||||
reinterpret_cast<TryOnItem_t>(Offsets::FUN_DRESSUPMODEL_TRY_ON_ITEM)(pane, WornItem(itemID));
|
||||
g_tryOnPane = nullptr;
|
||||
}
|
||||
|
||||
// The lane a hand name picks: the PaperDollItemFrame row it names
|
||||
// (GetInventorySlotInfo's match), if that row is a held slot; else 0.
|
||||
uint32_t NamedHand(const char *name) {
|
||||
const auto *records = Game::Read<const uint8_t *>(Offsets::VAR_PAPERDOLLITEMFRAME_RECORDS);
|
||||
const int count = Game::Read<int>(Offsets::VAR_PAPERDOLLITEMFRAME_COUNT);
|
||||
if (records == nullptr)
|
||||
return 0;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint8_t *row = records + i * Offsets::OFF_PAPERDOLLITEMFRAME_RECORD_STRIDE;
|
||||
if (Game::StrCmpI(Game::Read<const char *>(row, Offsets::OFF_PAPERDOLLITEMFRAME_SLOT_NAME),
|
||||
name) != 0)
|
||||
continue;
|
||||
switch (Game::Read<int>(row, Offsets::OFF_PAPERDOLLITEMFRAME_SLOT_NUMBER)) {
|
||||
case Offsets::INVSLOT_MAINHAND: return kBodySlotMainHand;
|
||||
case Offsets::INVSLOT_OFFHAND: return kBodySlotOffHand;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
// The worker's armor gate: the component and the ready byte.
|
||||
bool ArmorReady(void *pane) {
|
||||
return Game::Read<void *>(pane, Offsets::OFF_DRESSUPMODEL_COMPONENT) != nullptr &&
|
||||
Game::Read<uint8_t>(pane, Offsets::OFF_DRESSUPMODEL_READY) != 0;
|
||||
}
|
||||
|
||||
// What the worker did with `itemID`, from the gates it passes (0x00504540).
|
||||
int TryOnResult(void *pane, uint32_t itemID) {
|
||||
const uint8_t *record = Item::PeekRecord(itemID);
|
||||
if (record == nullptr)
|
||||
return kTryOnDataPending;
|
||||
return TryOn::kDataPending;
|
||||
if (Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID) == 0 ||
|
||||
Pane::Model(pane) == nullptr)
|
||||
return kTryOnNotEquippable;
|
||||
switch (Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE)) {
|
||||
case Offsets::INVTYPE_HEAD:
|
||||
case Offsets::INVTYPE_SHOULDER:
|
||||
case Offsets::INVTYPE_BODY:
|
||||
case Offsets::INVTYPE_CHEST:
|
||||
case Offsets::INVTYPE_WAIST:
|
||||
case Offsets::INVTYPE_LEGS:
|
||||
case Offsets::INVTYPE_FEET:
|
||||
case Offsets::INVTYPE_WRIST:
|
||||
case Offsets::INVTYPE_HAND:
|
||||
case Offsets::INVTYPE_CLOAK:
|
||||
case Offsets::INVTYPE_TABARD:
|
||||
case Offsets::INVTYPE_ROBE:
|
||||
// The armor arms: the component and the ready byte.
|
||||
return Game::Read<void *>(pane, Offsets::OFF_DRESSUPMODEL_COMPONENT) != nullptr &&
|
||||
Game::Read<uint8_t>(pane, Offsets::OFF_DRESSUPMODEL_READY) != 0
|
||||
? kTryOnSuccess
|
||||
: kTryOnNotEquippable;
|
||||
case Offsets::INVTYPE_WEAPON:
|
||||
case Offsets::INVTYPE_SHIELD:
|
||||
case Offsets::INVTYPE_RANGED:
|
||||
case Offsets::INVTYPE_2HWEAPON:
|
||||
case Offsets::INVTYPE_WEAPONMAINHAND:
|
||||
case Offsets::INVTYPE_WEAPONOFFHAND:
|
||||
case Offsets::INVTYPE_HOLDABLE:
|
||||
case Offsets::INVTYPE_THROWN:
|
||||
case Offsets::INVTYPE_RANGEDRIGHT:
|
||||
return kTryOnSuccess; // the held arm
|
||||
return TryOn::kNotEquippable;
|
||||
switch (Outfit::ArmOf(Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE))) {
|
||||
case Outfit::Arm::Armor:
|
||||
return ArmorReady(pane) ? TryOn::kSuccess : TryOn::kNotEquippable;
|
||||
case Outfit::Arm::Held:
|
||||
return TryOn::kSuccess;
|
||||
default:
|
||||
return kTryOnNotEquippable;
|
||||
return TryOn::kNotEquippable;
|
||||
}
|
||||
}
|
||||
|
||||
// `__thiscall(pane, itemID)` as `__fastcall` with the EDX slot unused, so it
|
||||
// can stand at the worker's call sites. An armor arm the worker took wears
|
||||
// the item in its slot.
|
||||
void __fastcall TryOnItem(void *pane, void *, uint32_t itemID) {
|
||||
PaneState &s = g_panes[pane];
|
||||
const bool quiet = g_tryOnQuiet || s.quietPending.erase(itemID) != 0;
|
||||
const uint32_t item = TryOn::WornItem(itemID);
|
||||
const uint8_t *record = Item::PeekRecord(item);
|
||||
uint32_t componentSlot = 0;
|
||||
const Outfit::Arm arm =
|
||||
record != nullptr
|
||||
? Outfit::ArmOf(Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE),
|
||||
&componentSlot)
|
||||
: Outfit::Arm::Nothing;
|
||||
if (record == nullptr) {
|
||||
if (quiet)
|
||||
s.quietPending.insert(item);
|
||||
} else if (quiet && arm == Outfit::Arm::NotEquippable) {
|
||||
return;
|
||||
}
|
||||
g_tryOnPane = pane;
|
||||
g_tryOnItem = item;
|
||||
reinterpret_cast<TryOnItem_t>(Offsets::FUN_DRESSUPMODEL_TRY_ON_ITEM)(pane, item);
|
||||
g_tryOnPane = nullptr;
|
||||
g_tryOnItem = 0;
|
||||
if (arm == Outfit::Arm::Armor && TryOnResult(pane, item) == TryOn::kSuccess)
|
||||
s.worn[Outfit::InvSlotOfComponent(componentSlot) - 1] = {item, 0};
|
||||
}
|
||||
|
||||
// TryOn's step: the worker with the request's hand, enchant and quiet.
|
||||
int TryOnRequest(void *pane, const TryOn::Request &request) {
|
||||
g_tryOnBodySlot = request.bodySlot;
|
||||
g_tryOnEnchant = request.enchant;
|
||||
g_tryOnQuiet = request.quiet;
|
||||
TryOnItem(pane, nullptr, request.itemID);
|
||||
g_tryOnBodySlot = g_tryOnEnchant = 0;
|
||||
g_tryOnQuiet = false;
|
||||
return TryOnResult(pane, TryOn::WornItem(request.itemID));
|
||||
}
|
||||
|
||||
int __fastcall Script_TryOn(void *L) {
|
||||
void *pane = Game::Lua::ResolveDressUpModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
// Read before the name search below, which stomps the Lua stack.
|
||||
const uint32_t bodySlot = Game::Lua::Type(L, 3) == Game::Lua::TYPE_STRING
|
||||
? NamedHand(Game::Lua::ToString(L, 3))
|
||||
: 0;
|
||||
const bool enchantGiven = Game::Lua::IsNumber(L, 4) != 0;
|
||||
uint32_t enchant = enchantGiven ? static_cast<uint32_t>(Game::Lua::ToNumber(L, 4)) : 0;
|
||||
uint32_t itemID = 0;
|
||||
if (Game::Lua::Type(L, 2) == Game::Lua::TYPE_NUMBER) {
|
||||
itemID = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
|
||||
} else if (Game::Lua::Type(L, 2) == Game::Lua::TYPE_STRING) {
|
||||
// Copied: the name search below stomps the Lua stack.
|
||||
char s[256];
|
||||
std::snprintf(s, sizeof(s), "%s", Game::Lua::ToString(L, 2));
|
||||
if (std::strstr(s, "enchant:") != nullptr) {
|
||||
itemID = RecipeItem(s);
|
||||
} else {
|
||||
// An item link, `item:` string or numeric string resolves to an ID;
|
||||
// any other string is the name of an item the player carries.
|
||||
const Item::Arg::Resolved arg = Item::Arg::ResolveString(s);
|
||||
Item::Location::ByGUIDResult found{};
|
||||
if (arg.itemID != 0) {
|
||||
itemID = static_cast<uint32_t>(arg.itemID);
|
||||
if (!enchantGiven)
|
||||
enchant = static_cast<uint32_t>(arg.enchant);
|
||||
} else if (arg.name != nullptr && Item::Location::FindByArg(L, arg, &found))
|
||||
itemID = static_cast<uint32_t>(Item::ID::FromCGItem(found.item));
|
||||
}
|
||||
}
|
||||
if (itemID == 0) {
|
||||
reinterpret_cast<DisplayError_t>(Offsets::FUN_GAMEUI_DISPLAY_ERROR)(
|
||||
Offsets::GAME_ERROR_NOT_EQUIPPABLE);
|
||||
Game::Lua::PushNumber(L, kTryOnNotEquippable);
|
||||
const TryOn::Request request = TryOn::ReadArgs(L);
|
||||
if (request.itemID == 0) {
|
||||
TryOn::ShowNotEquippable();
|
||||
Game::Lua::PushNumber(L, TryOn::kNotEquippable);
|
||||
return 1;
|
||||
}
|
||||
g_tryOnBodySlot = bodySlot;
|
||||
g_tryOnEnchant = enchant;
|
||||
TryOnItem(pane, nullptr, itemID);
|
||||
g_tryOnBodySlot = g_tryOnEnchant = 0;
|
||||
Game::Lua::PushNumber(L, TryOnResult(pane, WornItem(itemID)));
|
||||
Game::Lua::PushNumber(L, TryOnRequest(pane, request));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -552,6 +353,81 @@ int __fastcall Script_GetAutoDress(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// What the pane wears now: nothing without a model, and no slot it no
|
||||
// longer shows.
|
||||
const Outfit::Worn &WornOf(void *pane) {
|
||||
PaneState &s = g_panes[pane];
|
||||
if (Pane::Model(pane) == nullptr) {
|
||||
for (Outfit::Appearance &slot : s.worn)
|
||||
slot = {};
|
||||
}
|
||||
Outfit::TrimWorn(Game::Read<void *>(pane, Offsets::OFF_DRESSUPMODEL_COMPONENT), LanesOf(pane),
|
||||
s.worn);
|
||||
return s.worn;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetItemTransmogInfo(void *L) {
|
||||
void *pane = Game::Lua::ResolveDressUpModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:GetItemTransmogInfo(inventorySlot)",
|
||||
Game::Lua::ObjectDisplayName(pane));
|
||||
return 0;
|
||||
}
|
||||
const int invSlot = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
if (Pane::Model(pane) == nullptr || invSlot < 1 || invSlot > Outfit::kInventorySlots)
|
||||
return 0;
|
||||
TryOn::PushTransmogInfo(L, WornOf(pane)[invSlot - 1]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetItemTransmogInfoList(void *L) {
|
||||
void *pane = Game::Lua::ResolveDressUpModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
TryOn::PushTransmogInfoList(L, WornOf(pane));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// TryOn with the info's item and enchant, quiet. No appearance changes
|
||||
// nothing.
|
||||
int __fastcall Script_SetItemTransmogInfo(void *L) {
|
||||
void *pane = Game::Lua::ResolveDressUpModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
TryOn::Request request;
|
||||
if (!TryOn::ReadTransmogInfo(L, Game::Lua::ObjectDisplayName(pane), request))
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, request.itemID == 0 ? TryOn::kNotEquippable
|
||||
: TryOnRequest(pane, request));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The slots the worker's arms put an item in. No 1.12 model hides a slot it
|
||||
// takes, so IsSlotVisible gives the same.
|
||||
int SlotQuery(void *L, const char *usage) {
|
||||
void *pane = Game::Lua::ResolveDressUpModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, usage, Game::Lua::ObjectDisplayName(pane));
|
||||
return 0;
|
||||
}
|
||||
Game::Lua::PushBool(L, Pane::Model(pane) != nullptr &&
|
||||
Outfit::SlotAllowed(static_cast<int>(Game::Lua::ToNumber(L, 2)),
|
||||
ArmorReady(pane)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsSlotAllowed(void *L) {
|
||||
return SlotQuery(L, "Usage: %s:IsSlotAllowed(slot)");
|
||||
}
|
||||
|
||||
int __fastcall Script_IsSlotVisible(void *L) {
|
||||
return SlotQuery(L, "Usage: %s:IsSlotVisible(slot)");
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"UndressSlot", &Script_UndressSlot},
|
||||
{"TryOn", &Script_TryOn},
|
||||
@@ -561,6 +437,11 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"GetSheathed", &Script_GetSheathed},
|
||||
{"SetAutoDress", &Script_SetAutoDress},
|
||||
{"GetAutoDress", &Script_GetAutoDress},
|
||||
{"GetItemTransmogInfo", &Script_GetItemTransmogInfo},
|
||||
{"GetItemTransmogInfoList", &Script_GetItemTransmogInfoList},
|
||||
{"SetItemTransmogInfo", &Script_SetItemTransmogInfo},
|
||||
{"IsSlotAllowed", &Script_IsSlotAllowed},
|
||||
{"IsSlotVisible", &Script_IsSlotVisible},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kUndressArgs[] = {
|
||||
@@ -609,18 +490,58 @@ const Game::Doc::Field kEnabledRets[] = {
|
||||
const Game::Doc::Function kGetAutoDress{
|
||||
"Returns whether the model wears its unit's equipment when the unit is set.", {},
|
||||
kEnabledRets};
|
||||
const Game::Doc::Field kTransmogInfoRets[] = {
|
||||
Game::Doc::Req("itemTransmogInfo", "ItemTransmogInfo",
|
||||
"What the slot wears: appearanceID is the item ID, illusionID the enchant whose "
|
||||
"glow a weapon shows. Nil when the model shows nothing."),
|
||||
};
|
||||
const Game::Doc::Function kGetItemTransmogInfo{
|
||||
"Returns what the model wears in one inventory slot.", kUndressArgs, kTransmogInfoRets};
|
||||
const Game::Doc::Field kTransmogInfoListRets[] = {
|
||||
Game::Doc::Req("infoList", "table", "An ItemTransmogInfo for each inventory slot 1 to 19."),
|
||||
};
|
||||
const Game::Doc::Function kGetItemTransmogInfoList{
|
||||
"Returns what the model wears in each inventory slot.", {}, kTransmogInfoListRets};
|
||||
const Game::Doc::Field kSetTransmogInfoArgs[] = {
|
||||
Game::Doc::Req("transmogInfo", "ItemTransmogInfo",
|
||||
"appearanceID is an item ID, illusionID a SpellItemEnchantment ID."),
|
||||
Game::Doc::Opt("inventorySlots", "number", nullptr, "16 or 17: the hand that holds a weapon."),
|
||||
Game::Doc::Opt("ignoreChildItems", "bool", "false", "Has no effect."),
|
||||
};
|
||||
const Game::Doc::Function kSetItemTransmogInfo{
|
||||
"Puts an item on the model, as TryOn does. Shows no error. An appearanceID of 0 changes "
|
||||
"nothing.",
|
||||
kSetTransmogInfoArgs, kTryOnRets};
|
||||
const Game::Doc::Field kSlotAllowedRets[] = {
|
||||
Game::Doc::Req("allowed", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kIsSlotAllowed{
|
||||
"Returns whether TryOn can put an item in an inventory slot of the model. The hands are "
|
||||
"always allowed. An armor slot is allowed when TryOn can put armor on the model.",
|
||||
kUndressArgs, kSlotAllowedRets};
|
||||
const Game::Doc::Field kSlotVisibleRets[] = {
|
||||
Game::Doc::Req("visible", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kIsSlotVisible{
|
||||
"Returns whether an item in an inventory slot shows on the model. Same as IsSlotAllowed.",
|
||||
kUndressArgs, kSlotVisibleRets};
|
||||
const Game::Doc::Method g_methodDocs[] = {
|
||||
{"UndressSlot", &kUndressSlot}, {"TryOn", &kTryOn},
|
||||
{"Dress", &kDress}, {"IsGeoReady", &kIsGeoReady},
|
||||
{"SetSheathed", &kSetSheathed}, {"GetSheathed", &kGetSheathed},
|
||||
{"SetAutoDress", &kSetAutoDress}, {"GetAutoDress", &kGetAutoDress},
|
||||
{"GetItemTransmogInfo", &kGetItemTransmogInfo},
|
||||
{"GetItemTransmogInfoList", &kGetItemTransmogInfoList},
|
||||
{"SetItemTransmogInfo", &kSetItemTransmogInfo},
|
||||
{"IsSlotAllowed", &kIsSlotAllowed},
|
||||
{"IsSlotVisible", &kIsSlotVisible},
|
||||
};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kItemTryOnReason[] = {
|
||||
{"Success", kTryOnSuccess},
|
||||
{"WrongRace", kTryOnWrongRace},
|
||||
{"NotEquippable", kTryOnNotEquippable},
|
||||
{"DataPending", kTryOnDataPending},
|
||||
{"Success", TryOn::kSuccess},
|
||||
{"WrongRace", TryOn::kWrongRace},
|
||||
{"NotEquippable", TryOn::kNotEquippable},
|
||||
{"DataPending", TryOn::kDataPending},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
|
||||
+318
@@ -19,6 +19,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Model::M2 {
|
||||
@@ -50,6 +51,19 @@ inline void *CreateModel(void *scene, const char *path) {
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_M2SCENE_CREATE_MODEL)(scene, path, 0);
|
||||
}
|
||||
|
||||
// A new instance of `source`'s model in `scene` without its item attachments
|
||||
// (helm, shoulders, held items), the way the portrait bake copies a unit:
|
||||
// CreateModelFromModel (FUN_M2SCENE_COPY_MODEL), then DetachAllItemModels
|
||||
// (FUN_DETACH_ALL_ITEM_MODELS). The caller owns one reference, or null.
|
||||
inline void *CopyWithoutItems(void *scene, void *source) {
|
||||
using Copy_t = void *(__thiscall *)(void *, void *, uint32_t);
|
||||
using DetachAll_t = void(__fastcall *)(void *);
|
||||
void *copy = reinterpret_cast<Copy_t>(Offsets::FUN_M2SCENE_COPY_MODEL)(scene, source, 0);
|
||||
if (copy != nullptr)
|
||||
reinterpret_cast<DetachAll_t>(Offsets::FUN_DETACH_ALL_ITEM_MODELS)(copy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
inline void PlayAnimation(void *model, int keyBone, uint32_t id, int variation, int offset,
|
||||
float speed, int blend, int primary) {
|
||||
using Fn = void(__thiscall *)(void *, int, uint32_t, int, int, float, int, int);
|
||||
@@ -57,6 +71,45 @@ inline void PlayAnimation(void *model, int keyBone, uint32_t id, int variation,
|
||||
offset, speed, blend, primary);
|
||||
}
|
||||
|
||||
// The key bone's sequence as it plays (FUN_M2MODEL_GET_KEY_BONE_ANIM_STATE).
|
||||
struct AnimState {
|
||||
uint32_t id;
|
||||
uint32_t variation;
|
||||
uint32_t position; // ms into the sequence
|
||||
float speed;
|
||||
uint32_t lo;
|
||||
uint32_t hi;
|
||||
uint32_t latch;
|
||||
};
|
||||
static_assert(sizeof(AnimState) == Offsets::M2_KEY_BONE_ANIM_STATE_SIZE);
|
||||
static_assert(offsetof(AnimState, id) == Offsets::OFF_M2_KEY_BONE_ANIM_STATE_ID);
|
||||
static_assert(offsetof(AnimState, variation) == Offsets::OFF_M2_KEY_BONE_ANIM_STATE_VARIATION);
|
||||
static_assert(offsetof(AnimState, position) == Offsets::OFF_M2_KEY_BONE_ANIM_STATE_POSITION);
|
||||
static_assert(offsetof(AnimState, speed) == Offsets::OFF_M2_KEY_BONE_ANIM_STATE_SPEED);
|
||||
static_assert(offsetof(AnimState, lo) == Offsets::OFF_M2_KEY_BONE_ANIM_STATE_LO);
|
||||
static_assert(offsetof(AnimState, hi) == Offsets::OFF_M2_KEY_BONE_ANIM_STATE_HI);
|
||||
static_assert(offsetof(AnimState, latch) == Offsets::OFF_M2_KEY_BONE_ANIM_STATE_LATCH);
|
||||
|
||||
// Waits for an unloaded instance.
|
||||
inline AnimState GetAnimState(void *model, int keyBone) {
|
||||
using Fn = void(__thiscall *)(void *, int, AnimState *);
|
||||
AnimState state;
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_GET_KEY_BONE_ANIM_STATE)(model, keyBone, &state);
|
||||
return state;
|
||||
}
|
||||
|
||||
// Where the key bone's animation stands, in ms into its sequence. Waits for
|
||||
// an unloaded instance.
|
||||
inline int GetAnimPosition(void *model, int keyBone) {
|
||||
return static_cast<int>(GetAnimState(model, keyBone).position);
|
||||
}
|
||||
|
||||
// Moves the key bone's animation to `ms` into its sequence, keeping its speed.
|
||||
inline void SetAnimPosition(void *model, int keyBone, int ms) {
|
||||
using Fn = void(__thiscall *)(void *, int, int);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_KEY_BONE_ANIM_TIME)(model, keyBone, ms);
|
||||
}
|
||||
|
||||
inline void SetAnimDoneCallback(void *model, const void *callback, uint32_t userLow,
|
||||
uint32_t userHigh) {
|
||||
using Fn = void(__thiscall *)(void *, const void *, uint32_t, uint32_t);
|
||||
@@ -69,6 +122,220 @@ inline void SetVisible(void *model, bool visible) {
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_VISIBLE)(model, visible ? 1 : 0);
|
||||
}
|
||||
|
||||
inline void SetAlpha(void *model, float alpha) {
|
||||
using Fn = void(__thiscall *)(void *, float);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_ALPHA)(model, alpha);
|
||||
}
|
||||
|
||||
// The tint the model is drawn with, {r, g, b}; white is none.
|
||||
inline void SetDiffuse(void *model, const float (&rgb)[3]) {
|
||||
using Fn = void(__thiscall *)(void *, const float *);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_DIFFUSE_COLOR)(model, rgb);
|
||||
}
|
||||
|
||||
// CM2Model::FindChild: the model hung at attachment `attachId`, or null.
|
||||
inline void *FindChild(void *model, int attachId) {
|
||||
using Fn = void *(__thiscall *)(void *, int);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_FIND_CHILD)(model, attachId);
|
||||
}
|
||||
|
||||
// The model this one is attached to, or null.
|
||||
inline void *Parent(const void *model) {
|
||||
return Game::Read<void *>(model, Offsets::OFF_M2MODEL_PARENT);
|
||||
}
|
||||
|
||||
// CM2Model::Attach: `child` on the parent's attachment `id`. The child leaves
|
||||
// the animate list (the parent animates it) and the parent takes a reference
|
||||
// on it.
|
||||
inline void Attach(void *child, void *parent, uint32_t id) {
|
||||
using Fn = void(__thiscall *)(void *, void *, uint32_t);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_ATTACH)(child, parent, id);
|
||||
}
|
||||
|
||||
// Whether `child` may hang on `parent`: two models, and the parent is not the
|
||||
// child or hung (at any depth) from it. Animate recurses through the parent
|
||||
// chain (EnsureAnimated, 0x00714000), so a cycle never ends.
|
||||
inline bool CanAttach(const void *child, const void *parent) {
|
||||
if (child == nullptr || parent == nullptr)
|
||||
return false;
|
||||
for (const void *m = parent; m != nullptr; m = Parent(m))
|
||||
if (m == child)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// CM2Model::HasAttachment. Waits for an unloaded model file.
|
||||
inline bool HasAttachment(void *model, uint32_t id) {
|
||||
using Fn = int(__thiscall *)(void *, uint32_t);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_HAS_ATTACHMENT)(model, id) != 0;
|
||||
}
|
||||
|
||||
// CM2Model::GetAttachmentPosition: the attachment's point in the space the
|
||||
// root model's base matrix places it in (a scene's actor coordinates). The
|
||||
// model must have the attachment (HasAttachment).
|
||||
inline void GetAttachmentPosition(void *model, uint32_t id, float (&out)[3]) {
|
||||
using Fn = float *(__thiscall *)(void *, float *, uint32_t);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_GET_ATTACHMENT_POSITION)(model, out, id);
|
||||
}
|
||||
|
||||
// CM2Model::GetAttachmentOffset: the attachment's position in model space,
|
||||
// not posed by any animation. The model must have the attachment
|
||||
// (HasAttachment). Waits for an unloaded model file.
|
||||
inline void GetAttachmentOffset(void *model, uint32_t id, float (&out)[3]) {
|
||||
using Fn = void(__thiscall *)(void *, float *, uint32_t);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_GET_ATTACHMENT_OFFSET)(model, out, id);
|
||||
}
|
||||
|
||||
// The animation-event callback (Offsets::FUN_M2MODEL_SET_EVENT_CALLBACK).
|
||||
inline void SetEventCallback(void *model, const void *callback, uint32_t userLow,
|
||||
uint32_t userHigh) {
|
||||
using Fn = void(__thiscall *)(void *, const void *, uint32_t, uint32_t);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_EVENT_CALLBACK)(model, callback, userLow,
|
||||
userHigh);
|
||||
}
|
||||
|
||||
// CM2Model::HasEvent. Waits for an unloaded model file.
|
||||
inline bool HasEvent(void *model, uint32_t tag) {
|
||||
using Fn = int(__thiscall *)(void *, uint32_t);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_HAS_EVENT)(model, tag) != 0;
|
||||
}
|
||||
|
||||
// CM2Model::GetEventPosition: the event's point in the scene, as
|
||||
// GetAttachmentPosition's. Waits for an unloaded model file.
|
||||
inline void GetEventPosition(void *model, uint32_t tag, float (&out)[3]) {
|
||||
using Fn = float *(__thiscall *)(void *, float *, uint32_t);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_GET_EVENT_POSITION)(model, out, tag);
|
||||
}
|
||||
|
||||
inline bool HasLiveParticles(void *model) {
|
||||
using Fn = int(__thiscall *)(void *);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_HAS_LIVE_PARTICLES)(model) != 0;
|
||||
}
|
||||
|
||||
// CM2Model::Detach, when the model is attached: off its parent, which drops
|
||||
// its reference on it. Detach itself drops that reference unconditionally,
|
||||
// so the world's Dismount (0x00607CE0) tests the parent first, as this does.
|
||||
inline void Detach(void *model) {
|
||||
using Fn = void(__fastcall *)(void *);
|
||||
if (Parent(model) != nullptr)
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_DETACH)(model);
|
||||
}
|
||||
|
||||
// The model's overall bounds {min xyz, max xyz}. Waits for an unloaded file.
|
||||
inline void GetBoundingBox(void *model, float (&box)[6]) {
|
||||
using Fn = void(__thiscall *)(void *, float *);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_GET_BOUNDING_BOX)(model, box);
|
||||
}
|
||||
|
||||
// The model's collision box {min xyz, max xyz}. Waits for an unloaded file.
|
||||
inline void GetCollisionBox(void *model, float (&box)[6]) {
|
||||
using Fn = void(__thiscall *)(void *, float *);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_GET_COLLISION_BOX)(model, box);
|
||||
}
|
||||
|
||||
// The model's bounding sphere {centre xyz, radius}. Waits for an unloaded file.
|
||||
inline void GetBoundingSphere(void *model, float (&sphere)[4]) {
|
||||
using Fn = void(__thiscall *)(void *, float *);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_GET_BOUNDING_SPHERE)(model, sphere);
|
||||
}
|
||||
|
||||
// CM2Model::HasAnimation: whether the model has a sequence for the
|
||||
// animation. Waits for an unloaded file.
|
||||
inline bool HasAnimation(void *model, uint32_t animation) {
|
||||
using Fn = bool(__thiscall *)(void *, uint32_t);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_HAS_ANIMATION)(model, animation);
|
||||
}
|
||||
|
||||
// An animation's sequence record (FUN_M2MODEL_GET_SEQUENCE_INFO), all zero
|
||||
// when the model has no sequence for it.
|
||||
struct SequenceInfo {
|
||||
uint32_t flags;
|
||||
uint32_t length; // ms
|
||||
float speed; // the move speed times the model's scale
|
||||
float bounds[6]; // {min xyz, max xyz}
|
||||
uint8_t rest[Offsets::M2_SEQUENCE_INFO_SIZE - 0x24];
|
||||
};
|
||||
static_assert(sizeof(SequenceInfo) == Offsets::M2_SEQUENCE_INFO_SIZE);
|
||||
static_assert(offsetof(SequenceInfo, length) == Offsets::OFF_M2_SEQUENCE_INFO_LENGTH);
|
||||
static_assert(offsetof(SequenceInfo, speed) == Offsets::OFF_M2_SEQUENCE_INFO_SPEED);
|
||||
static_assert(offsetof(SequenceInfo, bounds) == Offsets::OFF_M2_SEQUENCE_INFO_BOUNDS);
|
||||
|
||||
// Waits for an unloaded file.
|
||||
inline SequenceInfo GetSequenceInfo(void *model, uint32_t animation, uint32_t variation) {
|
||||
using Fn = void(__thiscall *)(void *, uint32_t, uint32_t, SequenceInfo *);
|
||||
SequenceInfo info{};
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_GET_SEQUENCE_INFO)(model, animation, variation, &info);
|
||||
return info;
|
||||
}
|
||||
|
||||
// The bounds of the animation's sequence, all zero when the model has none.
|
||||
// Waits for an unloaded file.
|
||||
inline void GetSequenceBounds(void *model, uint32_t animation, uint32_t variation,
|
||||
float (&box)[6]) {
|
||||
const SequenceInfo info = GetSequenceInfo(model, animation, variation);
|
||||
for (int i = 0; i < 6; ++i)
|
||||
box[i] = info.bounds[i];
|
||||
}
|
||||
|
||||
// The world pick's steps (Offsets::FUN_M2SCENE_RAY_HIT): ray hits on, the
|
||||
// candidates on the scene's hit list, then the ray.
|
||||
inline void EnableRayHits(void *scene) {
|
||||
using Fn = void(__fastcall *)(void *);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2SCENE_ENABLE_RAY_HITS)(scene);
|
||||
}
|
||||
|
||||
// Puts the model on its scene's hit list (skinned triangles), with `value`
|
||||
// for RayHit to return.
|
||||
inline void SetHitTest(void *model, uintptr_t value) {
|
||||
using Fn = void(__thiscall *)(void *, int, int, uintptr_t);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_HIT_TEST_MODE)(model, Offsets::M2_HIT_TEST_SKINNED,
|
||||
0, value);
|
||||
}
|
||||
|
||||
// The hit value of the nearest triangle on p0..p1 (view space), else 0.
|
||||
// Empties the hit list.
|
||||
inline uintptr_t RayHit(void *scene, const float (&p0)[3], const float (&p1)[3]) {
|
||||
using Fn = uintptr_t(__thiscall *)(void *, const float *, const float *, float *, int);
|
||||
float fraction = 1.0f;
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_M2SCENE_RAY_HIT)(scene, p0, p1, &fraction, 1);
|
||||
}
|
||||
|
||||
// The base matrix (row vectors: rows 0..2 the scaled axes, row 3 the
|
||||
// translation).
|
||||
inline void GetBaseTransform(void *model, float (&m)[16]) {
|
||||
using Fn = void(__thiscall *)(void *, float *);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_GET_BASE_TRANSFORM)(model, m);
|
||||
}
|
||||
|
||||
// CM2Model::SetBaseTransform: the base matrix as given.
|
||||
inline void SetBaseTransform(void *model, const float (&m)[16]) {
|
||||
using Fn = void(__thiscall *)(void *, const float *);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_BASE_TRANSFORM)(model, m);
|
||||
}
|
||||
|
||||
// CM2Model::SetBaseTransformTRS: identity, translated to `position`, rotated
|
||||
// by `angle` about `axis`, then each row scaled.
|
||||
inline void SetBaseTRS(void *model, const float (&position)[3], float angle, const float (&axis)[3],
|
||||
const float (&scale)[3]) {
|
||||
using Fn = void(__thiscall *)(void *, const float *, float, const float *, const float *);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_BASE_TRS)(model, position, angle, axis, scale);
|
||||
}
|
||||
|
||||
// The base matrix in place, for the engine's matrix calls after SetBaseTRS.
|
||||
inline float *BaseMatrix(void *model) { return Game::Ptr<float>(model, Offsets::OFF_M2MODEL_BASE_MATRIX); }
|
||||
|
||||
inline float GetAlpha(void *model) {
|
||||
using Fn = float(__thiscall *)(void *);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_GET_ALPHA)(model);
|
||||
}
|
||||
|
||||
inline void *FirstChild(const void *model) {
|
||||
return Game::Read<void *>(model, Offsets::OFF_MODEL_INSTANCE_CHILD_HEAD);
|
||||
}
|
||||
inline void *NextSibling(const void *model) {
|
||||
return Game::Read<void *>(model, Offsets::OFF_MODEL_INSTANCE_CHILD_NEXT);
|
||||
}
|
||||
|
||||
inline void SetAnimated(void *model, bool animated) {
|
||||
using Fn = void(__thiscall *)(void *, int);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_ANIMATED)(model, animated ? 1 : 0);
|
||||
@@ -79,11 +346,24 @@ inline void AnimateScene(void *scene, const float *cameraPos) {
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2SCENE_ANIMATE)(scene, cameraPos);
|
||||
}
|
||||
|
||||
// M2SceneDraw: pass 0 the opaque models, 1 the translucent ones.
|
||||
inline void DrawScene(void *scene, uint32_t pass) {
|
||||
using Fn = void(__thiscall *)(void *, uint32_t);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2SCENE_DRAW)(scene, pass);
|
||||
}
|
||||
|
||||
inline void AttachItemVisuals(void *model, uint32_t visualID) {
|
||||
using Fn = void(__fastcall *)(void *, uint32_t);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_ATTACH_ITEM_VISUALS)(model, visualID);
|
||||
}
|
||||
|
||||
// CM2Model::SetDrawCallback: a callback the model's draw runs as one of its
|
||||
// elements, in list 0 when `listZero`; a null callback removes it.
|
||||
inline void SetDrawCallback(void *model, const void *callback, void *userData, bool listZero) {
|
||||
using Fn = void(__thiscall *)(void *, const void *, void *, int);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_DRAW_CALLBACK)(model, callback, userData, listZero ? 1 : 0);
|
||||
}
|
||||
|
||||
inline void SetRenderCallback(void *model, const void *callback, void *userData) {
|
||||
using Fn = void(__thiscall *)(void *, const void *, void *);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_MODEL_INSTANCE_SET_RENDER_CB)(model, callback, userData);
|
||||
@@ -102,6 +382,44 @@ inline uint16_t LookupIndex(void *model, uintptr_t tableOffset, uint32_t id) {
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_M2_LOOKUP_INDEX)(id, Data(model) + tableOffset);
|
||||
}
|
||||
|
||||
// ---- a model light (M2LIGHT_SIZE bytes) -----------------------------------------
|
||||
|
||||
inline void InitLight(void *light) {
|
||||
reinterpret_cast<void(__fastcall *)(void *)>(Offsets::FUN_M2LIGHT_INITIALIZE)(light);
|
||||
}
|
||||
// 0 directional, 1 point.
|
||||
inline void SetLightType(void *light, int type) {
|
||||
reinterpret_cast<void(__thiscall *)(void *, int)>(Offsets::FUN_M2LIGHT_SET_TYPE)(light, type);
|
||||
}
|
||||
inline void SetLightEnabled(void *light, bool enabled) {
|
||||
reinterpret_cast<void(__thiscall *)(void *, int)>(Offsets::FUN_M2LIGHT_SET_ENABLED)(light, enabled ? 1 : 0);
|
||||
}
|
||||
inline void SetLightDirection(void *light, const float *v) {
|
||||
reinterpret_cast<void(__thiscall *)(void *, const float *)>(Offsets::FUN_M2LIGHT_SET_DIRECTION)(light, v);
|
||||
}
|
||||
inline void SetLightPosition(void *light, const float *v) {
|
||||
reinterpret_cast<void(__thiscall *)(void *, const float *)>(Offsets::FUN_M2LIGHT_SET_POSITION)(light, v);
|
||||
}
|
||||
|
||||
// ---- animation classes (Offsets::FUN_IS_*_ANIMATION) -----------------------------
|
||||
// The unit's tests of an AnimationData ID. The death test is __stdcall, the
|
||||
// others __fastcall.
|
||||
|
||||
inline bool IsDeathAnimation(int anim) {
|
||||
return reinterpret_cast<int(__stdcall *)(int)>(Offsets::FUN_IS_DEATH_ANIMATION)(anim) != 0;
|
||||
}
|
||||
|
||||
namespace Detail {
|
||||
inline bool AnimTest(uintptr_t fn, int anim) { return reinterpret_cast<int(__fastcall *)(int)>(fn)(anim) != 0; }
|
||||
} // namespace Detail
|
||||
|
||||
inline bool IsBowAnimation(int anim) { return Detail::AnimTest(Offsets::FUN_IS_BOW_ANIMATION, anim); }
|
||||
inline bool IsRifleAnimation(int anim) { return Detail::AnimTest(Offsets::FUN_IS_RIFLE_ANIMATION, anim); }
|
||||
inline bool IsThrownAnimation(int anim) { return Detail::AnimTest(Offsets::FUN_IS_THROWN_ANIMATION, anim); }
|
||||
inline bool IsEmoteAnimation(int anim) { return Detail::AnimTest(Offsets::FUN_IS_EMOTE_ANIMATION, anim); }
|
||||
inline bool IsSpellCastAnimation(int anim) { return Detail::AnimTest(Offsets::FUN_IS_SPELL_CAST_ANIMATION, anim); }
|
||||
inline bool IsMoveAnimation(int anim) { return Detail::AnimTest(Offsets::FUN_IS_MOVE_ANIMATION, anim); }
|
||||
|
||||
} // namespace Model::M2
|
||||
|
||||
namespace Model::Pane {
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_ModelInfo.GetModelSceneInfoByID(modelSceneID)` -> modelSceneType,
|
||||
// modelCameraIDs, modelActorsIDs, flags
|
||||
// `C_ModelInfo.GetModelSceneCameraInfoByID(modelSceneCameraID)` -> UIModelSceneCameraInfo
|
||||
// `C_ModelInfo.GetModelSceneActorInfoByID(modelActorID)` -> UIModelSceneActorInfo
|
||||
// `C_ModelInfo.GetModelSceneActorDisplayInfoByID(modelActorDisplayID)` -> UIModelSceneActorDisplayInfo
|
||||
//
|
||||
// Retail's getters read its UiModelScene DB2s; these read the rows embedded
|
||||
// in model/SceneData.h, shaped as retail returns them. An unknown ID returns
|
||||
// nothing, as retail's do (MayReturnNothing). The vector3 fields are
|
||||
// Vector3DMixin objects (CreateVector3D, Util/Vector3D.lua). From a retail
|
||||
// probe (docs/notes/model-scene.md § Data): the actor flags split into the
|
||||
// three useCenterForOrigin booleans, and a NormalizedScale of 0 is a nil
|
||||
// normalizeScaleAggressiveness; a display ID of 0 is likewise nil. Retail's
|
||||
// active-scene bookkeeping (Add/ClearActiveModelScene[Actor]) does nothing in
|
||||
// public clients and is the embedded addon's (Compat.lua).
|
||||
|
||||
#include "Game.h"
|
||||
#include "model/SceneData.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Model::ModelInfo {
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace Model::SceneData;
|
||||
|
||||
// Enum.UIModelSceneActorFlag.
|
||||
constexpr uint32_t kUseCenterForOriginX = 0x2;
|
||||
constexpr uint32_t kUseCenterForOriginY = 0x4;
|
||||
constexpr uint32_t kUseCenterForOriginZ = 0x8;
|
||||
|
||||
template <typename Row, size_t N>
|
||||
const Row *Find(const Row (&rows)[N], uint32_t id) {
|
||||
for (const Row &row : rows)
|
||||
if (row.id == id)
|
||||
return &row;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IdArg(void *L, uint32_t &id) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return false;
|
||||
id = static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
|
||||
return true;
|
||||
}
|
||||
|
||||
// `t[key] = CreateVector3D(v)` on the table at the top of the stack.
|
||||
void SetFieldVector3D(void *L, const char *key, const Vec3 &v) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::PushString(L, "CreateVector3D");
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
|
||||
Game::Lua::PushNumber(L, v.x);
|
||||
Game::Lua::PushNumber(L, v.y);
|
||||
Game::Lua::PushNumber(L, v.z);
|
||||
Game::Lua::Call(L, 3, 1);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// An array of the IDs of every row of `rows` in `sceneID`, in ID order.
|
||||
template <typename Row, size_t N>
|
||||
void PushSceneIDs(void *L, const Row (&rows)[N], uint32_t sceneID) {
|
||||
Game::Lua::NewTable(L);
|
||||
int n = 0;
|
||||
for (const Row &row : rows) {
|
||||
if (row.sceneID != sceneID)
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, row.id);
|
||||
Game::Lua::RawSetI(L, -2, ++n);
|
||||
}
|
||||
}
|
||||
|
||||
int __fastcall Script_GetModelSceneInfoByID(void *L) {
|
||||
uint32_t id = 0;
|
||||
const SceneRow *scene = IdArg(L, id) ? Find(kScenes, id) : nullptr;
|
||||
if (scene == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, scene->uiSystemType);
|
||||
PushSceneIDs(L, kCameras, id);
|
||||
PushSceneIDs(L, kActors, id);
|
||||
Game::Lua::PushNumber(L, scene->flags);
|
||||
return 4;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetModelSceneCameraInfoByID(void *L) {
|
||||
uint32_t id = 0;
|
||||
const CameraRow *camera = IdArg(L, id) ? Find(kCameras, id) : nullptr;
|
||||
if (camera == nullptr)
|
||||
return 0;
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "modelSceneCameraID", camera->id);
|
||||
Game::Lua::SetFieldString(L, "scriptTag", camera->scriptTag);
|
||||
Game::Lua::SetFieldString(L, "cameraType", "OrbitCamera");
|
||||
SetFieldVector3D(L, "target", camera->target);
|
||||
Game::Lua::SetFieldNumber(L, "yaw", camera->yaw);
|
||||
Game::Lua::SetFieldNumber(L, "pitch", camera->pitch);
|
||||
Game::Lua::SetFieldNumber(L, "roll", camera->roll);
|
||||
Game::Lua::SetFieldNumber(L, "zoomDistance", camera->zoomDistance);
|
||||
Game::Lua::SetFieldNumber(L, "minZoomDistance", camera->minZoomDistance);
|
||||
Game::Lua::SetFieldNumber(L, "maxZoomDistance", camera->maxZoomDistance);
|
||||
SetFieldVector3D(L, "zoomedTargetOffset", camera->zoomedTargetOffset);
|
||||
Game::Lua::SetFieldNumber(L, "zoomedYawOffset", camera->zoomedYawOffset);
|
||||
Game::Lua::SetFieldNumber(L, "zoomedPitchOffset", camera->zoomedPitchOffset);
|
||||
Game::Lua::SetFieldNumber(L, "zoomedRollOffset", camera->zoomedRollOffset);
|
||||
Game::Lua::SetFieldNumber(L, "flags", camera->flags);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetModelSceneActorInfoByID(void *L) {
|
||||
uint32_t id = 0;
|
||||
const ActorRow *actor = IdArg(L, id) ? Find(kActors, id) : nullptr;
|
||||
if (actor == nullptr)
|
||||
return 0;
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "modelActorID", actor->id);
|
||||
Game::Lua::SetFieldString(L, "scriptTag", actor->scriptTag);
|
||||
SetFieldVector3D(L, "position", actor->position);
|
||||
Game::Lua::SetFieldNumber(L, "yaw", actor->yaw);
|
||||
Game::Lua::SetFieldNumber(L, "pitch", actor->pitch);
|
||||
Game::Lua::SetFieldNumber(L, "roll", actor->roll);
|
||||
if (actor->normalizedScale != 0.0f)
|
||||
Game::Lua::SetFieldNumber(L, "normalizeScaleAggressiveness", actor->normalizedScale);
|
||||
Game::Lua::SetFieldBool(L, "useCenterForOriginX", (actor->flags & kUseCenterForOriginX) != 0);
|
||||
Game::Lua::SetFieldBool(L, "useCenterForOriginY", (actor->flags & kUseCenterForOriginY) != 0);
|
||||
Game::Lua::SetFieldBool(L, "useCenterForOriginZ", (actor->flags & kUseCenterForOriginZ) != 0);
|
||||
if (actor->displayID != 0)
|
||||
Game::Lua::SetFieldNumber(L, "modelActorDisplayID", actor->displayID);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetModelSceneActorDisplayInfoByID(void *L) {
|
||||
uint32_t id = 0;
|
||||
const ActorDisplayRow *display = IdArg(L, id) ? Find(kActorDisplays, id) : nullptr;
|
||||
if (display == nullptr)
|
||||
return 0;
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "animation", display->animation);
|
||||
Game::Lua::SetFieldNumber(L, "animationVariation", display->animationVariation);
|
||||
Game::Lua::SetFieldNumber(L, "animSpeed", display->animSpeed);
|
||||
if (display->animKitID != 0)
|
||||
Game::Lua::SetFieldNumber(L, "animationKitID", display->animKitID);
|
||||
if (display->spellVisualKitID != 0)
|
||||
Game::Lua::SetFieldNumber(L, "spellVisualKitID", display->spellVisualKitID);
|
||||
Game::Lua::SetFieldNumber(L, "alpha", display->alpha);
|
||||
Game::Lua::SetFieldNumber(L, "scale", display->scale);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kActorDisplayInfoFields[] = {
|
||||
Game::Doc::Req("animation", "number"),
|
||||
Game::Doc::Req("animationVariation", "number"),
|
||||
Game::Doc::Req("animSpeed", "number"),
|
||||
Game::Doc::Opt("animationKitID", "number"),
|
||||
Game::Doc::Opt("spellVisualKitID", "number"),
|
||||
Game::Doc::Req("alpha", "number"),
|
||||
Game::Doc::Req("scale", "number"),
|
||||
};
|
||||
const Game::Doc::Structure kActorDisplayInfo{"UIModelSceneActorDisplayInfo", "ModelInfo",
|
||||
kActorDisplayInfoFields};
|
||||
|
||||
const Game::Doc::Field kActorInfoFields[] = {
|
||||
Game::Doc::Req("modelActorID", "number"),
|
||||
Game::Doc::Req("scriptTag", "cstring"),
|
||||
Game::Doc::Req("position", "vector3"),
|
||||
Game::Doc::Req("yaw", "number"),
|
||||
Game::Doc::Req("pitch", "number"),
|
||||
Game::Doc::Req("roll", "number"),
|
||||
Game::Doc::Opt("normalizeScaleAggressiveness", "number"),
|
||||
Game::Doc::Req("useCenterForOriginX", "bool"),
|
||||
Game::Doc::Req("useCenterForOriginY", "bool"),
|
||||
Game::Doc::Req("useCenterForOriginZ", "bool"),
|
||||
Game::Doc::Opt("modelActorDisplayID", "number"),
|
||||
};
|
||||
const Game::Doc::Structure kActorInfo{"UIModelSceneActorInfo", "ModelInfo", kActorInfoFields};
|
||||
|
||||
const Game::Doc::Field kCameraInfoFields[] = {
|
||||
Game::Doc::Req("modelSceneCameraID", "number"),
|
||||
Game::Doc::Req("scriptTag", "cstring"),
|
||||
Game::Doc::Req("cameraType", "cstring"),
|
||||
Game::Doc::Req("target", "vector3"),
|
||||
Game::Doc::Req("yaw", "number"),
|
||||
Game::Doc::Req("pitch", "number"),
|
||||
Game::Doc::Req("roll", "number"),
|
||||
Game::Doc::Req("zoomDistance", "number"),
|
||||
Game::Doc::Req("minZoomDistance", "number"),
|
||||
Game::Doc::Req("maxZoomDistance", "number"),
|
||||
Game::Doc::Req("zoomedTargetOffset", "vector3"),
|
||||
Game::Doc::Req("zoomedYawOffset", "number"),
|
||||
Game::Doc::Req("zoomedPitchOffset", "number"),
|
||||
Game::Doc::Req("zoomedRollOffset", "number"),
|
||||
Game::Doc::Req("flags", "ModelSceneSetting"),
|
||||
};
|
||||
const Game::Doc::Structure kCameraInfo{"UIModelSceneCameraInfo", "ModelInfo", kCameraInfoFields};
|
||||
|
||||
const Game::Doc::Field kSceneIDArg[] = {Game::Doc::Req("modelSceneID", "number")};
|
||||
const Game::Doc::Field kSceneInfoRets[] = {
|
||||
Game::Doc::Req("modelSceneType", "ModelSceneType"),
|
||||
Game::Doc::Req("modelCameraIDs", "table"),
|
||||
Game::Doc::Req("modelActorsIDs", "table"),
|
||||
Game::Doc::Req("flags", "number"),
|
||||
};
|
||||
const Game::Doc::Function kGetModelSceneInfoByID{
|
||||
"Returns a model scene's type and the IDs of its cameras and actors.", kSceneIDArg,
|
||||
kSceneInfoRets};
|
||||
|
||||
const Game::Doc::Field kCameraIDArg[] = {Game::Doc::Req("modelSceneCameraID", "number")};
|
||||
const Game::Doc::Field kCameraInfoRet[] = {
|
||||
Game::Doc::Req("modelSceneCameraInfo", "UIModelSceneCameraInfo")};
|
||||
const Game::Doc::Function kGetModelSceneCameraInfoByID{"Returns a model scene camera's settings.",
|
||||
kCameraIDArg, kCameraInfoRet};
|
||||
|
||||
const Game::Doc::Field kActorIDArg[] = {Game::Doc::Req("modelActorID", "number")};
|
||||
const Game::Doc::Field kActorInfoRet[] = {Game::Doc::Req("actorInfo", "UIModelSceneActorInfo")};
|
||||
const Game::Doc::Function kGetModelSceneActorInfoByID{
|
||||
"Returns where a model scene actor stands and how it is scaled.", kActorIDArg,
|
||||
kActorInfoRet};
|
||||
|
||||
const Game::Doc::Field kActorDisplayIDArg[] = {Game::Doc::Req("modelActorDisplayID", "number")};
|
||||
const Game::Doc::Field kActorDisplayInfoRet[] = {
|
||||
Game::Doc::Req("actorDisplayInfo", "UIModelSceneActorDisplayInfo")};
|
||||
const Game::Doc::Function kGetModelSceneActorDisplayInfoByID{
|
||||
"Returns the animation, alpha and scale a model scene actor starts with.",
|
||||
kActorDisplayIDArg, kActorDisplayInfoRet};
|
||||
|
||||
// Enum.ModelSceneSetting: the camera rows' flags, which OrbitCameraMixin
|
||||
// tests on every update (ShouldAlignLightToOrbitDelta).
|
||||
const Game::Lua::EnumIntegerEntry kModelSceneSetting[] = {
|
||||
{"AlignLightToOrbitDelta", 1},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ModelSceneSetting", kModelSceneSetting, "UIModelInfo");
|
||||
Game::Lua::RegisterTableFunction("C_ModelInfo", "GetModelSceneInfoByID",
|
||||
&Script_GetModelSceneInfoByID, &kGetModelSceneInfoByID);
|
||||
Game::Lua::RegisterTableFunction("C_ModelInfo", "GetModelSceneCameraInfoByID",
|
||||
&Script_GetModelSceneCameraInfoByID,
|
||||
&kGetModelSceneCameraInfoByID);
|
||||
Game::Lua::RegisterTableFunction("C_ModelInfo", "GetModelSceneActorInfoByID",
|
||||
&Script_GetModelSceneActorInfoByID,
|
||||
&kGetModelSceneActorInfoByID);
|
||||
Game::Lua::RegisterTableFunction("C_ModelInfo", "GetModelSceneActorDisplayInfoByID",
|
||||
&Script_GetModelSceneActorDisplayInfoByID,
|
||||
&kGetModelSceneActorDisplayInfoByID);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Model::ModelInfo
|
||||
@@ -0,0 +1,488 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The outfit calls of a character model copy: DressUpModel's own steps
|
||||
// (SetModelInstance 0x00504350, ClearModel 0x00504490, 4.3.4's per-slot
|
||||
// UndressSlot) and the unit's AttachHandItem step, on whatever holds the
|
||||
// component and the lanes. benilla ui/dressup and charactermodel/equipment.
|
||||
//
|
||||
// The slot map is 4.3.4's UndressSlot map, from inventory slot to component
|
||||
// slot.
|
||||
//
|
||||
// What a slot wears (Worn) is read from the unit's visible items, the item
|
||||
// IDs the engine keeps per equipment slot for every player, and kept only
|
||||
// while the model shows it: an armor slot while the component holds a record
|
||||
// for it (so a hidden helm or cloak wears nothing), a hand while its lane
|
||||
// holds an item. A weapon's enchant is the one behind the glow the engine
|
||||
// shows, picked as GetItemDescVisual (0x0062EC70) picks it.
|
||||
//
|
||||
// TryOn is DressUpModel::TryOnItem's arms (0x00504540, benilla ui/dressup)
|
||||
// for a holder that is not a pane, the ModelScene actor: the pane runs the
|
||||
// engine's worker itself (model/DressUp.cpp). Two differences:
|
||||
// - An evicted lane is zeroed, as Undress zeroes one. The worker leaves its
|
||||
// record, which only makes a later try-on detach it again.
|
||||
// - A tabard keeps no guild emblem. The worker's emblem step reads the
|
||||
// pane's player.
|
||||
|
||||
#include "model/Outfit.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "model/DisplayInfo.h"
|
||||
#include "model/ItemDisplay.h"
|
||||
#include "model/M2.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <iterator>
|
||||
|
||||
namespace Model::Outfit {
|
||||
namespace {
|
||||
|
||||
using DetachHeldItem_t = void(__fastcall *)(void *model, uint32_t bodySlot, uint32_t sheatheType,
|
||||
bool shield);
|
||||
using AttachItemModel_t = int(__fastcall *)(void *model, const void *record, uint32_t bodySlot,
|
||||
uint32_t sheatheType, bool sheathed, bool shield,
|
||||
bool ranged);
|
||||
using GetHeldItemDisplay_t = const void *(__thiscall *)(void *unit, uint32_t hand);
|
||||
using GetHeldItemLane_t = const HeldLane *(__thiscall *)(void *unit, uint32_t hand, int one);
|
||||
using GetVisibleItem_t = const void *(__thiscall *)(void *player, uint32_t slot);
|
||||
using GetDescVisual_t = int(__thiscall *)(const void *visibleItem);
|
||||
using CanHoldBoth_t = bool(__fastcall *)(uint32_t mainType, uint32_t offType, bool dualWield);
|
||||
|
||||
constexpr uint32_t kBodySlots[2] = {kBodySlotMainHand, kBodySlotOffHand};
|
||||
|
||||
// The component slots ClearModel clears (0..11).
|
||||
constexpr uint32_t kComponentSlots = 12;
|
||||
|
||||
// The inventory slot of each component slot 0..10 (4.3.4's UndressSlot map,
|
||||
// inverted); slot 11 has no inventory slot.
|
||||
constexpr int kInvSlotOfComponent[] = {1, 3, 4, 5, 6, 7, 8, 9, 10, 19, 15};
|
||||
|
||||
// The ItemDisplayInfo record the component shows in `slot`, or null.
|
||||
const void *ComponentRecord(void *component, uint32_t slot) {
|
||||
return Game::Read<const void *>(component, Offsets::OFF_CHARCOMP_ITEM_RECORDS + slot * 4);
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
Fn VirtualOf(void *object, uintptr_t slot) {
|
||||
return Game::Read<Fn>(Game::Read<const void *>(object, 0), slot);
|
||||
}
|
||||
|
||||
// DetachHeldItemSlot on lane `i`'s item.
|
||||
void DetachLane(void *model, int i, const HeldLane &lane) {
|
||||
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(
|
||||
model, kBodySlots[i], lane.sheatheType, lane.inventoryType == Offsets::INVTYPE_SHIELD);
|
||||
}
|
||||
|
||||
// ClearModel's lane step: detach the held item and zero the lane.
|
||||
void ClearLane(void *model, Lanes &lanes, int i) {
|
||||
if (lanes[i].itemClass == 0)
|
||||
return;
|
||||
DetachLane(model, i, lanes[i]);
|
||||
lanes[i] = HeldLane{};
|
||||
}
|
||||
|
||||
// TryOnItem's hand for a held type: the named one, else a second one-hander
|
||||
// in the off hand of a dual wielder, and each type's own hand.
|
||||
int HandFor(uint32_t type, const Lanes &lanes, const HeldTryOn &held) {
|
||||
if (held.bodySlot != 0)
|
||||
return LaneIndex(held.bodySlot);
|
||||
switch (type) {
|
||||
case Offsets::INVTYPE_WEAPON:
|
||||
return lanes[0].itemClass != 0 &&
|
||||
(lanes[0].inventoryType == Offsets::INVTYPE_WEAPON ||
|
||||
lanes[0].inventoryType == Offsets::INVTYPE_WEAPONMAINHAND) &&
|
||||
held.dualWield
|
||||
? 1
|
||||
: 0;
|
||||
case Offsets::INVTYPE_SHIELD:
|
||||
case Offsets::INVTYPE_RANGED:
|
||||
case Offsets::INVTYPE_WEAPONOFFHAND:
|
||||
case Offsets::INVTYPE_HOLDABLE:
|
||||
return 1;
|
||||
default: // two-hand, main-hand, thrown and right-hand ranged
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// The unit's AttachHandItem step on lane `i`: detach it, then hang it in the
|
||||
// hand or the sheath, or leave it off.
|
||||
void HangLane(void *model, int i, const HeldLane &lane, const Weapons &weapons, bool sheathed,
|
||||
bool hide) {
|
||||
if (lane.itemClass == 0 || weapons.display[i] == nullptr)
|
||||
return;
|
||||
DetachLane(model, i, lane);
|
||||
if (hide)
|
||||
return;
|
||||
const int attachId = reinterpret_cast<AttachItemModel_t>(Offsets::FUN_ATTACH_ITEM_MODEL)(
|
||||
model, weapons.display[i], kBodySlots[i], lane.sheatheType, sheathed,
|
||||
lane.inventoryType == Offsets::INVTYPE_SHIELD,
|
||||
lane.inventoryType == Offsets::INVTYPE_RANGEDRIGHT);
|
||||
HangVisual(model, attachId, weapons.visual[i]);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int ComponentSlotOf(int invSlot) {
|
||||
for (int slot = 0; slot < static_cast<int>(std::size(kInvSlotOfComponent)); ++slot) {
|
||||
if (kInvSlotOfComponent[slot] == invSlot)
|
||||
return slot;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int InvSlotOfComponent(uint32_t componentSlot) {
|
||||
return componentSlot < std::size(kInvSlotOfComponent) ? kInvSlotOfComponent[componentSlot] : 0;
|
||||
}
|
||||
|
||||
bool SlotAllowed(int invSlot, bool armorReady) {
|
||||
if (invSlot == Offsets::INVSLOT_MAINHAND || invSlot == Offsets::INVSLOT_OFFHAND)
|
||||
return true;
|
||||
return ComponentSlotOf(invSlot) >= 0 && armorReady;
|
||||
}
|
||||
|
||||
void ReadLanes(void *unit, Lanes &lanes) {
|
||||
const auto get = VirtualOf<GetHeldItemLane_t>(unit, Offsets::VTBL_UNIT_GET_HELD_ITEM_LANE);
|
||||
for (uint32_t hand = 0; hand < 2; ++hand) {
|
||||
const HeldLane *lane = get(unit, hand, 1);
|
||||
lanes[hand] = lane != nullptr ? *lane : HeldLane{};
|
||||
}
|
||||
}
|
||||
|
||||
void ReadWeapons(uint64_t guid, Weapons &weapons) {
|
||||
weapons = Weapons{};
|
||||
if (void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid)) {
|
||||
const auto get =
|
||||
VirtualOf<GetHeldItemDisplay_t>(unit, Offsets::VTBL_UNIT_GET_HELD_ITEM_DISPLAY);
|
||||
weapons.display[0] = get(unit, 0);
|
||||
weapons.display[1] = get(unit, 1);
|
||||
}
|
||||
// AttachHandEnchant's glow (0x005EED00), for players only: the visible
|
||||
// item in the lane's slot (0xF / 0x10, the same numbers as the body
|
||||
// slots), and its visual unless the display hangs its own (-1).
|
||||
void *player = Object::ByGuid(Offsets::TYPEMASK_PLAYER, guid);
|
||||
if (player == nullptr)
|
||||
return;
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const void *item =
|
||||
reinterpret_cast<GetVisibleItem_t>(Offsets::FUN_UNIT_GET_VISIBLE_ITEM)(player,
|
||||
kBodySlots[i]);
|
||||
if (item == nullptr || Game::Read<uint32_t>(item, Offsets::OFF_VISIBLE_ITEM_ITEM_ID) == 0)
|
||||
continue;
|
||||
const int visual =
|
||||
reinterpret_cast<GetDescVisual_t>(Offsets::FUN_VISIBLE_ITEM_GET_DESC_VISUAL)(item);
|
||||
weapons.visual[i] = visual > 0 ? static_cast<uint32_t>(visual) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
void *CloneComponent(void *model, void *unit) {
|
||||
void *source = Game::Read<void *>(unit, Offsets::OFF_CGUNIT_CHARACTER_COMPONENT);
|
||||
if (source == nullptr)
|
||||
return nullptr;
|
||||
void *component = DisplayInfo::NewDresser();
|
||||
if (component == nullptr)
|
||||
return nullptr;
|
||||
M2::AddRef(model); // the component's reference
|
||||
DisplayInfo::CloneDresser(component, model, source);
|
||||
return component;
|
||||
}
|
||||
|
||||
void HangVisual(void *model, int attachId, uint32_t visual) {
|
||||
if (visual == 0 || attachId < 0)
|
||||
return;
|
||||
if (void *item = M2::FindChild(model, attachId))
|
||||
M2::AttachItemVisuals(item, visual);
|
||||
}
|
||||
|
||||
void HangWeapons(void *model, const Lanes &lanes, const Weapons &weapons, bool sheathed,
|
||||
bool hide) {
|
||||
if (model == nullptr)
|
||||
return;
|
||||
for (int i = 0; i < 2; ++i)
|
||||
HangLane(model, i, lanes[i], weapons, sheathed, hide);
|
||||
}
|
||||
|
||||
void SetSheatheType(void *model, Lanes &lanes, int i, const Weapons &weapons,
|
||||
uint8_t sheatheType, bool sheathed, bool hide) {
|
||||
if (model == nullptr || lanes[i].itemClass == 0)
|
||||
return;
|
||||
DetachLane(model, i, lanes[i]);
|
||||
lanes[i].sheatheType = sheatheType;
|
||||
HangLane(model, i, lanes[i], weapons, sheathed, hide);
|
||||
}
|
||||
|
||||
uint8_t SheatheTypeFor(uint8_t own, uint32_t category) {
|
||||
switch (category) {
|
||||
case kSheatheBack:
|
||||
if (own == Offsets::ITEM_SHEATH_HIP)
|
||||
return static_cast<uint8_t>(Offsets::ITEM_SHEATH_BACK);
|
||||
return own;
|
||||
case kSheatheSide:
|
||||
if (own == Offsets::ITEM_SHEATH_BACK || own == Offsets::ITEM_SHEATH_LARGE_BACK)
|
||||
return static_cast<uint8_t>(Offsets::ITEM_SHEATH_HIP);
|
||||
return own;
|
||||
case kSheatheHide:
|
||||
return static_cast<uint8_t>(Offsets::ITEM_SHEATH_NONE);
|
||||
default:
|
||||
return own;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t EnchantVisual(const void *display, uint32_t enchantID) {
|
||||
if (display == nullptr ||
|
||||
Model::ItemDisplay::OwnVisual(static_cast<const uint8_t *>(display)) != 0)
|
||||
return 0;
|
||||
const uint8_t *enchant = DBC::Record(Offsets::VAR_SPELLITEMENCHANT_RECORDS,
|
||||
Offsets::VAR_SPELLITEMENCHANT_COUNT, enchantID);
|
||||
return enchant != nullptr
|
||||
? Game::Read<uint32_t>(enchant, Offsets::OFF_SPELLITEMENCHANT_ITEM_VISUAL)
|
||||
: 0;
|
||||
}
|
||||
|
||||
Arm ArmOf(uint32_t inventoryType, uint32_t *componentSlot) {
|
||||
uint32_t slot = 0;
|
||||
switch (inventoryType) {
|
||||
case Offsets::INVTYPE_HEAD: slot = 0; break;
|
||||
case Offsets::INVTYPE_SHOULDER: slot = 1; break;
|
||||
case Offsets::INVTYPE_BODY: slot = 2; break;
|
||||
case Offsets::INVTYPE_CHEST:
|
||||
case Offsets::INVTYPE_ROBE: slot = 3; break;
|
||||
case Offsets::INVTYPE_WAIST: slot = 4; break;
|
||||
case Offsets::INVTYPE_LEGS: slot = 5; break;
|
||||
case Offsets::INVTYPE_FEET: slot = 6; break;
|
||||
case Offsets::INVTYPE_WRIST: slot = 7; break;
|
||||
case Offsets::INVTYPE_HAND: slot = 8; break;
|
||||
case Offsets::INVTYPE_TABARD: slot = 9; break;
|
||||
case Offsets::INVTYPE_CLOAK: slot = 10; break;
|
||||
case Offsets::INVTYPE_WEAPON:
|
||||
case Offsets::INVTYPE_SHIELD:
|
||||
case Offsets::INVTYPE_RANGED:
|
||||
case Offsets::INVTYPE_2HWEAPON:
|
||||
case Offsets::INVTYPE_WEAPONMAINHAND:
|
||||
case Offsets::INVTYPE_WEAPONOFFHAND:
|
||||
case Offsets::INVTYPE_HOLDABLE:
|
||||
case Offsets::INVTYPE_THROWN:
|
||||
case Offsets::INVTYPE_RANGEDRIGHT:
|
||||
return Arm::Held;
|
||||
case Offsets::INVTYPE_NECK:
|
||||
case Offsets::INVTYPE_FINGER:
|
||||
case Offsets::INVTYPE_TRINKET:
|
||||
case Offsets::INVTYPE_RELIC:
|
||||
return Arm::Nothing;
|
||||
default:
|
||||
return Arm::NotEquippable;
|
||||
}
|
||||
if (componentSlot != nullptr)
|
||||
*componentSlot = slot;
|
||||
return Arm::Armor;
|
||||
}
|
||||
|
||||
bool CanDualWield(uint64_t guid) {
|
||||
return guid != 0 && guid == Unit::Identity::PlayerGuid() &&
|
||||
reinterpret_cast<int(__cdecl *)()>(Offsets::FUN_PLAYER_CAN_DUAL_WIELD)() != 0;
|
||||
}
|
||||
|
||||
int TryOn(void *model, void *component, bool armorReady, Lanes &lanes, Weapons &weapons,
|
||||
const uint8_t *record, const HeldTryOn &held) {
|
||||
const auto displayID = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
if (model == nullptr || displayID == 0)
|
||||
return 0;
|
||||
const auto type = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
|
||||
uint32_t componentSlot = 0;
|
||||
switch (ArmOf(type, &componentSlot)) {
|
||||
case Arm::Armor:
|
||||
if (component == nullptr || !armorReady)
|
||||
return 0;
|
||||
DisplayInfo::UnequipDresserSlot(component, componentSlot);
|
||||
DisplayInfo::SetDresserItem(component, componentSlot, displayID);
|
||||
return kInvSlotOfComponent[componentSlot];
|
||||
case Arm::Held:
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
const int lane = HandFor(type, lanes, held);
|
||||
const int other = 1 - lane;
|
||||
if (lanes[other].itemClass != 0) {
|
||||
const uint32_t mainType = lane == 0 ? type : lanes[0].inventoryType;
|
||||
const uint32_t offType = lane == 0 ? lanes[1].inventoryType : type;
|
||||
if (!reinterpret_cast<CanHoldBoth_t>(Offsets::FUN_DRESSUPMODEL_CAN_HOLD_BOTH)(
|
||||
mainType, offType, held.dualWield)) {
|
||||
ClearLane(model, lanes, other);
|
||||
weapons.display[other] = nullptr;
|
||||
weapons.visual[other] = 0;
|
||||
}
|
||||
}
|
||||
ClearLane(model, lanes, lane);
|
||||
HeldLane &target = lanes[lane];
|
||||
target = LaneOf(record);
|
||||
const uint8_t *display = DBC::Record(Offsets::VAR_ITEMDISPLAYINFO_RECORDS,
|
||||
Offsets::VAR_ITEMDISPLAYINFO_COUNT, displayID);
|
||||
weapons.display[lane] = display;
|
||||
weapons.visual[lane] = EnchantVisual(display, held.enchant);
|
||||
const int invSlot = lane == 0 ? Offsets::INVSLOT_MAINHAND : Offsets::INVSLOT_OFFHAND;
|
||||
if (display == nullptr || held.hide)
|
||||
return invSlot;
|
||||
const int attachId = reinterpret_cast<AttachItemModel_t>(Offsets::FUN_ATTACH_ITEM_MODEL)(
|
||||
model, display, kBodySlots[lane], target.sheatheType, held.sheathed,
|
||||
type == Offsets::INVTYPE_SHIELD, type == Offsets::INVTYPE_RANGEDRIGHT);
|
||||
HangVisual(model, attachId, weapons.visual[lane]);
|
||||
return invSlot;
|
||||
}
|
||||
|
||||
void UndressSlot(void *model, void *component, Lanes &lanes, int invSlot) {
|
||||
if (model == nullptr || component == nullptr)
|
||||
return;
|
||||
if (invSlot == Offsets::INVSLOT_MAINHAND) {
|
||||
ClearLane(model, lanes, 0);
|
||||
} else if (invSlot == Offsets::INVSLOT_OFFHAND) {
|
||||
ClearLane(model, lanes, 1);
|
||||
} else {
|
||||
const int compSlot = ComponentSlotOf(invSlot);
|
||||
if (compSlot >= 0)
|
||||
DisplayInfo::UnequipDresserSlot(component, static_cast<uint32_t>(compSlot));
|
||||
}
|
||||
}
|
||||
|
||||
void DressSlot(void *model, void *component, bool armorReady, Lanes &lanes, Weapons &weapons,
|
||||
void *unit, uint64_t guid, int invSlot, bool sheathed, bool hide) {
|
||||
if (model == nullptr || unit == nullptr)
|
||||
return;
|
||||
if (invSlot == Offsets::INVSLOT_MAINHAND || invSlot == Offsets::INVSLOT_OFFHAND) {
|
||||
const int i = invSlot == Offsets::INVSLOT_OFFHAND ? 1 : 0;
|
||||
Lanes worn;
|
||||
Weapons wornWeapons;
|
||||
ReadLanes(unit, worn);
|
||||
ReadWeapons(guid, wornWeapons);
|
||||
ClearLane(model, lanes, i);
|
||||
lanes[i] = worn[i];
|
||||
weapons.display[i] = wornWeapons.display[i];
|
||||
weapons.visual[i] = wornWeapons.visual[i];
|
||||
HangLane(model, i, lanes[i], weapons, sheathed, hide);
|
||||
return;
|
||||
}
|
||||
const int compSlot = ComponentSlotOf(invSlot);
|
||||
void *source = Game::Read<void *>(unit, Offsets::OFF_CGUNIT_CHARACTER_COMPONENT);
|
||||
if (compSlot < 0 || component == nullptr || source == nullptr || !armorReady)
|
||||
return;
|
||||
const auto slot = static_cast<uint32_t>(compSlot);
|
||||
const void *record = ComponentRecord(source, slot);
|
||||
DisplayInfo::UnequipDresserSlot(component, slot);
|
||||
if (record != nullptr)
|
||||
DisplayInfo::SetDresserItem(component, slot, Game::Read<uint32_t>(record, 0));
|
||||
}
|
||||
|
||||
void ReadWorn(void *unit, uint64_t guid, const Weapons &weapons, Worn &worn) {
|
||||
for (Appearance &slot : worn)
|
||||
slot = Appearance{};
|
||||
// Only a player has visible items: a creature's table is not initialized
|
||||
// (FUN_UNIT_GET_VISIBLE_ITEM's hazard).
|
||||
void *player = Object::ByGuid(Offsets::TYPEMASK_PLAYER, guid);
|
||||
if (unit == nullptr || player == nullptr)
|
||||
return;
|
||||
const auto visibleItem = reinterpret_cast<GetVisibleItem_t>(Offsets::FUN_UNIT_GET_VISIBLE_ITEM);
|
||||
const auto itemOf = [&](int invSlot) -> const uint8_t * {
|
||||
const void *item = visibleItem(player, static_cast<uint32_t>(invSlot - 1));
|
||||
return static_cast<const uint8_t *>(item);
|
||||
};
|
||||
void *component = Game::Read<void *>(unit, Offsets::OFF_CGUNIT_CHARACTER_COMPONENT);
|
||||
if (component != nullptr) {
|
||||
for (uint32_t slot = 0; slot < std::size(kInvSlotOfComponent); ++slot) {
|
||||
const int invSlot = kInvSlotOfComponent[slot];
|
||||
const uint8_t *item = itemOf(invSlot);
|
||||
if (item != nullptr && ComponentRecord(component, slot) != nullptr)
|
||||
worn[invSlot - 1].item = Game::Read<uint32_t>(item, Offsets::OFF_VISIBLE_ITEM_ITEM_ID);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const int invSlot = i == 0 ? Offsets::INVSLOT_MAINHAND : Offsets::INVSLOT_OFFHAND;
|
||||
const uint8_t *item = itemOf(invSlot);
|
||||
if (item == nullptr || weapons.display[i] == nullptr)
|
||||
continue;
|
||||
Appearance &held = worn[invSlot - 1];
|
||||
held.item = Game::Read<uint32_t>(item, Offsets::OFF_VISIBLE_ITEM_ITEM_ID);
|
||||
if (weapons.visual[i] == 0)
|
||||
continue;
|
||||
// GetItemDescVisual's walk: the first enchant with an ItemVisual.
|
||||
for (uint32_t e = 0; e < Offsets::VISIBLE_ITEM_ENCHANT_COUNT; ++e) {
|
||||
const auto id = Game::Read<uint32_t>(item, Offsets::OFF_VISIBLE_ITEM_ENCHANTS + e * 4);
|
||||
const uint8_t *enchant = DBC::Record(Offsets::VAR_SPELLITEMENCHANT_RECORDS,
|
||||
Offsets::VAR_SPELLITEMENCHANT_COUNT, id);
|
||||
if (enchant != nullptr &&
|
||||
Game::Read<uint32_t>(enchant, Offsets::OFF_SPELLITEMENCHANT_ITEM_VISUAL) != 0) {
|
||||
held.enchant = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TrimWorn(void *component, const Lanes &lanes, Worn &worn) {
|
||||
for (uint32_t slot = 0; slot < std::size(kInvSlotOfComponent); ++slot) {
|
||||
if (component == nullptr || ComponentRecord(component, slot) == nullptr)
|
||||
worn[kInvSlotOfComponent[slot] - 1] = Appearance{};
|
||||
}
|
||||
if (lanes[0].itemClass == 0)
|
||||
worn[Offsets::INVSLOT_MAINHAND - 1] = Appearance{};
|
||||
if (lanes[1].itemClass == 0)
|
||||
worn[Offsets::INVSLOT_OFFHAND - 1] = Appearance{};
|
||||
}
|
||||
|
||||
HeldLane LaneOf(const uint8_t *record) {
|
||||
HeldLane lane{};
|
||||
lane.itemClass = static_cast<uint8_t>(Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_CLASS));
|
||||
lane.subclass = static_cast<uint8_t>(Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SUBCLASS));
|
||||
lane.inventoryType = static_cast<uint8_t>(Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE));
|
||||
lane.sheatheType = static_cast<uint8_t>(Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SHEATH));
|
||||
return lane;
|
||||
}
|
||||
|
||||
void ReadRanged(void *unit, uint64_t guid, HeldLane &lane, const void *&display, Appearance &worn) {
|
||||
lane = HeldLane{};
|
||||
display = nullptr;
|
||||
worn = Appearance{};
|
||||
if (unit == nullptr)
|
||||
return;
|
||||
const HeldLane *held = VirtualOf<GetHeldItemLane_t>(unit, Offsets::VTBL_UNIT_GET_HELD_ITEM_LANE)(
|
||||
unit, Offsets::UNIT_HAND_RANGED, 0);
|
||||
const void *row = VirtualOf<GetHeldItemDisplay_t>(unit, Offsets::VTBL_UNIT_GET_HELD_ITEM_DISPLAY)(
|
||||
unit, Offsets::UNIT_HAND_RANGED);
|
||||
if (held == nullptr || held->itemClass == 0 || row == nullptr)
|
||||
return;
|
||||
lane = *held;
|
||||
display = row;
|
||||
// Only a player has visible items (ReadWorn's hazard).
|
||||
void *player = Object::ByGuid(Offsets::TYPEMASK_PLAYER, guid);
|
||||
if (player == nullptr)
|
||||
return;
|
||||
if (const void *item = reinterpret_cast<GetVisibleItem_t>(Offsets::FUN_UNIT_GET_VISIBLE_ITEM)(
|
||||
player, static_cast<uint32_t>(Offsets::INVSLOT_RANGED - 1)))
|
||||
worn.item = Game::Read<uint32_t>(item, Offsets::OFF_VISIBLE_ITEM_ITEM_ID);
|
||||
}
|
||||
|
||||
void Undress(void *model, void *component, Lanes &lanes, bool weapons) {
|
||||
if (model == nullptr)
|
||||
return;
|
||||
if (component != nullptr) {
|
||||
for (uint32_t slot = 0; slot < kComponentSlots; ++slot)
|
||||
DisplayInfo::UnequipDresserSlot(component, slot);
|
||||
}
|
||||
if (weapons) {
|
||||
ClearLane(model, lanes, 0);
|
||||
ClearLane(model, lanes, 1);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Model::Outfit
|
||||
@@ -0,0 +1,191 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// What a copy of a unit's character model wears, apart from its own model:
|
||||
// the character component that composites its armor, and the two held-item
|
||||
// lanes. 1.12's DressUpModel keeps these in the pane (benilla ui/dressup);
|
||||
// the ModelScene actor keeps its own. Both change them with the calls here.
|
||||
namespace Model::Outfit {
|
||||
|
||||
// Body slots of the two held lanes.
|
||||
constexpr uint32_t kBodySlotMainHand = 0xF;
|
||||
constexpr uint32_t kBodySlotOffHand = 0x10;
|
||||
|
||||
// A held lane, as the unit's VTBL_UNIT_GET_HELD_ITEM_LANE returns it and the
|
||||
// pane stores it (OFF_DRESSUPMODEL_*_HAND_LANE).
|
||||
struct HeldLane {
|
||||
uint8_t itemClass; // 0 = empty
|
||||
uint8_t subclass; // the item subclass (UNIT_VIRTUAL_ITEM_INFO byte 1)
|
||||
uint8_t unknown2;
|
||||
uint8_t inventoryType;
|
||||
uint8_t sheatheType;
|
||||
uint8_t unknown5[3];
|
||||
};
|
||||
static_assert(sizeof(HeldLane) == 8, "ClearModel zeroes 8 bytes per lane");
|
||||
using Lanes = HeldLane[2]; // main hand, off hand
|
||||
|
||||
// Retail's Enum.TransmogOutfitSlotOptionSheatheCategory: where a held item
|
||||
// hangs while sheathed.
|
||||
enum SheatheCategory : uint32_t {
|
||||
kSheatheDefault = 0, // the item's own sheath point
|
||||
kSheatheBack = 1,
|
||||
kSheatheSide = 2,
|
||||
kSheatheHide = 3, // no sheath point: nothing shows while sheathed
|
||||
};
|
||||
|
||||
// The sheathe type an item whose own type is `own` (OFF_ITEMSTATS_SHEATH)
|
||||
// hangs with in `category`: Back moves a hip weapon to the back pair, Side
|
||||
// moves a back weapon to the hip, Hide leaves no sheath point. Every other
|
||||
// item keeps its own (a shield, an item with no sheath point).
|
||||
uint8_t SheatheTypeFor(uint8_t own, uint32_t category);
|
||||
|
||||
// The row and glow each lane's item is hung with. The lanes keep no display
|
||||
// row, so whoever re-hangs a weapon keeps these beside them.
|
||||
struct Weapons {
|
||||
const void *display[2] = {}; // ItemDisplayInfo row on each lane, or null
|
||||
uint32_t visual[2] = {}; // enchant ItemVisuals row on each lane, or 0
|
||||
};
|
||||
|
||||
// The lane (0 or 1) of a held body slot.
|
||||
inline int LaneIndex(uint32_t bodySlot) { return bodySlot == kBodySlotOffHand ? 1 : 0; }
|
||||
|
||||
// DressUpModel SetModelInstance's lane copy (0x00504350): the unit's two
|
||||
// held-item lanes, zero where a hand is empty.
|
||||
void ReadLanes(void *unit, Lanes &lanes);
|
||||
|
||||
// The rows the unit `guid` hangs its held items with (AttachHandItem's
|
||||
// source), and, for a player, each one's enchant glow (AttachHandEnchant).
|
||||
void ReadWeapons(uint64_t guid, Weapons &weapons);
|
||||
|
||||
// A new character component for `model`, cloned from the unit's, as
|
||||
// DressUpModel SetModelInstance clones it; null when the unit has none.
|
||||
// It is pumped and released like a dresser (Model::DisplayInfo::PumpDresser,
|
||||
// DestroyDresser): both are the engine's character component.
|
||||
void *CloneComponent(void *model, void *unit);
|
||||
|
||||
// AttachEnchantVisual's step (0x005EED50): the glow on the item hung at
|
||||
// `attachId`.
|
||||
void HangVisual(void *model, int attachId, uint32_t visual);
|
||||
|
||||
// The unit's AttachHandItem step on each occupied lane: detach it, then hang
|
||||
// it in the hand or the sheath, or leave it off.
|
||||
void HangWeapons(void *model, const Lanes &lanes, const Weapons &weapons, bool sheathed,
|
||||
bool hide);
|
||||
|
||||
// Lane `i` hung again with `sheatheType`: detached at the type it hangs with
|
||||
// now (DetachHeldItemSlot finds the sheath point from it), then hung in the
|
||||
// hand or the sheath, or left off. The lane keeps the new type, so every
|
||||
// later detach finds the new sheath point.
|
||||
void SetSheatheType(void *model, Lanes &lanes, int i, const Weapons &weapons,
|
||||
uint8_t sheatheType, bool sheathed, bool hide);
|
||||
|
||||
// The glow `enchantID` gives an item shown with `display` (an ItemDisplayInfo
|
||||
// row): none when the display hangs its own (GetItemDescVisual's rule).
|
||||
uint32_t EnchantVisual(const void *display, uint32_t enchantID);
|
||||
|
||||
// TryOnItem's dispatch (0x00504540) on an item's inventory type.
|
||||
enum class Arm {
|
||||
Armor, // a component slot
|
||||
Held, // a held-item lane
|
||||
Nothing, // neck, finger, trinket, relic: nothing, and no error
|
||||
NotEquippable, // anything else: the worker shows ERR_NOT_EQUIPPABLE
|
||||
};
|
||||
|
||||
// The arm for `inventoryType`; `componentSlot` receives an armor arm's slot.
|
||||
Arm ArmOf(uint32_t inventoryType, uint32_t *componentSlot = nullptr);
|
||||
|
||||
// DressUpModel's dual-wield test: the model shows the active player, and the
|
||||
// player can dual wield.
|
||||
bool CanDualWield(uint64_t guid);
|
||||
|
||||
// How a held try-on is hung.
|
||||
struct HeldTryOn {
|
||||
uint32_t bodySlot = 0; // the hand named (kBodySlot*), or 0 for the worker's choice
|
||||
bool dualWield = false;
|
||||
bool sheathed = false;
|
||||
bool hide = false;
|
||||
uint32_t enchant = 0; // its glow's SpellItemEnchantment, or 0
|
||||
};
|
||||
|
||||
// TryOnItem's arms on whatever holds the component and the lanes: an armor
|
||||
// slot is cleared and set on the component, which `armorReady` (the pane's
|
||||
// ready byte) must allow; a held item evicts an other-lane item it cannot be
|
||||
// held with, then takes its lane and hangs. The inventory slot (1-based) the
|
||||
// item went on, or 0.
|
||||
int TryOn(void *model, void *component, bool armorReady, Lanes &lanes, Weapons &weapons,
|
||||
const uint8_t *record, const HeldTryOn &held);
|
||||
|
||||
// UndressSlot: the item in one inventory slot (1-based) off the model. A
|
||||
// held lane is detached and zeroed, an armor slot cleared on the component.
|
||||
// Without a component it does nothing, held lanes included (4.3.4's
|
||||
// CGDressUpModelFrame::UndressSlot returns first).
|
||||
void UndressSlot(void *model, void *component, Lanes &lanes, int invSlot);
|
||||
|
||||
// The component slot of an inventory slot (1-based), or -1 for a slot the
|
||||
// component has no record for (the neck, fingers, trinkets, hands held and
|
||||
// ranged).
|
||||
int ComponentSlotOf(int invSlot);
|
||||
|
||||
// The inventory slot (1-based) of a component slot ArmOf gives, or 0.
|
||||
int InvSlotOfComponent(uint32_t componentSlot);
|
||||
|
||||
// Whether TryOnItem's arms put anything in an inventory slot (1-based): a
|
||||
// hand takes any held item, an armor slot needs `armorReady` (a component the
|
||||
// model's ready gate allows).
|
||||
bool SlotAllowed(int invSlot, bool armorReady);
|
||||
|
||||
// What `unit` wears in one inventory slot (1-based), back on the model: an
|
||||
// armor slot takes the unit's component record (empty when the unit hides
|
||||
// its helm or cloak), a held lane the unit's lane, row and glow. Armor needs
|
||||
// a component and `armorReady`.
|
||||
void DressSlot(void *model, void *component, bool armorReady, Lanes &lanes, Weapons &weapons,
|
||||
void *unit, uint64_t guid, int invSlot, bool sheathed, bool hide);
|
||||
|
||||
// DressUpModel ClearModel (0x00504490): component slots 0..11 cleared, and,
|
||||
// with `weapons`, both lanes detached and zeroed.
|
||||
void Undress(void *model, void *component, Lanes &lanes, bool weapons);
|
||||
|
||||
// What one inventory slot wears, as retail's ItemTransmogInfo names it: the
|
||||
// item (1.12's appearance) and, on a hand, the SpellItemEnchantment whose
|
||||
// glow it shows. Zero for nothing.
|
||||
struct Appearance {
|
||||
uint32_t item = 0;
|
||||
uint32_t enchant = 0;
|
||||
};
|
||||
constexpr int kInventorySlots = 19;
|
||||
using Worn = Appearance[kInventorySlots]; // inventory slot n at [n - 1]
|
||||
|
||||
// What the unit `guid` wears where its model shows it: each armor slot its
|
||||
// component shows and each hand `weapons` (ReadWeapons) holds. Nothing for a
|
||||
// unit that is not a player, which has no visible items.
|
||||
void ReadWorn(void *unit, uint64_t guid, const Weapons &weapons, Worn &worn);
|
||||
|
||||
// Zeroes each slot of `worn` the model no longer shows: an armor slot the
|
||||
// component holds no record for (every one without a component), an empty
|
||||
// lane.
|
||||
void TrimWorn(void *component, const Lanes &lanes, Worn &worn);
|
||||
|
||||
// The held lane an item record gives, as TryOnItem copies it: class,
|
||||
// subclass, inventory type and sheathe type.
|
||||
HeldLane LaneOf(const uint8_t *record);
|
||||
|
||||
// RefreshRangedWeaponModel's source (0x00611E10): the unit's ranged lane and
|
||||
// display row (hand 2), and, for a player, the item its ranged slot wears.
|
||||
// All empty when the unit holds no ranged weapon.
|
||||
void ReadRanged(void *unit, uint64_t guid, HeldLane &lane, const void *&display, Appearance &worn);
|
||||
|
||||
} // namespace Model::Outfit
|
||||
+44
-85
@@ -51,7 +51,8 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "frame/Layout.h"
|
||||
#include "gx/Device.h"
|
||||
#include "model/Camera.h"
|
||||
#include "model/DisplayInfo.h"
|
||||
#include "model/M2.h"
|
||||
@@ -72,25 +73,11 @@ constexpr uint32_t kTexels = kSize * kSize;
|
||||
|
||||
using CreateScene_t = void *(__cdecl *)();
|
||||
using ReleaseScene_t = void(__thiscall *)(void *scene);
|
||||
using CopyModel_t = void *(__thiscall *)(void *scene, void *source, uint32_t flags);
|
||||
using ApplyDisplay_t = void(__fastcall *)(void *model, const uint8_t *display,
|
||||
const uint8_t *modelData);
|
||||
using DetachAll_t = void(__fastcall *)(void *model);
|
||||
using DrawScene_t = void(__thiscall *)(void *scene, uint32_t pass);
|
||||
using SetTransforms_t = void(__fastcall *)(void *camera, const float *rect, uint32_t unused);
|
||||
using GetMatrix_t = void(__fastcall *)(float *m);
|
||||
using SetMatrix_t = void(__fastcall *)(const float *m);
|
||||
using GetViewport_t = void(__fastcall *)(float *, float *, float *, float *, float *, float *);
|
||||
using SetViewport_t = void(__stdcall *)(float, float, float, float, float, float);
|
||||
using ScreenSize_t = void(__fastcall *)(float *rect);
|
||||
using DdcToNdc_t = void(__fastcall *)(float *x, float *y, float inX, float inY);
|
||||
using DeviceGate_t = int(__cdecl *)();
|
||||
using MasterGet_t = bool(__fastcall *)(uint32_t cap);
|
||||
using MasterSet_t = void(__fastcall *)(uint32_t cap, uint32_t on);
|
||||
using SetClearColor_t = void(__stdcall *)(uint32_t argb);
|
||||
using Clear_t = void(__fastcall *)(uint32_t mask);
|
||||
using AlphaMask_t = const uint8_t *(__fastcall *)(uint32_t size);
|
||||
using GetDeviceTexture_t = void *(__fastcall *)(void *texture, uint32_t wait, void *callback);
|
||||
using MarkDirty_t = void(__fastcall *)(void *gxTex, const int32_t *rect, uint32_t immediate);
|
||||
using SetImage_t = void(__thiscall *)(void *region, void *texture);
|
||||
|
||||
@@ -152,77 +139,62 @@ bool Bake(Portrait &p) {
|
||||
void *scene = reinterpret_cast<CreateScene_t>(Offsets::FUN_M2_CREATE_SCENE)();
|
||||
if (scene == nullptr)
|
||||
return false;
|
||||
void *copy = reinterpret_cast<CopyModel_t>(Offsets::FUN_M2SCENE_COPY_MODEL)(scene, source, 0);
|
||||
void *copy = M2::CopyWithoutItems(scene, source);
|
||||
if (copy == nullptr) {
|
||||
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(scene);
|
||||
return false;
|
||||
}
|
||||
reinterpret_cast<DetachAll_t>(Offsets::FUN_DETACH_ALL_ITEM_MODELS)(copy);
|
||||
void *camera = Model::Camera::CameraById(copy, 0);
|
||||
M2::PlayAnimation(copy, -1, 0, 0, 0, 1.0f, 0, 1);
|
||||
|
||||
float projection[16];
|
||||
float view[16];
|
||||
float viewport[6];
|
||||
reinterpret_cast<GetMatrix_t>(Offsets::FUN_GX_GET_PROJECTION)(projection);
|
||||
reinterpret_cast<GetMatrix_t>(Offsets::FUN_GX_GET_VIEW)(view);
|
||||
reinterpret_cast<GetViewport_t>(Offsets::FUN_GX_GET_VIEWPORT)(
|
||||
&viewport[0], &viewport[1], &viewport[2], &viewport[3], &viewport[4], &viewport[5]);
|
||||
const bool fog = reinterpret_cast<MasterGet_t>(Offsets::FUN_GX_MASTER_ENABLE_GET)(
|
||||
Offsets::GX_MASTER_FOG);
|
||||
auto setMaster = reinterpret_cast<MasterSet_t>(Offsets::FUN_GX_MASTER_ENABLE_SET);
|
||||
setMaster(Offsets::GX_MASTER_FOG, 0);
|
||||
{
|
||||
const Gx::SavedTransforms saved;
|
||||
const float aspect = static_cast<float>((static_cast<double>(x1) - x0) /
|
||||
(static_cast<double>(y1) - y0));
|
||||
Frame::Layout::DdcToNdc(&x0, &y0, x0, y0);
|
||||
Frame::Layout::DdcToNdc(&x1, &y1, x1, y1);
|
||||
|
||||
const float aspect = static_cast<float>((static_cast<double>(x1) - x0) /
|
||||
(static_cast<double>(y1) - y0));
|
||||
auto toNdc = reinterpret_cast<DdcToNdc_t>(Offsets::FUN_COORD_DDC_TO_NDC);
|
||||
toNdc(&x0, &y0, x0, y0);
|
||||
toNdc(&x1, &y1, x1, y1);
|
||||
// First collection: evaluate the portrait camera.
|
||||
M2::IsLoaded(copy, 1, 1);
|
||||
M2::SetAnimated(copy, true);
|
||||
const float origin[3] = {0.0f, 0.0f, 0.0f};
|
||||
M2::AnimateScene(scene, origin);
|
||||
const Model::Camera::Vec3 eye = Model::Camera::GetVector(camera, Offsets::CAMERA_PROP_EYE);
|
||||
Model::Camera::GetVector(camera, Offsets::CAMERA_PROP_TARGET); // the bake reads it too
|
||||
const float rect[4] = {0.0f, 0.0f, 1.0f, aspect};
|
||||
Model::Camera::SetDeviceTransforms(camera, rect);
|
||||
Gx::SetViewport({x0, x1, y0, y1, 0.0f, 1.0f});
|
||||
Gx::SetClearColor(0);
|
||||
Gx::Clear(3);
|
||||
|
||||
// First collection: evaluate the portrait camera.
|
||||
M2::IsLoaded(copy, 1, 1);
|
||||
M2::SetAnimated(copy, true);
|
||||
const float origin[3] = {0.0f, 0.0f, 0.0f};
|
||||
M2::AnimateScene(scene, origin);
|
||||
const Model::Camera::Vec3 eye = Model::Camera::GetVector(camera, Offsets::CAMERA_PROP_EYE);
|
||||
Model::Camera::GetVector(camera, Offsets::CAMERA_PROP_TARGET); // the bake reads it too
|
||||
const float rect[4] = {0.0f, 0.0f, 1.0f, aspect};
|
||||
reinterpret_cast<SetTransforms_t>(Offsets::FUN_CAMERA_SET_DEVICE_TRANSFORMS)(camera, rect, 0);
|
||||
reinterpret_cast<SetViewport_t>(Offsets::FUN_GX_SET_VIEWPORT)(x0, x1, y0, y1, 0.0f, 1.0f);
|
||||
reinterpret_cast<SetClearColor_t>(Offsets::FUN_GX_SET_CLEAR_COLOR)(0);
|
||||
auto clear = reinterpret_cast<Clear_t>(Offsets::FUN_GX_CLEAR);
|
||||
clear(3);
|
||||
// Second collection from the portrait eye, then both passes.
|
||||
M2::IsReady(copy, 1, 1);
|
||||
M2::SetAnimated(copy, true);
|
||||
M2::SetVisible(copy, true);
|
||||
M2::SetRenderCallback(copy, reinterpret_cast<const void *>(Offsets::FUN_PORTRAIT_LIGHTING),
|
||||
nullptr);
|
||||
M2::AnimateScene(scene, &eye.x);
|
||||
M2::DrawScene(scene, 0);
|
||||
M2::DrawScene(scene, 1);
|
||||
|
||||
// Second collection from the portrait eye, then both passes.
|
||||
M2::IsReady(copy, 1, 1);
|
||||
M2::SetAnimated(copy, true);
|
||||
M2::SetVisible(copy, true);
|
||||
M2::SetRenderCallback(copy, reinterpret_cast<const void *>(Offsets::FUN_PORTRAIT_LIGHTING),
|
||||
nullptr);
|
||||
M2::AnimateScene(scene, &eye.x);
|
||||
auto draw = reinterpret_cast<DrawScene_t>(Offsets::FUN_M2SCENE_DRAW);
|
||||
draw(scene, 0);
|
||||
draw(scene, 1);
|
||||
|
||||
const int32_t box[4] = {0, 0, static_cast<int32_t>(kSize), static_cast<int32_t>(kSize)};
|
||||
TexelArray out{kTexels, kTexels, p.texels.data(), kTexels};
|
||||
reinterpret_cast<void(__fastcall *)(const int32_t *, TexelArray *)>(
|
||||
Offsets::FUN_GX_READ_PIXELS)(box, &out);
|
||||
clear(3);
|
||||
const auto *mask = reinterpret_cast<const AlphaMask *>(
|
||||
reinterpret_cast<AlphaMask_t>(Offsets::FUN_PORTRAIT_ALPHA_MASK)(kSize));
|
||||
if (out.data == p.texels.data()) {
|
||||
for (uint32_t i = 0; i < kTexels; ++i)
|
||||
p.texels[i] = (p.texels[i] & 0x00FFFFFFu) | static_cast<uint32_t>(mask->alpha[i]) << 24;
|
||||
void *gxTex = reinterpret_cast<GetDeviceTexture_t>(Offsets::FUN_TEXTURE_GET_RENDERABLE)(
|
||||
p.texture, 1, nullptr);
|
||||
reinterpret_cast<MarkDirty_t>(Offsets::FUN_GX_TEX_MARK_RECT_DIRTY)(gxTex, box, 1);
|
||||
const int32_t box[4] = {0, 0, static_cast<int32_t>(kSize), static_cast<int32_t>(kSize)};
|
||||
TexelArray out{kTexels, kTexels, p.texels.data(), kTexels};
|
||||
reinterpret_cast<void(__fastcall *)(const int32_t *, TexelArray *)>(
|
||||
Offsets::FUN_GX_READ_PIXELS)(box, &out);
|
||||
Gx::Clear(3);
|
||||
const auto *mask = reinterpret_cast<const AlphaMask *>(
|
||||
reinterpret_cast<AlphaMask_t>(Offsets::FUN_PORTRAIT_ALPHA_MASK)(kSize));
|
||||
if (out.data == p.texels.data()) {
|
||||
for (uint32_t i = 0; i < kTexels; ++i)
|
||||
p.texels[i] = (p.texels[i] & 0x00FFFFFFu) | static_cast<uint32_t>(mask->alpha[i]) << 24;
|
||||
void *gxTex = Texture::DeviceTexture(p.texture);
|
||||
reinterpret_cast<MarkDirty_t>(Offsets::FUN_GX_TEX_MARK_RECT_DIRTY)(gxTex, box, 1);
|
||||
}
|
||||
}
|
||||
|
||||
reinterpret_cast<SetMatrix_t>(Offsets::FUN_GX_SET_PROJECTION)(projection);
|
||||
reinterpret_cast<SetMatrix_t>(Offsets::FUN_GX_SET_VIEW)(view);
|
||||
reinterpret_cast<SetViewport_t>(Offsets::FUN_GX_SET_VIEWPORT)(
|
||||
viewport[0], viewport[1], viewport[2], viewport[3], viewport[4], viewport[5]);
|
||||
setMaster(Offsets::GX_MASTER_FOG, fog ? 1 : 0);
|
||||
M2::Release(copy);
|
||||
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(scene);
|
||||
@@ -270,27 +242,14 @@ Portrait *Acquire(uint32_t displayID) {
|
||||
auto found = g_portraits.find(displayID);
|
||||
if (found != g_portraits.end())
|
||||
return found->second.get();
|
||||
const uint8_t *display = DBC::Record(Offsets::VAR_CREATUREDISPLAYINFO_RECORDS,
|
||||
Offsets::VAR_CREATUREDISPLAYINFO_COUNT, displayID);
|
||||
if (display == nullptr)
|
||||
return nullptr;
|
||||
const uint8_t *modelData = DBC::Record(
|
||||
Offsets::VAR_CREATUREMODELDATA_RECORDS, Offsets::VAR_CREATUREMODELDATA_COUNT,
|
||||
Game::Read<uint32_t>(display, Offsets::OFF_CREATUREDISPLAYINFO_MODEL_ID));
|
||||
if (modelData == nullptr)
|
||||
return nullptr;
|
||||
|
||||
void *scene = reinterpret_cast<CreateScene_t>(Offsets::FUN_M2_CREATE_SCENE)();
|
||||
if (scene == nullptr)
|
||||
return nullptr;
|
||||
void *model = M2::CreateModel(
|
||||
scene, Game::Read<const char *>(modelData, Offsets::OFF_CREATUREMODELDATA_MODEL_PATH));
|
||||
void *model = Model::DisplayInfo::CreateDisplayModel(scene, displayID);
|
||||
if (model == nullptr) {
|
||||
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(scene);
|
||||
return nullptr;
|
||||
}
|
||||
reinterpret_cast<ApplyDisplay_t>(Offsets::FUN_APPLY_CREATURE_DISPLAY)(model, display,
|
||||
modelData);
|
||||
|
||||
// The bake's own texture: 64x64, one mip, ARGB8888, flags (1, 0, 0, 0, 0,
|
||||
// 0, 1, 0), "Portrait1" (0x005257D3).
|
||||
|
||||
+2363
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,645 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "animation/Object.h"
|
||||
#include "model/Outfit.h"
|
||||
#include "model/SceneContact.h"
|
||||
#include "model/TryOn.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// The ModelScene frame (model/Scene.cpp) and its actors (model/SceneActor.cpp).
|
||||
// docs/notes/model-scene.md has the plan, the retail contract and the probes.
|
||||
namespace Model::Scene {
|
||||
|
||||
class Actor;
|
||||
namespace World {
|
||||
struct Ambience;
|
||||
}
|
||||
namespace Mover {
|
||||
struct Body;
|
||||
}
|
||||
|
||||
// The light of a spell's model in flight or left behind, as the world gives
|
||||
// a missile or an effect a light node of its own: an entity standing on the
|
||||
// WMO under it (World::Settle / ActorLight), else the frame's light.
|
||||
struct EntityLight {
|
||||
explicit EntityLight(void *frame);
|
||||
~EntityLight();
|
||||
EntityLight(const EntityLight &) = delete;
|
||||
EntityLight &operator=(const EntityLight &) = delete;
|
||||
void *frame;
|
||||
World::Ambience *ambience;
|
||||
};
|
||||
|
||||
// The model lights as an entity of the scene's WMO through its render
|
||||
// callback, with its own `light` (model/SceneSpell.cpp).
|
||||
void LightAsEntity(void *model, std::unique_ptr<EntityLight> &light, void *frame);
|
||||
// Gives the model the frame's light callback again, frees `light`, and
|
||||
// hides and releases the model.
|
||||
void ReleaseLit(void *model, std::unique_ptr<EntityLight> &light);
|
||||
// The light's re-placement when the model moved, and its ease, each tick.
|
||||
void SettleLight(void *model, EntityLight *light, float elapsed);
|
||||
|
||||
// One PlaySpellVisual on its caster (model/SceneSpell.cpp): the precast, the
|
||||
// cast, then the missile in flight.
|
||||
struct SpellCast {
|
||||
enum class Phase { Precast, Cast, Flight };
|
||||
uint32_t id = 0; // PlaySpellVisual's cast ID
|
||||
uint32_t spellID = 0;
|
||||
// The visual the caster shows (UnitGetSpellVisual's 64 bytes: the
|
||||
// spell's record, a ranged spell's gaps filled from its weapon's).
|
||||
uint8_t visual[Offsets::SPELL_VISUAL_SIZE] = {};
|
||||
// The ammo the shot carries (SMSG_SPELL_GO's): an ItemDisplayInfo id and
|
||||
// inventory type, 0 for a spell that is not ranged.
|
||||
uint32_t ammoDisplay = 0;
|
||||
uint32_t ammoType = 0;
|
||||
Actor *target = nullptr; // the caster for a cast on itself
|
||||
Phase phase = Phase::Precast;
|
||||
float precastLeftSec = 0.0f;
|
||||
std::vector<void *> precastEffects; // one reference each
|
||||
bool precastTinted = false; // the precast kit set a tint or alpha
|
||||
void *missile = nullptr; // one reference, from the cast on
|
||||
void *missileSound = nullptr;
|
||||
float queuedSec = 0.0f; // since the missile was made, until its release
|
||||
float speed = 0.0f;
|
||||
float position[3] = {};
|
||||
float destination[3] = {};
|
||||
int attachment = -1; // the target attachment the launch settled, or -1
|
||||
bool sticks = false; // shot from a bow, crossbow or thrown weapon
|
||||
std::unique_ptr<EntityLight> light; // the missile's
|
||||
};
|
||||
|
||||
// A cast that reached its target this frame, for the caster's
|
||||
// OnSpellVisualImpact.
|
||||
struct SpellImpact {
|
||||
uint32_t castID = 0;
|
||||
uint32_t spellID = 0;
|
||||
Actor *target = nullptr;
|
||||
};
|
||||
|
||||
// A missile stuck in its target, until its time is up.
|
||||
struct StuckMissile {
|
||||
void *model = nullptr; // one reference
|
||||
float leftSec = 0.0f;
|
||||
};
|
||||
|
||||
// An impact model left at a missile's arrival, until its particles are gone.
|
||||
struct ImpactModel {
|
||||
void *model = nullptr; // one reference
|
||||
int framesShown = 0;
|
||||
std::unique_ptr<EntityLight> light;
|
||||
};
|
||||
|
||||
// A shard of a spell's ground area (model/SceneArea.cpp): one model of the
|
||||
// emitter's file, played once where it fell, as the world's shard
|
||||
// (0x006EBB30). Its callbacks hold its address.
|
||||
struct AreaShard {
|
||||
void *frame = nullptr;
|
||||
void *model = nullptr; // one reference
|
||||
float position[3] = {};
|
||||
uint32_t startMs = 0; // the scene clock at which it starts
|
||||
bool started = false;
|
||||
bool done = false; // its animation ended
|
||||
std::unique_ptr<EntityLight> light;
|
||||
};
|
||||
|
||||
// The area kit of a spell's ground area while it runs: the shard emitter
|
||||
// (0x006EB930) and the kit's looping sound.
|
||||
struct AreaEmitter {
|
||||
bool running = false;
|
||||
const char *path = nullptr; // the shard file, null for a kit without an emitter
|
||||
float rate = 0.0f; // shards a second
|
||||
double accumulator = 0.0;
|
||||
int32_t soundID = 0;
|
||||
void *loopSound = nullptr;
|
||||
};
|
||||
|
||||
// A ModelSceneActor: one model in its scene frame's M2 scene, placed by the
|
||||
// scene's paint. A script object (Animation::Object) whose parent is the scene
|
||||
// frame, which owns it.
|
||||
class Actor : public Animation::Object {
|
||||
public:
|
||||
static Animation::Class s_class;
|
||||
|
||||
Actor(void *scene, const char *name);
|
||||
~Actor();
|
||||
|
||||
void *SceneFrame() const { return parent; }
|
||||
|
||||
// From the scene frame's update, before the scene clock advances: puts a
|
||||
// ready model on the scene's animate list.
|
||||
void Link();
|
||||
// From the scene frame's update walk: load and dress state, scripts.
|
||||
// `elapsedSec` is the frame's time (OnUpdate), `sceneSec` the time the
|
||||
// scene clock moved (casts; 0 while the scene is paused).
|
||||
void Update(float elapsedSec, float sceneSec);
|
||||
// From the scene's paint, before the scene animates: the model's
|
||||
// placement matrix.
|
||||
void Place();
|
||||
// The scene frame's alpha changed.
|
||||
void ApplyAlpha();
|
||||
|
||||
// Shows `model` (one reference, which the actor takes over), or nothing.
|
||||
void SetModel(void *model, const char *path, const uint8_t *dressRecord);
|
||||
// Shows a copy of the unit's model, dressed and armed per autoDress,
|
||||
// sheathed and hideWeapons; false (and no model) when it has none.
|
||||
bool SetUnitModel(uint64_t guid);
|
||||
// Hangs the hand lanes per MeleeSheathed and hideWeapons, and the ranged
|
||||
// weapon in hand while it is drawn.
|
||||
void ApplyWeapons();
|
||||
// A ranged spell's start (the unit's sheathe state 2): the melee items
|
||||
// on their sheath points, the ranged weapon in hand, until SetSheathed or
|
||||
// a new model.
|
||||
void DrawRanged();
|
||||
// Leaves sheathe state 2: the ranged weapon leaves the hand.
|
||||
void SheatheRanged();
|
||||
// Whether the hand lanes hang at their sheath points: sheathed, or the
|
||||
// ranged weapon drawn.
|
||||
bool MeleeSheathed() const { return sheathed || rangedDrawn; }
|
||||
// Puts the item on the model (Outfit::TryOn), once the model has loaded
|
||||
// and the item's record is cached; returns the ItemTryOnReason.
|
||||
int TryOnItem(const Model::TryOn::Request &request);
|
||||
void ClearModel();
|
||||
void PlayAnimation();
|
||||
// Shows the kit (0 for none) on the model, replacing the one shown.
|
||||
void SetSpellVisualKit(uint32_t kitID, bool oneShot);
|
||||
// Loads the actor's XML element (an <Actor> template), then runs OnLoad.
|
||||
void LoadXml(const void *node, void *status);
|
||||
|
||||
void *model = nullptr;
|
||||
std::string modelPath;
|
||||
const uint8_t *dressRecord = nullptr; // CreatureDisplayInfoExtra, until dressed
|
||||
float displayScale = 1.0f; // a creature display's CreatureModelScale, else 1
|
||||
// The CreatureDisplayInfo the model shows (a creature display's, or the
|
||||
// unit's UNIT_FIELD_DISPLAYID), for its sound data; 0 for a model file.
|
||||
uint32_t displayID = 0;
|
||||
bool playAnimationSounds = false; // SetPlayAnimationSounds
|
||||
// SetModelBySpellArea: the ground area of a spell, as the world shows a
|
||||
// dynamic object (CGDynamicObject_C). Its SpellVisualEffectName row and
|
||||
// its radius in yards; null and 0 for any other model.
|
||||
const uint8_t *areaEffect = nullptr;
|
||||
float areaRadius = 0.0f;
|
||||
const uint8_t *areaVisual = nullptr; // the spell's SpellVisual row
|
||||
// The area's kit, and its shards, which play out after the area goes.
|
||||
AreaEmitter areaEmitter;
|
||||
std::vector<std::unique_ptr<AreaShard>> areaShards;
|
||||
bool shadowShown = false; // SetShadowShown: a blob shadow under the model
|
||||
// SetHighlight: a unit's light bits (CGObject +0xB8, Offsets
|
||||
// CGOBJECT_LIGHT_BIT_*); while any is set the model glows in the scene
|
||||
// light's ambient colour.
|
||||
uint32_t highlightBits = 0;
|
||||
// SetSelectionCircle: a selection circle under the model, in this ARGB.
|
||||
bool selectionCircleShown = false;
|
||||
uint32_t selectionCircle = 0;
|
||||
// SetGroundReticle: the world's ground-target reticle at the actor's
|
||||
// position: its radius as given (0 takes the world's default) and
|
||||
// whether it shows the in-range texture.
|
||||
bool groundReticleShown = false;
|
||||
float groundReticleRadius = 0.0f;
|
||||
bool groundReticleValid = true;
|
||||
// The model's emissive colour for the highlight bits.
|
||||
void ApplyHighlight();
|
||||
// SetNameText: the name over the head, drawn as the world draws a unit's
|
||||
// (UpdatePlate 0x006C6E90) from the model's draw callback. Its text and
|
||||
// ARGB colour; the font string, made at the next draw while nameDirty,
|
||||
// with the font that made it (the world's name font, destroyed at
|
||||
// logout before the actors); and the block height, the extra lines times
|
||||
// the plate scale at that draw.
|
||||
std::string nameText;
|
||||
uint32_t nameColor = 0xFFFFFFFFu;
|
||||
void *nameString = nullptr;
|
||||
const void *nameFont = nullptr;
|
||||
bool nameDirty = false;
|
||||
float nameBlockHeight = 0.0f;
|
||||
// GetNamePosition (0x00608640) for the model, in scene coordinates;
|
||||
// false without a loaded model.
|
||||
bool NamePosition(float (&out)[3]) const;
|
||||
// From the model's draw callback: UpdatePlate's name half.
|
||||
void DrawName();
|
||||
void DropNameString();
|
||||
World::Ambience *ambience; // its light in the scene's WMO (owned)
|
||||
// The character component dressing the model: a display's dress, or the
|
||||
// clone of a unit's.
|
||||
void *dresser = nullptr;
|
||||
// A unit's model (SetModelByUnit): the unit and what the copy holds.
|
||||
uint64_t unitGuid = 0;
|
||||
Outfit::Lanes lanes = {};
|
||||
Outfit::Weapons weapons;
|
||||
Outfit::Worn worn = {}; // what each inventory slot wears (GetItemTransmogInfoList)
|
||||
bool autoDress = true;
|
||||
bool sheathed = false;
|
||||
bool hideWeapons = false;
|
||||
bool rangedDrawn = false; // the unit's sheathe state 2
|
||||
// The ranged slot (a ClassicAPI extension): a unit's third held lane,
|
||||
// inventory slot 18, which every bow, gun, crossbow, wand and thrown
|
||||
// weapon takes. Its ItemDisplayInfo row, and its model (a unit's +0xD24,
|
||||
// one reference) while hung: in hand while drawn, else only at a sheath
|
||||
// point its type has.
|
||||
Outfit::HeldLane ranged = {};
|
||||
const void *rangedDisplay = nullptr;
|
||||
void *rangedModel = nullptr;
|
||||
// Per lane: the SetSheathedCategory category (Outfit::SheatheCategory),
|
||||
// and the sheathe type the lane's item has of its own. The lane itself
|
||||
// holds the type it hangs with.
|
||||
uint32_t sheatheCategory[2] = {};
|
||||
uint8_t ownSheathe[2] = {};
|
||||
bool useUnitSheatheCategory = false; // UseUnitSheatheCategory
|
||||
// The ammo tried on (an INVTYPE_AMMO item, as a player equips it): its
|
||||
// ItemDisplayInfo id, 0 for none.
|
||||
uint32_t ammoDisplay = 0;
|
||||
// Lane `i` (on `onModel`, the actor's model or the copy being built) was
|
||||
// just filled with its item's own type: hang it per its category.
|
||||
void TookLane(void *onModel, int i);
|
||||
// Hangs lane `i` per its category.
|
||||
void ApplySheatheCategory(void *onModel, int i);
|
||||
// DressUpModel's ready byte: the model may take armor (not a shapeshifted
|
||||
// unit's).
|
||||
bool armorReady = true;
|
||||
std::vector<Model::TryOn::Request> pendingTryOns; // waiting for the model or the record
|
||||
bool loaded = false; // OnModelLoaded has run for the model
|
||||
bool animFinished = false;
|
||||
|
||||
// The client's mover (SetMovementEnabled, a ClassicAPI extension): while
|
||||
// set, it moves `position` and turns `yaw` through the scene's WMO.
|
||||
std::unique_ptr<Mover::Body> mover;
|
||||
// A unit's facings while the mover is on (CGUnit_C +0xC98, +0xC94): the
|
||||
// display facing the body turns toward, the body facing the model is
|
||||
// drawn at, the time the display last stood still (+0xCB0), and the body
|
||||
// pass's bits (+0xD58): 0x1 turning, 0x80 and 0x100 the model's twist
|
||||
// bones, marked for `bonesOf` once it has loaded. `shuffle` is the pass's
|
||||
// 0x800 (1) or 0x1000 (-1): the body caught up turning left or right on
|
||||
// the spot this frame.
|
||||
struct Facings {
|
||||
float display = 0.0f;
|
||||
float body = 0.0f;
|
||||
uint32_t stamp = 0;
|
||||
uint32_t bits = 0;
|
||||
int shuffle = 0;
|
||||
const void *bonesOf = nullptr;
|
||||
} facings;
|
||||
// The yaw the model is drawn at: the body facing while the mover is on.
|
||||
float ShownYaw() const { return mover != nullptr ? facings.body : yaw; }
|
||||
|
||||
float position[3] = {0.0f, 0.0f, 0.0f};
|
||||
float yaw = 0.0f, pitch = 0.0f, roll = 0.0f;
|
||||
float scale = 1.0f;
|
||||
float alpha = 1.0f;
|
||||
bool shown = true;
|
||||
// FadeOut: the world's fade of a removed object's model, on the scene
|
||||
// clock from fadeStartMs; the actor hides when it ends.
|
||||
bool fadingOut = false;
|
||||
uint32_t fadeStartMs = 0;
|
||||
bool useCenterForOrigin[3] = {false, false, false};
|
||||
uint32_t animation = 0;
|
||||
int variation = -1;
|
||||
float animSpeed = 1.0f;
|
||||
float animOffsetSec = 0.0f;
|
||||
uint32_t blendOperation = 1; // Enum.ModelBlendOperation: 0 None, 1 Anim
|
||||
bool paused = false;
|
||||
bool globalPaused = false;
|
||||
int heldPositionMs = -1; // the paused frame; -1 until read from a loaded model
|
||||
uint32_t spellVisualKit = 0;
|
||||
bool spellVisualKitOneShot = false;
|
||||
std::vector<void *> kitEffects; // the kit's effect models, one reference each
|
||||
// The kit's world effect (OFF_SPELLVISUALKIT_WORLD_EFFECT): a model of its
|
||||
// own in the scene, planted where the actor stood, one reference, lit as
|
||||
// an entity; `kitWorldDone` once a one-shot effect has ended.
|
||||
void *kitWorldEffect = nullptr;
|
||||
float kitWorldPosition[3] = {0.0f, 0.0f, 0.0f};
|
||||
std::unique_ptr<EntityLight> kitWorldLight;
|
||||
bool kitWorldDone = false;
|
||||
// ApplySpellVisualKit calls made before the model loaded: {kit, oneShot}.
|
||||
std::vector<std::pair<uint32_t, bool>> pendingKits;
|
||||
|
||||
// A spell kit's animation, shown in place of `animation` (-1 none): a
|
||||
// precast's loops, a cast's or an impact's plays once.
|
||||
int kitAnimation = -1;
|
||||
bool kitAnimationLoops = false;
|
||||
// The precast's looping animation (-1 none), which a once-only kit
|
||||
// animation (a wound) gives back when it ends, as a unit's replays its
|
||||
// spell's looping kit animation.
|
||||
int kitLoopAnimation = -1;
|
||||
bool kitAnimationEnded = false; // a once-only kit animation ended this frame
|
||||
void PlayKitAnimation(int anim, bool loops);
|
||||
// The animation the body plays: a spell kit's, else the script's.
|
||||
int CurrentAnimation() const { return kitAnimation >= 0 ? kitAnimation : static_cast<int>(animation); }
|
||||
// A release event of the model this frame ($CSL / $CSR / $CST, or $BWR
|
||||
// from the bow or rifle release point), and its point.
|
||||
bool releaseEvent = false;
|
||||
float releasePoint[3] = {};
|
||||
// The ranged pose, as a unit keeps it: the ammo in hand (one reference,
|
||||
// nocked at attachment 35 while the bow is drawn), its display, and the
|
||||
// drawn bit.
|
||||
void *heldAmmo = nullptr;
|
||||
uint32_t heldAmmoDisplay = 0;
|
||||
bool bowDrawn = false;
|
||||
// The unit fields DrawBowstring reads (the model and the drawn bit), the
|
||||
// user data of a hung bow's string; each update, before the draw.
|
||||
alignas(4) uint8_t stringUnit[Offsets::OFF_CGUNIT_STATE_FLAGS + 4] = {};
|
||||
std::vector<SpellCast> spellCasts;
|
||||
// The casts that reached their target, in order, until the update runs
|
||||
// OnSpellVisualImpact for them: outside the casts' step, which a script
|
||||
// that casts or stops a spell would change under it.
|
||||
std::vector<SpellImpact> spellImpacts;
|
||||
// The moves (MoveTo) the mover reported done, until the update runs
|
||||
// OnMoveFinished for them.
|
||||
uint32_t movesFinished = 0;
|
||||
std::vector<ImpactModel> impactModels;
|
||||
std::vector<StuckMissile> stuckMissiles; // the missiles stuck in this actor
|
||||
};
|
||||
|
||||
// The spell visuals an actor casts (model/SceneSpell.cpp).
|
||||
namespace Casts {
|
||||
// Starts the cast and returns its ID (from 1); 0 when the spell, its visual
|
||||
// or the caster's model is missing. `castTimeMs` < 0 takes the spell's cast
|
||||
// time.
|
||||
uint32_t Play(Actor &caster, uint32_t spellID, Actor *target, int castTimeMs);
|
||||
// Interrupts the casts still preparing; casts already made go on.
|
||||
void Stop(Actor &caster);
|
||||
// The model's $BWP: the bow draws and the ammo is nocked, while a ranged
|
||||
// spell is cast.
|
||||
void BowPull(Actor &caster);
|
||||
// The model's $BWR: the weapon plays its release before the missiles go.
|
||||
void BowRelease(Actor &caster);
|
||||
// The animation a ranged load or hold goes on to when it ends, or -1 for
|
||||
// none (the ranged shot cycle).
|
||||
int RangedAnimationAfter(const Actor &caster, int ended);
|
||||
// EndRangedPose (0x0060F530): the bow is no longer drawn, the ammo leaves the
|
||||
// hand, and the weapon stands again unless it plays its release.
|
||||
void EndRangedPose(Actor &actor);
|
||||
// From the actor's update, after the scene advance: the casts move on.
|
||||
void Update(Actor &caster, float elapsedSec);
|
||||
// From the actor's Link: the released missiles and the impact models go on
|
||||
// the scene's animate list.
|
||||
void Link(Actor &caster);
|
||||
// From the actor's Place: the missiles' placement.
|
||||
void Place(Actor &caster);
|
||||
// Drops every cast, missile and impact model of the caster.
|
||||
void Drop(Actor &caster);
|
||||
// Stops the missiles' loop sounds (the scene hid; its update stops).
|
||||
void Silence(Actor &caster);
|
||||
} // namespace Casts
|
||||
|
||||
// The byte a spell visual kit procedure keeps in a float value: the bits of
|
||||
// the value + 512.0, shifted right 14 (the engine's decode, 0x005D5665 and
|
||||
// 0x0060DB19; model/SceneArea.cpp).
|
||||
uint8_t DecodeKitByte(float value);
|
||||
|
||||
// The chain effects of a scene's casts (model/SceneChain.cpp): Chain
|
||||
// Lightning's bolts, drawn by a lightning renderer the scene owns.
|
||||
namespace Chains {
|
||||
// From a cast's start: the cast kit's chain procedures (kinds 0 and 12)
|
||||
// draw a bolt from the caster to the target.
|
||||
void Start(Actor &caster, Actor &target, const uint8_t *visual, const uint8_t *kit);
|
||||
// From the scene's update, after the actors': the hops move with their
|
||||
// ends and end on the scene clock; the bolts animate.
|
||||
void Update(void *frame, float sceneSec);
|
||||
// From the scene's paint, after the models: draws the bolts.
|
||||
void Render(void *frame, const float (&eye)[3]);
|
||||
// The actor goes: its hops go with it.
|
||||
void Forget(Actor &actor);
|
||||
// The scene goes.
|
||||
void Destroy(void *frame);
|
||||
} // namespace Chains
|
||||
|
||||
// The area kit of an actor that shows a spell's ground area
|
||||
// (model/SceneArea.cpp): its shard emitter and its looping sound.
|
||||
namespace Areas {
|
||||
// From the actor's update, after the scene advance: the kit starts while the
|
||||
// actor shows an area and is shown, and stops otherwise; shards fall and
|
||||
// the finished ones go.
|
||||
void Update(Actor &actor, float sceneSec);
|
||||
// From the actor's Link: the shards whose start came go on the animate list.
|
||||
void Link(Actor &actor);
|
||||
// From the actor's Place: the shards' placement and the loop sound's.
|
||||
void Place(Actor &actor);
|
||||
// The kit stops (the model changed): no new shards, the loop fades.
|
||||
void Stop(Actor &actor);
|
||||
// Stops the kit and frees every shard (the actor goes).
|
||||
void Drop(Actor &actor);
|
||||
// Stops the loop sound (the scene hid; its update stops). It loops again
|
||||
// at the next update.
|
||||
void Silence(Actor &actor);
|
||||
} // namespace Areas
|
||||
|
||||
// The unit's movement animations (model/SceneLocomotion.cpp): an actor with
|
||||
// the mover on picks and plays them as the client's unit does from its own
|
||||
// mover. Each is a no-op for an actor without the mover, without a loaded
|
||||
// model, or dead (its own animation a death animation).
|
||||
namespace Locomotion {
|
||||
// The movement messages a unit animates (AnimateMoveOpcode 0x0060E480), by
|
||||
// their 1.12 opcodes.
|
||||
enum class Move : uint32_t {
|
||||
StartForward = 0xB5,
|
||||
StartBackward = 0xB6,
|
||||
Stop = 0xB7,
|
||||
StartStrafeLeft = 0xB8,
|
||||
StartStrafeRight = 0xB9,
|
||||
StopStrafe = 0xBA,
|
||||
Jump = 0xBB,
|
||||
StartTurnLeft = 0xBC,
|
||||
StartTurnRight = 0xBD,
|
||||
StopTurn = 0xBE,
|
||||
RunMode = 0xC2,
|
||||
WalkMode = 0xC3,
|
||||
TeleportAck = 0xC7,
|
||||
StartSwim = 0xCA,
|
||||
StopSwim = 0xCB,
|
||||
KnockBack = 0xF0, // CMSG_MOVE_KNOCK_BACK_ACK, after the launch
|
||||
};
|
||||
// A movement message for a command the mover took, or a change the actor
|
||||
// made (ProcessLocalMoveEvent 0x0060E0A0 -> AnimateMoveOpcode).
|
||||
void OnMove(Actor &actor, Move move);
|
||||
// The mover's substep hook (Mover::Body::stepped), context the actor: an
|
||||
// arrival at a spline's end, a landing, a fall or a swim's end.
|
||||
void OnStep(void *actor, uint32_t oldFlags, bool jumped, bool arrived);
|
||||
// The body pass's shuffle (the tail of CGUnit_C::Update 0x00607ED0), `turn`
|
||||
// the turn the body made this frame.
|
||||
void OnBodyTurn(Actor &actor, float turn);
|
||||
// The actor's own animation ended: plays what follows it
|
||||
// (PlayFollowOnAnimation 0x005FC3F0) and returns true; false when the chain
|
||||
// does not run, and the caller replays it.
|
||||
bool FollowOn(Actor &actor);
|
||||
// Picks again (UpdateBaseAnimation 0x005FD9E0).
|
||||
void Repick(Actor &actor);
|
||||
} // namespace Locomotion
|
||||
|
||||
// The actor model's animation events (model/SceneEvents.cpp).
|
||||
namespace Events {
|
||||
// The model event callback (Offsets::FUN_M2MODEL_SET_EVENT_CALLBACK), user
|
||||
// data the actor: missile releases for Casts, and the event sounds.
|
||||
void __stdcall OnModelEvent(uint32_t tag, uint32_t data, const float *point, uint32_t unknown,
|
||||
uint32_t userLow, uint32_t userHigh);
|
||||
} // namespace Events
|
||||
|
||||
// The scene's WMO (SetWorldModel; model/SceneWorld.cpp).
|
||||
namespace World {
|
||||
struct Backdrop;
|
||||
// Whether `path` names a WMO (ends in ".wmo"), which only the scene shows.
|
||||
bool IsWmoPath(const char *path);
|
||||
// Null when the engine cannot read `path` safely: it is not a WMO, a file
|
||||
// does not open, the root's chunks are not in the engine's order or it has
|
||||
// more groups than the engine holds.
|
||||
// `mapID` is the map whose light the outdoor water takes (Light.dbc at the
|
||||
// scene's camera), or kNoMap for the world's light where the player is.
|
||||
constexpr uint32_t kNoMap = 0xFFFFFFFFu;
|
||||
Backdrop *Load(const char *path, const float (&placement)[16], uint32_t doodadSet, uint32_t mapID);
|
||||
// The single WMO of a WMO-only map at the map's own placement; null for any
|
||||
// other map.
|
||||
Backdrop *LoadMap(uint32_t mapID);
|
||||
// SetWorldModel's placement: at `position`, turned by `yaw` about z.
|
||||
void Placement(const float (&position)[3], float yaw, float (&out)[16]);
|
||||
void Free(Backdrop *backdrop);
|
||||
// From the scene's update, before the scene advances: makes the doodads of
|
||||
// each group parsed since, puts the ready ones on the animate list and
|
||||
// moves their sounds with the camera, and moves the camera's WMO fog (the
|
||||
// MFOG records near the camera, faded in and out as the world fades them).
|
||||
// `sounds` is SetPlayWorldModelSounds.
|
||||
void Update(Backdrop &backdrop, void *frame, bool sounds, float elapsed);
|
||||
// Drops the doodads' loop sounds (the scene hid).
|
||||
void Silence(Backdrop &backdrop);
|
||||
// From the paint, in place of its depth clear: the world's frame clear and,
|
||||
// with the camera outside the WMO's interior groups and dry, the world's sky
|
||||
// (the dome, the sun and moons, the stars, the clouds and the skyboxes) in
|
||||
// the device's view, lit by the backdrop map's light at the camera.
|
||||
void ClearAndDrawSky(const Backdrop &backdrop, void *frame, const float (&eye)[3]);
|
||||
// From the paint, after the animate pass: the WMO in the device's view.
|
||||
void Draw(Backdrop &backdrop, void *frame, const float (&eye)[3]);
|
||||
// From the paint, between the scene's opaque and translucent lists: the
|
||||
// liquids of the WMO's groups (the world draws them between its model lists).
|
||||
void DrawLiquids(Backdrop &backdrop, void *frame, const float (&eye)[3]);
|
||||
// Whether the camera is in one of the WMO's liquids: the world then draws the
|
||||
// liquids after the translucent models.
|
||||
bool CameraWet(const Backdrop &backdrop);
|
||||
// How the world lights a unit that stands in a WMO, kept per actor: the
|
||||
// colour of the face under it, or the sun, and the WMO's lights near it.
|
||||
struct Ambience;
|
||||
Ambience *NewAmbience();
|
||||
void FreeAmbience(Ambience *ambience);
|
||||
// From the scene's update: places the model in the WMO again when it moved,
|
||||
// or when more of the WMO has loaded since.
|
||||
void Settle(const Backdrop &backdrop, Ambience &ambience, void *frame, void *model, float elapsed);
|
||||
// The actor's lighting slots: the frame's light and fog without a WMO, or
|
||||
// before the model is placed in this one.
|
||||
void ActorLight(const Backdrop *backdrop, const Ambience &ambience, void *frame, void *record);
|
||||
// The TerrainType id of the WMO face under the actor, or -1 (no face, no
|
||||
// WMO, or not placed in this one).
|
||||
int Ground(const Backdrop *backdrop, const Ambience &ambience);
|
||||
// The nearest face of the loaded groups that the segment from `start` to
|
||||
// `end` (scene space) crosses, of the faces the world's line of sight tests,
|
||||
// or with `camera` the faces its camera collides with: its fraction along the
|
||||
// segment and its unit normal, turned to face `start`. False on a miss or
|
||||
// before the WMO has loaded.
|
||||
bool Trace(const Backdrop &backdrop, const float (&start)[3], const float (&end)[3], bool camera,
|
||||
float &fraction, float (&normal)[3]);
|
||||
// The liquid a point (scene space) is in: the type (0 water, 1 ocean, 2
|
||||
// magma, 3 slime) and the surface's z, FLT_MAX for a group that is liquid
|
||||
// throughout. False when the point is not under a liquid surface of the
|
||||
// group below it, or before the WMO has loaded.
|
||||
bool LiquidAt(const Backdrop &backdrop, const float (&point)[3], uint32_t &type, float &surface);
|
||||
// The world query (0x006721B0) a scene body's sweeps make, for
|
||||
// Contact::Facets: the WMO's collision faces in a box (scene space), then
|
||||
// its doodads' collision meshes. False without a WMO, before it has loaded,
|
||||
// or while a group or doodad in the box is still loading, so a body there
|
||||
// does not move.
|
||||
bool QueryFacets(void *backdrop, const float (&box)[6], std::vector<Contact::Facet> &out);
|
||||
// The same query with a swimmer's class (0x30000), for Contact::Facets: the
|
||||
// water and ocean surfaces of the WMO's liquid groups in a box (scene space),
|
||||
// as their tiles' triangles. False without a WMO or before it has loaded.
|
||||
bool QueryLiquidFacets(void *backdrop, const float (&box)[6], std::vector<Contact::Facet> &out);
|
||||
// Sweeps a body by `move` to its first contact with the WMO's collision
|
||||
// faces, as the client's mover sweeps a unit's volume (CMovement::SweepVolume
|
||||
// 0x00632BA0, SceneContact.h): a box `radius` from `feet` on each side and
|
||||
// `height` tall over a downward point at `feet`. `end` is where the feet
|
||||
// stop; true when a face was met, `normal` the facet's plane normal (scene
|
||||
// space). The feet stay put before the WMO has loaded.
|
||||
bool Sweep(const Backdrop &backdrop, const float (&feet)[3], float radius, float height, const float (&move)[3],
|
||||
float (&end)[3], float (&normal)[3]);
|
||||
// From the paint, after Draw: a texture laid over the WMO's faces in `box`
|
||||
// (scene space), as the world lays a decal on its ground.
|
||||
void DrawDecal(const Backdrop &backdrop, const float (&box)[6], uint32_t argb, const float (&m)[16], float fade,
|
||||
uint32_t mask, const float (&eye)[3]);
|
||||
// The same decal in a scene without a WMO: laid on the flat ground at
|
||||
// height `z`, one quad over the box.
|
||||
void DrawDecalOnPlane(const float (&box)[6], uint32_t argb, const float (&m)[16], float fade, float z,
|
||||
const float (&eye)[3]);
|
||||
// The scene's fog on the device, for a decal at `point` that the world draws
|
||||
// in its own fog: the WMO's, or the frame's without one. Inside a state push.
|
||||
void CommitDecalFog(const Backdrop *backdrop, void *frame, const float (&point)[3], const float (&eye)[3]);
|
||||
} // namespace World
|
||||
|
||||
// The scene's WMO, or null.
|
||||
World::Backdrop *WorldOf(void *sceneFrame);
|
||||
|
||||
// The scene light's ambient colour {r, g, b}: the colour an actor's highlight
|
||||
// takes, as a unit's takes the world light state's ambient.
|
||||
const float *LightAmbient(void *sceneFrame);
|
||||
|
||||
// The scene clock in milliseconds (the M2 scene's): the actors' movement runs
|
||||
// on it as a unit's runs on the OS clock, so it stops while the scene is
|
||||
// paused or hidden, with the animations.
|
||||
uint32_t ClockMs(void *sceneFrame);
|
||||
// Whether SetPaused holds the scene.
|
||||
bool IsPaused(void *sceneFrame);
|
||||
// The radius the actor's ground-target reticle draws at: its own, or the
|
||||
// world's default for 0 (Offsets::RETICLE_DEFAULT_RADIUS).
|
||||
float GroundReticleRadius(const Actor &actor);
|
||||
|
||||
// The actor the scene's sounds are heard at (SetSoundListenerActor), or null.
|
||||
Actor *SoundListener(void *sceneFrame);
|
||||
|
||||
// The scene's sounds (model/SceneSound.cpp), heard from the scene camera:
|
||||
// each plays where its scene point lies from the camera, turned into the
|
||||
// sound listener's frame, so the engine's cutoff, falloff and panning apply
|
||||
// as they would to a unit at that distance.
|
||||
namespace Sounds {
|
||||
// A scene point as a point of the listener's (the engine's axes), heard at
|
||||
// the scene's listener actor while SoundListenerAtCharacter is on, else at
|
||||
// the scene camera.
|
||||
void Heard(void *sceneFrame, const float (&point)[3], float (&out)[3]);
|
||||
// Sound::Play::PlayKitIDAt, PlayKitLoopedAt and Move at a scene point.
|
||||
void *Play(void *sceneFrame, int soundKitID, const float (&point)[3]);
|
||||
void *Loop(void *sceneFrame, int soundKitID, const float (&point)[3]);
|
||||
void Move(void *sceneFrame, void *sound, const float (&point)[3]);
|
||||
} // namespace Sounds
|
||||
|
||||
// The scene camera (model/Scene.cpp).
|
||||
struct View {
|
||||
float eye[3];
|
||||
float forward[3];
|
||||
float up[3];
|
||||
float farClip;
|
||||
};
|
||||
View CameraView(void *sceneFrame);
|
||||
|
||||
// The scene's ground terrain (SetGroundTerrain): a TerrainType id, or -1.
|
||||
int GroundTerrain(void *sceneFrame);
|
||||
|
||||
// Registers the actor class in the current Lua state.
|
||||
void RegisterActorClass();
|
||||
|
||||
// The methods' argument checks, for the frame and its actors. True when the
|
||||
// `count` arguments from `first` are numbers; else the engine's usage error,
|
||||
// "Usage: <owner>:<usage>".
|
||||
bool NumberArgs(void *L, int first, int count, const char *owner, const char *usage);
|
||||
float FloatArg(void *L, int index);
|
||||
|
||||
} // namespace Model::Scene
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,308 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The area kit of a spell's ground area (actor:SetModelBySpellArea), as the
|
||||
// world's dynamic object starts it when it is created (CGDynamicObject_C,
|
||||
// 0x005D55C0) and stops it when it is deactivated or destroyed:
|
||||
//
|
||||
// - The kit is the visual's +0x34 SpellVisualKit. Its first procedure of
|
||||
// kind 9 starts the shard emitter (0x006ECE30 -> 0x006EB930): the shard
|
||||
// model is the type's file (the low byte of the bits of the procedure's
|
||||
// first value + 512.0, shifted right 14, into the seven paths at
|
||||
// 0x00870E24), and the rate is its second value, scaled by the spell
|
||||
// detail level (x0.33 at 0, x0.66 at 1).
|
||||
// - Each frame the emitter adds the frame's seconds times the rate and
|
||||
// spawns a shard for each whole 1.0 (0x006EBAD0). A shard picks a point of
|
||||
// the area (the radial draw is not square-rooted, so shards gather at the
|
||||
// centre) 16.67 yards up, traces up to it from just over the centre and
|
||||
// stops short of a ceiling, then traces 33.33 yards down to the floor
|
||||
// (0x006EBB64..0x006EBD20). It starts after 2 * a draw of 0..255 ms, plays
|
||||
// its animation once, and is freed when it ends (0x006EBEE0, 0x006EBE70).
|
||||
// Every fourth model event of the shards plays when it is a $SND
|
||||
// (0x006EBE30).
|
||||
// - The kit's sound loops on the area (0x005D569C), and stops with a 3.0
|
||||
// second fade (0x005D56D0).
|
||||
//
|
||||
// The world has no pause; a paused scene (or actor) holds its shards still
|
||||
// and silences the loop, which starts again when the pause ends.
|
||||
//
|
||||
// The random draws are the client's own (its random state 0x00882664). The
|
||||
// traces are the scene's WMO (World::Trace); a scene without a WMO has no
|
||||
// floor, so there its shards land at the area's height. The scene clock
|
||||
// times the shards, so they stop while the scene is paused.
|
||||
|
||||
#include "model/Scene.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "model/M2.h"
|
||||
#include "sound/Play.h"
|
||||
#include "time/Clock.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace Model::Scene::Areas {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kLoopStopFadeSec = 3.0f; // 0x005D56DD: Sound::Stop2's fade
|
||||
constexpr uint32_t kSND = 0x444E5324; // $SND, as 0x006EBE42 compares it
|
||||
uint32_t g_shardEvents = 0; // the shards' event count (the world's 0x00CEE944)
|
||||
|
||||
float FromBits(uint32_t bits) {
|
||||
float value;
|
||||
std::memcpy(&value, &bits, sizeof value);
|
||||
return value;
|
||||
}
|
||||
|
||||
uint32_t BitsOf(float value) {
|
||||
uint32_t bits;
|
||||
std::memcpy(&bits, &value, sizeof bits);
|
||||
return bits;
|
||||
}
|
||||
|
||||
void *RandomState() { return reinterpret_cast<void *>(static_cast<uintptr_t>(Offsets::VAR_CLIENT_RANDOM)); }
|
||||
|
||||
// A draw in [0, 1): the client's random uint32 as the mantissa of a float in
|
||||
// [1, 2), less 1.0 (0x006EBB64..0x006EBB8B).
|
||||
float Draw() {
|
||||
using Random_t = uint32_t(__thiscall *)(void *state);
|
||||
const uint32_t r = reinterpret_cast<Random_t>(Offsets::FUN_RANDOM_UINT32)(RandomState());
|
||||
return FromBits((r & 0x007FFFFFu) | 0x3F800000u) - Game::Read<float>(Offsets::VAR_ONE);
|
||||
}
|
||||
|
||||
// The client's random unit vector, x and y (0x006EBB83).
|
||||
void Direction(float (&out)[4]) {
|
||||
using Vector_t = const float *(__fastcall *)(float *out, void *state);
|
||||
reinterpret_cast<Vector_t>(Offsets::FUN_RANDOM_UNIT_VECTOR2)(out, RandomState());
|
||||
}
|
||||
|
||||
// The rate at the spell detail level (0x006EB95E..0x006EB98F).
|
||||
float DetailRate(float rate) {
|
||||
const auto *record = Game::Read<const uint8_t *>(Offsets::VAR_SPELL_EFFECT_LEVEL_RECORD);
|
||||
if (record == nullptr)
|
||||
return rate;
|
||||
const auto level = Game::Read<int32_t>(record, Offsets::OFF_SPELL_EFFECT_LEVEL);
|
||||
if (level == 0)
|
||||
return rate * Game::Read<float>(Offsets::VAR_SPELL_EFFECT_LOW_SCALE);
|
||||
if (level == 1)
|
||||
return rate * Game::Read<float>(Offsets::VAR_SPELL_EFFECT_MEDIUM_SCALE);
|
||||
return rate;
|
||||
}
|
||||
|
||||
// The shards' event callback (0x006EBE30): every fourth event, a $SND plays
|
||||
// its payload at the event's point.
|
||||
void __stdcall OnShardEvent(uint32_t tag, uint32_t data, const float *point, uint32_t /*unknown*/,
|
||||
uint32_t userLow, uint32_t /*userHigh*/) {
|
||||
++g_shardEvents;
|
||||
if ((g_shardEvents & 3u) != 0 || tag != kSND)
|
||||
return;
|
||||
auto *shard = reinterpret_cast<AreaShard *>(static_cast<uintptr_t>(userLow));
|
||||
const float *at = point != nullptr ? point : shard->position;
|
||||
const float where[3] = {at[0], at[1], at[2]};
|
||||
Sounds::Play(shard->frame, static_cast<int32_t>(data), where);
|
||||
}
|
||||
|
||||
// The shards' done callback (0x006EBE70): a natural end (reason 0) frees the
|
||||
// shard, here at the next update.
|
||||
void __fastcall OnShardDone(void * /*model*/, uint32_t /*keyBone*/, uint32_t /*animID*/, uint32_t reason,
|
||||
uint32_t /*overshoot*/, uint32_t userLow, uint32_t /*userHigh*/) {
|
||||
if (reason == 0)
|
||||
reinterpret_cast<AreaShard *>(static_cast<uintptr_t>(userLow))->done = true;
|
||||
}
|
||||
|
||||
void FreeShard(AreaShard &shard) {
|
||||
M2::SetEventCallback(shard.model, nullptr, 0, 0);
|
||||
M2::SetAnimDoneCallback(shard.model, nullptr, 0, 0);
|
||||
ReleaseLit(shard.model, shard.light);
|
||||
shard.model = nullptr;
|
||||
}
|
||||
|
||||
// The area kit's start (0x005D55C0): the emitter of its first kind-9
|
||||
// procedure and its sound.
|
||||
void Start(Actor &actor) {
|
||||
AreaEmitter &emitter = actor.areaEmitter;
|
||||
emitter = AreaEmitter{};
|
||||
emitter.running = true;
|
||||
const uint8_t *kit =
|
||||
DBC::Record(Offsets::VAR_SPELLVISUALKIT_RECORDS, Offsets::VAR_SPELLVISUALKIT_COUNT,
|
||||
Game::Read<uint32_t>(actor.areaVisual, Offsets::OFF_SPELLVISUAL_AREA_KIT));
|
||||
if (kit == nullptr)
|
||||
return;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (Game::Read<int32_t>(kit, Offsets::OFF_SPELLVISUALKIT_PROC_KIND + i * 4) !=
|
||||
Offsets::SPELLVISUALKIT_PROC_AREA_SHARDS)
|
||||
continue;
|
||||
// The engine reads the table with no bound; a type past it has no path.
|
||||
const uint8_t type = DecodeKitByte(Game::Read<float>(kit, Offsets::OFF_SPELLVISUALKIT_PROC_VALUE + i * 4));
|
||||
if (type < Offsets::AREA_SHARD_PATH_COUNT) {
|
||||
emitter.path = Game::Read<const char *>(Offsets::VAR_AREA_SHARD_PATHS + type * 4u);
|
||||
emitter.rate = DetailRate(Game::Read<float>(kit, Offsets::OFF_SPELLVISUALKIT_PROC_VALUE2 + i * 4));
|
||||
}
|
||||
break;
|
||||
}
|
||||
emitter.soundID = Game::Read<int32_t>(kit, Offsets::OFF_SPELLVISUALKIT_SOUND);
|
||||
}
|
||||
|
||||
// One shard (0x006EBB30..0x006EBDA9).
|
||||
void Spawn(Actor &actor) {
|
||||
void *frame = actor.SceneFrame();
|
||||
const float radius = actor.areaRadius;
|
||||
const float *centre = actor.position;
|
||||
const float u = Draw();
|
||||
float direction[4] = {};
|
||||
Direction(direction);
|
||||
const float us = u * direction[1];
|
||||
const float high[3] = {centre[0] + u * direction[0] * radius, centre[1] + us * radius,
|
||||
centre[2] + Game::Read<float>(Offsets::VAR_AREA_SHARD_DROP_HEIGHT)};
|
||||
const float low[3] = {centre[0], centre[1], centre[2] + Game::Read<float>(Offsets::VAR_ONE_SIXTH)};
|
||||
const World::Backdrop *world = WorldOf(frame);
|
||||
float fraction = 1.0f;
|
||||
float normal[3];
|
||||
float start[3] = {high[0], high[1], high[2]};
|
||||
// Short of a ceiling between the centre and the point up there.
|
||||
if (world != nullptr && World::Trace(*world, low, high, false, fraction, normal)) {
|
||||
const float share = fraction * Game::Read<float>(Offsets::VAR_AREA_SHARD_HIT_SHARE);
|
||||
for (int i = 0; i < 3; ++i)
|
||||
start[i] = low[i] + (high[i] - low[i]) * share;
|
||||
}
|
||||
auto shard = std::make_unique<AreaShard>();
|
||||
shard->frame = frame;
|
||||
const float below[3] = {start[0], start[1], start[2] - Game::Read<float>(Offsets::VAR_AREA_SHARD_FALL)};
|
||||
fraction = 1.0f;
|
||||
if (world == nullptr) {
|
||||
shard->position[0] = start[0];
|
||||
shard->position[1] = start[1];
|
||||
shard->position[2] = centre[2];
|
||||
} else if (World::Trace(*world, start, below, false, fraction, normal)) {
|
||||
for (int i = 0; i < 3; ++i)
|
||||
shard->position[i] = start[i] + (below[i] - start[i]) * fraction;
|
||||
} else {
|
||||
std::memcpy(shard->position, centre, sizeof shard->position);
|
||||
}
|
||||
void *model = M2::CreateModel(Model::Pane::Scene(frame), actor.areaEmitter.path);
|
||||
if (model == nullptr)
|
||||
return;
|
||||
shard->model = model;
|
||||
LightAsEntity(model, shard->light, frame);
|
||||
const auto user = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(shard.get()));
|
||||
M2::SetEventCallback(model, reinterpret_cast<const void *>(&OnShardEvent), user, 0);
|
||||
M2::SetAnimDoneCallback(model, reinterpret_cast<const void *>(&OnShardDone), user, 0);
|
||||
// The wait: 2 * (the bits of draw * 255.0 + 512.0, rounded once, >> 14).
|
||||
const auto wait = static_cast<float>(static_cast<double>(Draw()) * Game::Read<float>(Offsets::VAR_TWO_FIFTY_FIVE) +
|
||||
Game::Read<float>(Offsets::VAR_FIVE_TWELVE));
|
||||
shard->startMs = ClockMs(frame) + 2u * static_cast<uint8_t>(BitsOf(wait) >> 14);
|
||||
actor.areaShards.push_back(std::move(shard));
|
||||
}
|
||||
|
||||
void StopLoop(AreaEmitter &emitter, float fadeSec) {
|
||||
if (emitter.loopSound != nullptr) {
|
||||
Sound::Play::Stop(emitter.loopSound, fadeSec);
|
||||
emitter.loopSound = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Model::Scene::Areas
|
||||
|
||||
namespace Model::Scene {
|
||||
|
||||
uint8_t DecodeKitByte(float value) {
|
||||
uint32_t bits;
|
||||
const float sum = value + Game::Read<float>(Offsets::VAR_FIVE_TWELVE);
|
||||
std::memcpy(&bits, &sum, sizeof bits);
|
||||
return static_cast<uint8_t>(bits >> 14);
|
||||
}
|
||||
|
||||
} // namespace Model::Scene
|
||||
|
||||
namespace Model::Scene::Areas {
|
||||
|
||||
void Stop(Actor &actor) {
|
||||
StopLoop(actor.areaEmitter, kLoopStopFadeSec);
|
||||
actor.areaEmitter = AreaEmitter{};
|
||||
}
|
||||
|
||||
void Update(Actor &actor, float sceneSec) {
|
||||
AreaEmitter &emitter = actor.areaEmitter;
|
||||
const bool wanted = actor.areaEffect != nullptr && actor.shown;
|
||||
if (wanted && !emitter.running)
|
||||
Start(actor);
|
||||
else if (!wanted && emitter.running)
|
||||
Stop(actor);
|
||||
if (emitter.running) {
|
||||
const bool held = IsPaused(actor.SceneFrame()) || actor.paused || actor.globalPaused;
|
||||
if (held)
|
||||
StopLoop(emitter, 0.0f);
|
||||
else if (emitter.soundID > 0 && emitter.loopSound == nullptr)
|
||||
emitter.loopSound = Sounds::Loop(actor.SceneFrame(), emitter.soundID, actor.position);
|
||||
if (emitter.path != nullptr) {
|
||||
emitter.accumulator += static_cast<double>(sceneSec) * emitter.rate;
|
||||
while (emitter.accumulator >= 1.0) {
|
||||
Spawn(actor);
|
||||
emitter.accumulator -= 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < actor.areaShards.size();) {
|
||||
AreaShard &shard = *actor.areaShards[i];
|
||||
if (!shard.done) {
|
||||
SettleLight(shard.model, shard.light.get(), sceneSec);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
FreeShard(shard);
|
||||
actor.areaShards.erase(actor.areaShards.begin() + static_cast<ptrdiff_t>(i));
|
||||
}
|
||||
}
|
||||
|
||||
// The shard's draw callback (0x006EBEE0): nothing before its start, then the
|
||||
// first draw plays its animation once, and each draw shows and animates it.
|
||||
void Link(Actor &actor) {
|
||||
const uint32_t now = ClockMs(actor.SceneFrame());
|
||||
for (const auto &shard : actor.areaShards) {
|
||||
if (shard->done || !M2::IsReady(shard->model, 0, 1))
|
||||
continue;
|
||||
if (!shard->started) {
|
||||
if (!Time::Clock::Reached(now, shard->startMs))
|
||||
continue;
|
||||
shard->started = true;
|
||||
M2::PlayAnimation(shard->model, -1, 0, -1, 0, 1.0f, 1, 1);
|
||||
}
|
||||
M2::SetVisible(shard->model, true);
|
||||
M2::SetAnimated(shard->model, true);
|
||||
}
|
||||
}
|
||||
|
||||
void Place(Actor &actor) {
|
||||
const float axisZ[3] = {0.0f, 0.0f, 1.0f};
|
||||
const float scales[3] = {1.0f, 1.0f, 1.0f};
|
||||
for (const auto &shard : actor.areaShards)
|
||||
if (shard->started)
|
||||
M2::SetBaseTRS(shard->model, shard->position, 0.0f, axisZ, scales);
|
||||
if (actor.areaEmitter.loopSound != nullptr)
|
||||
Sounds::Move(actor.SceneFrame(), actor.areaEmitter.loopSound, actor.position);
|
||||
}
|
||||
|
||||
void Drop(Actor &actor) {
|
||||
Stop(actor);
|
||||
for (const auto &shard : actor.areaShards)
|
||||
FreeShard(*shard);
|
||||
actor.areaShards.clear();
|
||||
}
|
||||
|
||||
void Silence(Actor &actor) { StopLoop(actor.areaEmitter, 0.0f); }
|
||||
|
||||
} // namespace Model::Scene::Areas
|
||||
@@ -0,0 +1,261 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The chain effects of a scene's casts: the bolts Chain Lightning draws from
|
||||
// its caster to its target. The world makes them from a kit procedure of
|
||||
// kind 0 or 12 (0x0060D7FC through the byte table 0x0060DC20 -> 0x006ECB90 ->
|
||||
// 0x006ECBD0): the procedure's values give the SpellChainEffects row, the
|
||||
// strand count (at most 3) and a persistent flag; each strand is a
|
||||
// LightningObject whose hops each get a bolt of the client's lightning
|
||||
// renderer (Lightning.cpp). Each frame the hop update (0x006EC460) shows a
|
||||
// hop's bolt between its two ends while the hop lives, creating it with the
|
||||
// row's shape the first time and moving it after, and destroys it at its
|
||||
// end; the renderer then updates and draws every bolt (0x006ECA20,
|
||||
// 0x006ECB20).
|
||||
//
|
||||
// A scene owns a renderer of its own and drives it through the engine's
|
||||
// functions. A scene cast has one target, so a chain here has one hop, from
|
||||
// the caster to the target, living the row's life (+0x14) from the cast; a
|
||||
// script that casts again from each target draws a leaping chain. The ends
|
||||
// are the world's: the caster's $CSL event point, else its position raised
|
||||
// by 0.75 of its height (0x006EC6F0); the target's attachment that the
|
||||
// spell visual's +0x24 names, else its position (0x006EC780). A hidden end
|
||||
// hides the bolt, as a missing object does. The scene clock times the hops,
|
||||
// so a paused scene holds them. A persistent chain (a channel's) is drawn
|
||||
// for the row's life too, as a scene cast has no channel to end it.
|
||||
|
||||
#include "model/Scene.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "model/M2.h"
|
||||
#include "texture/Load.h"
|
||||
#include "time/Clock.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Model::Scene::Chains {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kCSL = 0x4C534324; // $CSL, as 0x006EC718 pushes it
|
||||
|
||||
struct Hop {
|
||||
Actor *from = nullptr;
|
||||
Actor *to = nullptr;
|
||||
uint32_t attachmentIndex = 0; // the spell visual's +0x24
|
||||
uint32_t endMs = 0; // the scene clock at which it goes
|
||||
const uint8_t *record = nullptr;
|
||||
void *texture = nullptr; // the scene's reference to the row's texture
|
||||
int handle = -1; // the renderer's bolt, or -1
|
||||
};
|
||||
|
||||
struct State {
|
||||
alignas(8) uint8_t renderer[Offsets::LIGHTNING_RENDERER_SIZE] = {};
|
||||
std::vector<Hop> hops;
|
||||
std::unordered_map<uint32_t, void *> textures; // by chain id, one reference each
|
||||
};
|
||||
|
||||
std::unordered_map<void *, std::unique_ptr<State>> g_states;
|
||||
|
||||
State *Find(void *frame) {
|
||||
const auto it = g_states.find(frame);
|
||||
return it != g_states.end() ? it->second.get() : nullptr;
|
||||
}
|
||||
|
||||
State &Make(void *frame) {
|
||||
std::unique_ptr<State> &state = g_states[frame];
|
||||
if (state == nullptr) {
|
||||
state = std::make_unique<State>();
|
||||
reinterpret_cast<void(__thiscall *)(void *)>(Offsets::FUN_LIGHTNING_INIT)(state->renderer);
|
||||
}
|
||||
return *state;
|
||||
}
|
||||
|
||||
int CreateBolt(State &state, const float (&start)[3], const float (&end)[3], const uint8_t *record, void *texture) {
|
||||
using Create_t = int(__thiscall *)(void *renderer, const float *start, const float *end, float segment,
|
||||
float width, uint32_t argb, float jitter, float period, float extra,
|
||||
void *texture, void *callback, void *argument);
|
||||
const float life = static_cast<float>(Game::Read<int32_t>(record, Offsets::OFF_SPELLCHAINEFFECTS_LIFE)) *
|
||||
Game::Read<float>(Offsets::VAR_ONE_THOUSANDTH);
|
||||
return reinterpret_cast<Create_t>(Offsets::FUN_LIGHTNING_CREATE_BOLT)(
|
||||
state.renderer, start, end, Game::Read<float>(record, Offsets::OFF_SPELLCHAINEFFECTS_SEGMENT),
|
||||
Game::Read<float>(record, Offsets::OFF_SPELLCHAINEFFECTS_WIDTH), Offsets::CHAIN_BOLT_COLOR,
|
||||
Game::Read<float>(record, Offsets::OFF_SPELLCHAINEFFECTS_JITTER),
|
||||
Game::Read<float>(record, Offsets::OFF_SPELLCHAINEFFECTS_PERIOD), life, texture, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void ShowBolt(State &state, int handle, bool on) {
|
||||
reinterpret_cast<void(__thiscall *)(void *, int, int)>(Offsets::FUN_LIGHTNING_SHOW_BOLT)(state.renderer, handle,
|
||||
on ? 1 : 0);
|
||||
}
|
||||
|
||||
void DestroyBolt(State &state, int handle) {
|
||||
if (handle != -1)
|
||||
reinterpret_cast<void(__thiscall *)(void *, int)>(Offsets::FUN_LIGHTNING_DESTROY_BOLT)(state.renderer,
|
||||
handle);
|
||||
}
|
||||
|
||||
// The caster's end (0x006EC6F0).
|
||||
bool CasterEnd(const Actor &actor, float (&out)[3]) {
|
||||
std::memcpy(out, actor.position, sizeof out);
|
||||
void *model = actor.model;
|
||||
if (model == nullptr || !M2::IsLoaded(model, 0, 0))
|
||||
return true;
|
||||
if (M2::HasEvent(model, kCSL)) {
|
||||
M2::GetEventPosition(model, kCSL, out);
|
||||
return true;
|
||||
}
|
||||
float m[16];
|
||||
M2::GetBaseTransform(model, m);
|
||||
float box[6];
|
||||
M2::GetBoundingBox(model, box);
|
||||
const float scale = std::sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]);
|
||||
const uint32_t bits = Offsets::CHAIN_CASTER_HEIGHT;
|
||||
float share;
|
||||
std::memcpy(&share, &bits, sizeof share);
|
||||
out[2] += static_cast<float>(static_cast<double>(scale) * (box[5] - box[2]) * share);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The target's end (0x006EC780): the attachment the visual names, else the
|
||||
// position. The engine reads the table with no bound; past it, the position.
|
||||
void TargetEnd(const Actor &actor, uint32_t index, float (&out)[3]) {
|
||||
std::memcpy(out, actor.position, sizeof out);
|
||||
void *model = actor.model;
|
||||
if (model == nullptr || !M2::IsLoaded(model, 0, 0) || index >= Offsets::SPELL_VISUAL_ATTACHMENT_COUNT)
|
||||
return;
|
||||
const auto id = Game::Read<uint32_t>(static_cast<uintptr_t>(Offsets::VAR_SPELL_VISUAL_ATTACHMENTS) + index * 4);
|
||||
if (M2::HasAttachment(model, id))
|
||||
M2::GetAttachmentPosition(model, id, out);
|
||||
}
|
||||
|
||||
void *ChainTexture(State &state, uint32_t chainID, const uint8_t *record) {
|
||||
void *&texture = state.textures[chainID];
|
||||
if (texture == nullptr) {
|
||||
const char *path = Game::Read<const char *>(record, Offsets::OFF_SPELLCHAINEFFECTS_TEXTURE);
|
||||
if (path != nullptr && *path != '\0')
|
||||
texture = ::Texture::LoadByPath(path);
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Start(Actor &caster, Actor &target, const uint8_t *visual, const uint8_t *kit) {
|
||||
if (kit == nullptr || &target == &caster)
|
||||
return;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const auto kind = Game::Read<int32_t>(kit, Offsets::OFF_SPELLVISUALKIT_PROC_KIND + i * 4);
|
||||
if (kind != Offsets::SPELLVISUALKIT_PROC_CHAIN && kind != Offsets::SPELLVISUALKIT_PROC_CHAIN_ALT)
|
||||
continue;
|
||||
const uint8_t chainID = DecodeKitByte(Game::Read<float>(kit, Offsets::OFF_SPELLVISUALKIT_PROC_VALUE + i * 4));
|
||||
uint32_t strands = DecodeKitByte(Game::Read<float>(kit, Offsets::OFF_SPELLVISUALKIT_PROC_VALUE2 + i * 4));
|
||||
const uint8_t *record =
|
||||
DBC::Record(Offsets::VAR_SPELLCHAINEFFECTS_RECORDS, Offsets::VAR_SPELLCHAINEFFECTS_COUNT, chainID);
|
||||
if (record == nullptr || strands == 0)
|
||||
continue;
|
||||
if (strands > Offsets::CHAIN_MAX_STRANDS)
|
||||
strands = Offsets::CHAIN_MAX_STRANDS;
|
||||
void *frame = caster.SceneFrame();
|
||||
State &state = Make(frame);
|
||||
void *texture = ChainTexture(state, chainID, record);
|
||||
const uint32_t now = ClockMs(frame);
|
||||
const auto life = static_cast<uint32_t>(Game::Read<int32_t>(record, Offsets::OFF_SPELLCHAINEFFECTS_LIFE));
|
||||
for (uint32_t s = 0; s < strands; ++s) {
|
||||
Hop hop;
|
||||
hop.from = &caster;
|
||||
hop.to = ⌖
|
||||
hop.attachmentIndex = Game::Read<uint32_t>(visual, Offsets::OFF_SPELLVISUAL_MISSILE_ATTACHMENT);
|
||||
hop.endMs = now + life;
|
||||
hop.record = record;
|
||||
hop.texture = texture;
|
||||
state.hops.push_back(hop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The hop update (0x006EC460) for each hop, then the renderer's update.
|
||||
void Update(void *frame, float sceneSec) {
|
||||
State *state = Find(frame);
|
||||
if (state == nullptr)
|
||||
return;
|
||||
const uint32_t now = ClockMs(frame);
|
||||
for (size_t i = 0; i < state->hops.size();) {
|
||||
Hop &hop = state->hops[i];
|
||||
if (Time::Clock::Reached(now, hop.endMs)) {
|
||||
DestroyBolt(*state, hop.handle);
|
||||
state->hops.erase(state->hops.begin() + static_cast<ptrdiff_t>(i));
|
||||
continue;
|
||||
}
|
||||
if (!hop.from->shown || !hop.to->shown) {
|
||||
if (hop.handle != -1)
|
||||
ShowBolt(*state, hop.handle, false);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
float start[3];
|
||||
float end[3];
|
||||
CasterEnd(*hop.from, start);
|
||||
TargetEnd(*hop.to, hop.attachmentIndex, end);
|
||||
if (hop.handle == -1)
|
||||
hop.handle = CreateBolt(*state, start, end, hop.record, hop.texture);
|
||||
else
|
||||
reinterpret_cast<void(__thiscall *)(void *, int, const float *, const float *)>(
|
||||
Offsets::FUN_LIGHTNING_MOVE_BOLT)(state->renderer, hop.handle, start, end);
|
||||
ShowBolt(*state, hop.handle, true);
|
||||
++i;
|
||||
}
|
||||
reinterpret_cast<void(__thiscall *)(void *, float)>(Offsets::FUN_LIGHTNING_UPDATE)(state->renderer, sceneSec);
|
||||
}
|
||||
|
||||
void Render(void *frame, const float (&eye)[3]) {
|
||||
State *state = Find(frame);
|
||||
if (state == nullptr || state->hops.empty())
|
||||
return;
|
||||
reinterpret_cast<void(__thiscall *)(void *, const float *)>(Offsets::FUN_LIGHTNING_RENDER)(state->renderer, eye);
|
||||
}
|
||||
|
||||
void Forget(Actor &actor) {
|
||||
State *state = Find(actor.SceneFrame());
|
||||
if (state == nullptr)
|
||||
return;
|
||||
for (size_t i = 0; i < state->hops.size();) {
|
||||
Hop &hop = state->hops[i];
|
||||
if (hop.from != &actor && hop.to != &actor) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
DestroyBolt(*state, hop.handle);
|
||||
state->hops.erase(state->hops.begin() + static_cast<ptrdiff_t>(i));
|
||||
}
|
||||
}
|
||||
|
||||
void Destroy(void *frame) {
|
||||
const auto it = g_states.find(frame);
|
||||
if (it == g_states.end())
|
||||
return;
|
||||
State &state = *it->second;
|
||||
reinterpret_cast<void(__thiscall *)(void *)>(Offsets::FUN_LIGHTNING_DESTROY)(state.renderer);
|
||||
for (const auto &[id, texture] : state.textures)
|
||||
if (texture != nullptr)
|
||||
::Texture::ReleaseHandle(texture);
|
||||
g_states.erase(it);
|
||||
}
|
||||
|
||||
} // namespace Model::Scene::Chains
|
||||
@@ -0,0 +1,432 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The client mover's contact sweep, function by function (see SceneContact.h).
|
||||
//
|
||||
// Precision: the client runs these on the x87 at 53 bits and stores floats,
|
||||
// so each sum here is a double and each store a float cast, in the operand
|
||||
// order of the disassembly (benilla-re systems/collision/contact.md has the
|
||||
// instruction-level notes; its crates/collision/src/resolve_clip.rs is the
|
||||
// difftested transcription this follows). Where that transcription and the
|
||||
// binary differ, the binary wins: 0x00632BA0 starts the best distance and
|
||||
// each face's cap at the length asked for, not at the 1/36 floor
|
||||
// (0x00632BC3, 0x00632E10); the degenerate-edge test of the prisms returns
|
||||
// only below 2^-20 (0x006325B5, 0x006330C6 `jnp`); 0x006329E0 divides at the
|
||||
// parallel bound; and the quad prism tests its side against the vertex
|
||||
// before the edge with the sum (z, x, y) (0x006330EA, 0x00633126).
|
||||
|
||||
#include "model/SceneContact.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "model/ScenePrecision.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cstddef>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <xmmintrin.h>
|
||||
|
||||
namespace Model::Scene::Contact {
|
||||
|
||||
namespace {
|
||||
|
||||
using Game::Live;
|
||||
using Precision::Bits;
|
||||
using Precision::D;
|
||||
|
||||
const Live<float> kSkin{Offsets::VAR_MOVER_SKIN}; // 1/720
|
||||
const Live<float> kAllBehind{Offsets::VAR_MOVER_ALL_BEHIND}; // -2^-20
|
||||
const Live<float> kDegenerate{Offsets::VAR_TWO_POW_MINUS_20};
|
||||
const Live<float> kThirtySixth{Offsets::VAR_ONE_THIRTY_SIXTH};
|
||||
const Live<float> kNegThirtySixth{Offsets::VAR_MINUS_ONE_THIRTY_SIXTH};
|
||||
const Live<float> kFacing{Offsets::VAR_MOVER_MINUS_FACING_EPSILON}; // -1e-5
|
||||
const float kGrowth = Bits(0x3E2AAAAB); // 1/6, the gather's growth
|
||||
const Live<float> kBevelOut{Offsets::VAR_MOVER_BEVEL_OUT}; // sin 61.6°
|
||||
const Live<float> kBevelDown{Offsets::VAR_MOVER_BEVEL_DOWN}; // cos 61.6°
|
||||
const Live<float> kWaist{Offsets::VAR_MOVER_WAIST}; // tan 61.6°
|
||||
constexpr float kFltMax = FLT_MAX;
|
||||
const float kTwelfth = Bits(0x3DAAAAAB); // 1/12, the point-over-facet margin
|
||||
const Live<float> kThreeQuarters{Offsets::VAR_THREE_QUARTERS}; // the swimmer's height
|
||||
|
||||
// The volume's faces as vertex indices (0x00631BE0's store block): the five
|
||||
// box faces as quads, in plane order, then the four bevels as triangles.
|
||||
constexpr uint8_t kFaces[32] = {1, 2, 6, 5, 3, 4, 8, 7, 2, 3, 7, 6, 4, 1, 5, 8,
|
||||
5, 6, 7, 8, 0, 1, 2, 0, 3, 4, 0, 2, 3, 0, 4, 1};
|
||||
|
||||
// A clip polygon (0xF4 bytes): up to 15 vertices, the plane each one came
|
||||
// from, and the count.
|
||||
struct Polygon {
|
||||
float v[15][3];
|
||||
int32_t tag[15];
|
||||
uint32_t count;
|
||||
};
|
||||
static_assert(sizeof(Polygon) == 0xF4 && offsetof(Polygon, tag) == 0xB4 && offsetof(Polygon, count) == 0xF0,
|
||||
"the client's clip polygon");
|
||||
static_assert(sizeof(Plane) == 0x10, "the client's plane");
|
||||
|
||||
struct Volume {
|
||||
Plane planes[9];
|
||||
float v[9][3];
|
||||
};
|
||||
|
||||
// ClipPolygonByPlane: keeps the inside of a plane, with a 1/720 band,
|
||||
// tagging the vertices it adds.
|
||||
void ClipByPlane(Polygon &poly, const Plane &pl, int32_t tag) {
|
||||
using Fn = void(__fastcall *)(const Plane *, Polygon *, int32_t);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_CLIP_POLYGON_BY_PLANE)(&pl, &poly, tag);
|
||||
}
|
||||
|
||||
// ClipPolygonByPlanes: by each plane in turn, false once empty.
|
||||
bool ClipByPlanes(Polygon &poly, const Plane *planes, int count) {
|
||||
using Fn = int(__fastcall *)(const Plane *, uint32_t, Polygon *);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_CLIP_POLYGON_BY_PLANES)(planes, static_cast<uint32_t>(count), &poly) != 0;
|
||||
}
|
||||
|
||||
// ClipPolygonByPlanesExcept: by every volume plane but one.
|
||||
bool ClipByPlanesExcept(Polygon &poly, const Plane (&planes)[9], int skip) {
|
||||
using Fn = int(__fastcall *)(const Plane *, uint32_t, uint32_t, Polygon *);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_CLIP_POLYGON_BY_PLANES_EXCEPT)(planes, 9, static_cast<uint32_t>(skip),
|
||||
&poly) != 0;
|
||||
}
|
||||
|
||||
// PlaneFromTriangle: the normalized plane through three points, normal
|
||||
// (p1 - p0) × (p2 - p0).
|
||||
Plane PlaneFromPoints(const float (&p0)[3], const float (&p1)[3], const float (&p2)[3]) {
|
||||
using Fn = void(__fastcall *)(Plane *, int, const float *, const float *, const float *);
|
||||
Plane pl;
|
||||
reinterpret_cast<Fn>(Offsets::FUN_PLANE_FROM_TRIANGLE)(&pl, 0, p0, p1, p2);
|
||||
return pl;
|
||||
}
|
||||
|
||||
// One edge plane of a face swept along `s` (0x00632460 / 0x00632F80): the
|
||||
// plane through the edge a→b and a + s, turned so that `other`, a face
|
||||
// vertex off the edge, is not on its positive side. False for an edge too
|
||||
// short or parallel to `s`.
|
||||
bool EdgePlane(const float (&a)[3], const float (&b)[3], const float (&other)[3], const float (&s)[3], bool quad,
|
||||
Plane &out) {
|
||||
float moved[3], u[3], w[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
moved[k] = static_cast<float>(D(a[k]) + s[k]);
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
u[k] = static_cast<float>(D(moved[k]) - a[k]);
|
||||
w[k] = static_cast<float>(D(b[k]) - a[k]);
|
||||
}
|
||||
// w × u (0x0063256F: three `fsubp st(1),st`).
|
||||
const float cz = static_cast<float>(D(u[1]) * w[0] - D(w[1]) * u[0]);
|
||||
const float cy = static_cast<float>(D(w[2]) * u[0] - D(u[2]) * w[0]);
|
||||
const float cx = static_cast<float>(D(u[2]) * w[1] - D(u[1]) * w[2]);
|
||||
if (std::fabs((D(cy) * cy + D(cz) * cz) + D(cx) * cx) < kDegenerate)
|
||||
return false;
|
||||
out = PlaneFromPoints(a, b, moved);
|
||||
float t[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
t[k] = static_cast<float>(D(other[k]) - a[k]);
|
||||
const double side = quad ? (D(t[2]) * out.n[2] + D(t[0]) * out.n[0]) + D(t[1]) * out.n[1]
|
||||
: (D(t[2]) * out.n[2] + D(t[1]) * out.n[1]) + D(t[0]) * out.n[0];
|
||||
if (side > 0.0) {
|
||||
for (float &c : out.n)
|
||||
c = -c;
|
||||
out.d = -out.d;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// BuildTrianglePrism (0x00632460): the edge planes of a bevel swept along s.
|
||||
bool TrianglePrism(const Volume &volume, const uint8_t *index, const float (&s)[3], Plane (&out)[4]) {
|
||||
for (int i = 0; i < 3; ++i)
|
||||
if (!EdgePlane(volume.v[index[i]], volume.v[index[(i + 1) % 3]], volume.v[index[(i + 2) % 3]], s, false,
|
||||
out[i]))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// BuildQuadPrism (0x00632F80): the edge planes of a box face swept along s.
|
||||
bool QuadPrism(const Volume &volume, const uint8_t *index, const float (&s)[3], Plane (&out)[4]) {
|
||||
for (int i = 0; i < 4; ++i)
|
||||
if (!EdgePlane(volume.v[index[i]], volume.v[index[(i + 1) & 3]], volume.v[index[(i + 3) & 3]], s, true,
|
||||
out[i]))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// BuildVolumePlanes (0x00631440) and CMovement::BuildVolume (0x00631BE0):
|
||||
// the nine planes and nine vertices of the volume at p.
|
||||
Volume BuildVolume(const float (&p)[3], float radius, float height) {
|
||||
const double x = p[0], y = p[1], z = p[2], r = radius, h = height;
|
||||
const double a = kBevelOut, b = kBevelDown, zero = 0.0;
|
||||
Volume v{};
|
||||
const auto set = [&](int i, float nx, float ny, float nz, double d) {
|
||||
v.planes[i] = Plane{{nx, ny, nz}, static_cast<float>(d)};
|
||||
};
|
||||
set(0, -1.0f, 0.0f, 0.0f, x - r);
|
||||
set(1, 1.0f, 0.0f, 0.0f, -x - r);
|
||||
set(2, 0.0f, 1.0f, 0.0f, -y - r);
|
||||
set(3, 0.0f, -1.0f, 0.0f, y - r);
|
||||
set(4, 0.0f, 0.0f, 1.0f, -z - h);
|
||||
set(5, -kBevelOut, 0.0f, -kBevelDown, (x * a + z * b) + y * zero);
|
||||
set(6, kBevelOut, 0.0f, -kBevelDown, (z * b - x * a) + y * zero);
|
||||
set(7, 0.0f, kBevelOut, -kBevelDown, (x * zero + z * b) - y * a);
|
||||
set(8, 0.0f, -kBevelOut, -kBevelDown, (x * zero + z * b) + y * a);
|
||||
|
||||
const float waist = static_cast<float>(z + r * kWaist);
|
||||
const float top = static_cast<float>(z + h);
|
||||
const float lowX = static_cast<float>(x - r), highX = static_cast<float>(x + r);
|
||||
const float lowY = static_cast<float>(y - r), highY = static_cast<float>(y + r);
|
||||
const float corners[4][2] = {{lowX, lowY}, {lowX, highY}, {highX, highY}, {highX, lowY}};
|
||||
std::memcpy(v.v[0], p, sizeof v.v[0]);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
v.v[1 + i][0] = v.v[5 + i][0] = corners[i][0];
|
||||
v.v[1 + i][1] = v.v[5 + i][1] = corners[i][1];
|
||||
v.v[1 + i][2] = waist;
|
||||
v.v[5 + i][2] = top;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// PlaneDistanceAlongRay: how far a plane moves along `dir` before it
|
||||
// reaches q; n·q + d undivided when it runs parallel.
|
||||
double DistanceAlongRay(const float (&q)[3], const Plane &pl, const float (&dir)[3]) {
|
||||
using Fn = double(__fastcall *)(const float *, const Plane *, const float *);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_PLANE_DISTANCE_ALONG_RAY)(q, &pl, dir);
|
||||
}
|
||||
|
||||
// PolygonContactDistance (0x00632830): the travel until volume face k
|
||||
// reaches the polygon, 0 when it already touches; nothing when the polygon
|
||||
// lies wholly behind the face, past 1/36 of the volume's inside.
|
||||
bool ContactDistance(Polygon &poly, const float (&dir)[3], const Plane (&planes)[9], int k, float &best) {
|
||||
float m = best;
|
||||
bool behind = true;
|
||||
for (uint32_t i = 0; i < poly.count; ++i) {
|
||||
const double t = DistanceAlongRay(poly.v[i], planes[k], dir);
|
||||
if (t < m)
|
||||
m = t > 0.0 ? static_cast<float>(t) : 0.0f;
|
||||
if (t > kAllBehind)
|
||||
behind = false;
|
||||
}
|
||||
if (behind) {
|
||||
if (!ClipByPlanesExcept(poly, planes, k))
|
||||
return false;
|
||||
for (uint32_t i = 0; i < poly.count; ++i)
|
||||
if (DistanceAlongRay(poly.v[i], planes[k], dir) > kNegThirtySixth)
|
||||
behind = false;
|
||||
if (behind)
|
||||
return false;
|
||||
}
|
||||
if (!(m >= best)) {
|
||||
best = m;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// FindNearestFacet (0x00632700): of the facets facing against the move, the
|
||||
// one volume face k reaches first inside its swept prism.
|
||||
bool NearestFacet(const std::vector<Facet> &list, const float (&dir)[3], const Plane *prism, int prismCount,
|
||||
const Plane (&planes)[9], int k, float &best, uint32_t &hit) {
|
||||
bool found = false;
|
||||
for (uint32_t i = 0; i < list.size(); ++i) {
|
||||
const Facet &f = list[i];
|
||||
const double facing = (D(f.plane.n[2]) * dir[2] + D(f.plane.n[1]) * dir[1]) + D(f.plane.n[0]) * dir[0];
|
||||
if (facing > kFacing)
|
||||
continue;
|
||||
Polygon poly;
|
||||
std::memcpy(poly.v, f.v, sizeof f.v);
|
||||
poly.tag[0] = poly.tag[1] = poly.tag[2] = -1;
|
||||
poly.count = 3;
|
||||
if (!ClipByPlanes(poly, prism, prismCount))
|
||||
continue;
|
||||
float t = kFltMax;
|
||||
if (!ContactDistance(poly, dir, planes, k, t))
|
||||
continue;
|
||||
if (t <= best) {
|
||||
best = t;
|
||||
hit = i;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
// The band rule of 0x00632280 and 0x00632BA0: a face nearer by more than
|
||||
// 1/720 replaces the list, one within 1/720 joins it, and the nearest leads.
|
||||
void Band(const Plane &face, float t, Contact &out, float &best) {
|
||||
if (D(best) - kSkin > t) {
|
||||
out.faces[0] = face;
|
||||
out.count = 1;
|
||||
} else if (D(best) + kSkin > t && out.count < 9) {
|
||||
out.faces[out.count++] = face;
|
||||
}
|
||||
if (t < best) {
|
||||
best = t;
|
||||
if (out.count > 1)
|
||||
std::swap(out.faces[out.count - 1], out.faces[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// A leading face: its normal along the sweep, or NaN.
|
||||
bool Leading(double dot) { return dot > 0.0 || std::isnan(dot); }
|
||||
|
||||
// CMovement::FindBevelContacts (0x00632280): the four bevels.
|
||||
void Bevels(const std::vector<Facet> &list, const float (&dir)[3], float length, const float (&s)[3],
|
||||
const Volume &volume, Contact &out, float &best) {
|
||||
for (int b = 0; b < 4; ++b) {
|
||||
const Plane &face = volume.planes[5 + b];
|
||||
if (!Leading((D(face.n[2]) * s[2] + D(face.n[0]) * s[0]) + D(face.n[1]) * s[1]))
|
||||
continue;
|
||||
Plane prism[4];
|
||||
if (!TrianglePrism(volume, kFaces + 20 + 3 * b, s, prism))
|
||||
continue;
|
||||
float t = length;
|
||||
if (!NearestFacet(list, dir, prism, 3, volume.planes, 5 + b, t, out.hit))
|
||||
continue;
|
||||
Band(face, t, out, best);
|
||||
}
|
||||
}
|
||||
|
||||
bool InBox(const float (&box)[6], const float (&p)[3]) {
|
||||
for (int i = 0; i < 3; ++i)
|
||||
if (box[i] > p[i] || p[i] > box[i + 3])
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// CMovement::GatherSweepFacets (0x00631E70): the facets around the volume
|
||||
// moved by s, unless the cached box already holds it; a swimmer's liquid too.
|
||||
// The box is the body's full height whichever list the sweep reads.
|
||||
bool Gather(Facets &facets, bool swimmer, const float (&p)[3], float radius, float height, const float (&s)[3]) {
|
||||
float e[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
e[k] = static_cast<float>(D(p[k]) + s[k]);
|
||||
float low[3] = {static_cast<float>(D(e[0]) - radius), static_cast<float>(D(e[1]) - radius), e[2]};
|
||||
float high[3] = {static_cast<float>(D(e[0]) + radius), static_cast<float>(D(e[1]) + radius),
|
||||
static_cast<float>(D(e[2]) + height)};
|
||||
if (InBox(facets.box, low) && InBox(facets.box, high))
|
||||
return true;
|
||||
float query[6];
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const float grownLow = static_cast<float>(D(low[k]) - kGrowth);
|
||||
const float grownHigh = static_cast<float>(D(high[k]) + kGrowth);
|
||||
// UnionBoxes (0x006373B0): the cached box on ties and NaN.
|
||||
query[k] = grownLow > facets.box[k] ? facets.box[k] : grownLow;
|
||||
query[k + 3] = facets.box[k + 3] > grownHigh ? facets.box[k + 3] : grownHigh;
|
||||
}
|
||||
if (!facets.query(facets.context, query, facets.solid))
|
||||
return false;
|
||||
if (swimmer)
|
||||
QueryLiquid(facets, query);
|
||||
std::memcpy(facets.box, query, sizeof query);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void QueryLiquid(Facets &facets, const float (&box)[6]) {
|
||||
facets.liquidQuery(facets.context, box, facets.liquid);
|
||||
for (Facet &f : facets.liquid) {
|
||||
for (float &c : f.plane.n)
|
||||
c = -c;
|
||||
f.plane.d = -f.plane.d;
|
||||
}
|
||||
}
|
||||
|
||||
Facet MakeFacet(const float (&v0)[3], const float (&v1)[3], const float (&v2)[3]) {
|
||||
// FlushTriangleBatchFacets (0x00671CC0), its SSE path: the client sets
|
||||
// s_useSse from the CPU's SSE bit (CMap::Initialize, 0x006920A7), so
|
||||
// every machine that runs it today scales by rsqrtss, a 12-bit estimate.
|
||||
Facet f;
|
||||
std::memcpy(f.v[0], v0, sizeof f.v[0]);
|
||||
std::memcpy(f.v[1], v1, sizeof f.v[1]);
|
||||
std::memcpy(f.v[2], v2, sizeof f.v[2]);
|
||||
const double ax = D(v2[0]) - v0[0], ay = D(v2[1]) - v0[1], az = D(v2[2]) - v0[2];
|
||||
const float bx = static_cast<float>(D(v1[0]) - v0[0]);
|
||||
const float by = static_cast<float>(D(v1[1]) - v0[1]);
|
||||
const double bz = D(v1[2]) - v0[2];
|
||||
const float nx = static_cast<float>(D(by) * az - bz * ay);
|
||||
const float ny = static_cast<float>(bz * ax - az * bx);
|
||||
const float nz = static_cast<float>(ay * bx - D(by) * ax);
|
||||
const float length2 = static_cast<float>((D(nx) * nx + D(ny) * ny) + D(nz) * nz);
|
||||
const float k = _mm_cvtss_f32(_mm_rsqrt_ss(_mm_set_ss(length2)));
|
||||
const double x = D(k) * nx, y = D(k) * ny, z = D(k) * nz;
|
||||
f.plane.n[0] = static_cast<float>(x);
|
||||
f.plane.n[1] = static_cast<float>(y);
|
||||
f.plane.n[2] = static_cast<float>(z);
|
||||
f.plane.d = static_cast<float>(-((z * v0[2] + y * v0[1]) + x * v0[0]));
|
||||
return f;
|
||||
}
|
||||
|
||||
bool PointOverFacet(const Facet &facet, const float (&p)[3]) {
|
||||
// 0x0063364A: the facet's prism along (0, 0, 1), its planes starting
|
||||
// as (0, 0, 1, 0); 0x00633671 sums (x, z, y) and fails above 1/12.
|
||||
const float up[3] = {0.0f, 0.0f, 1.0f};
|
||||
Plane planes[3] = {{{0.0f, 0.0f, 1.0f}, 0.0f}, {{0.0f, 0.0f, 1.0f}, 0.0f}, {{0.0f, 0.0f, 1.0f}, 0.0f}};
|
||||
for (int i = 0; i < 3; ++i)
|
||||
if (!EdgePlane(facet.v[i], facet.v[(i + 1) % 3], facet.v[(i + 2) % 3], up, false, planes[i]))
|
||||
break;
|
||||
for (const Plane &pl : planes)
|
||||
if (((D(pl.n[0]) * p[0] + D(pl.n[2]) * p[2]) + D(pl.n[1]) * p[1]) + pl.d > kTwelfth)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void BaseBox(const float (&p)[3], float radius, float height, float (&box)[6]) {
|
||||
box[0] = static_cast<float>(D(p[0]) - radius);
|
||||
box[1] = static_cast<float>(D(p[1]) - radius);
|
||||
box[2] = p[2];
|
||||
box[3] = static_cast<float>(D(p[0]) + radius);
|
||||
box[4] = static_cast<float>(D(p[1]) + radius);
|
||||
box[5] = static_cast<float>(D(p[2]) + height);
|
||||
}
|
||||
|
||||
bool Sweep(Facets &facets, bool liquid, bool swimmer, const float (&p)[3], float radius, float height,
|
||||
const float (&dir)[3], float length, Contact &out) {
|
||||
const std::vector<Facet> &list = liquid ? facets.liquid : facets.solid;
|
||||
out.count = 0;
|
||||
if (std::fabs(length) < kDegenerate) {
|
||||
out.distance = 0.0f;
|
||||
out.hit = static_cast<uint32_t>(list.size());
|
||||
return true;
|
||||
}
|
||||
// A swimmer meets the liquid with the lower three quarters of its body
|
||||
// (0x00632C64), so it rests with its head above the surface.
|
||||
const float volumeHeight = swimmer && liquid ? static_cast<float>(D(height) * kThreeQuarters) : height;
|
||||
// At least 1/36 of sweep, but every distance is capped at the length.
|
||||
const float d = length > kThirtySixth ? length : kThirtySixth;
|
||||
const float s[3] = {static_cast<float>(D(d) * dir[0]), static_cast<float>(D(d) * dir[1]),
|
||||
static_cast<float>(D(d) * dir[2])};
|
||||
const Volume volume = BuildVolume(p, radius, volumeHeight);
|
||||
if (!Gather(facets, swimmer, p, radius, height, s)) {
|
||||
out.distance = 0.0f;
|
||||
return false;
|
||||
}
|
||||
out.hit = static_cast<uint32_t>(list.size());
|
||||
float best = length;
|
||||
Bevels(list, dir, length, s, volume, out, best);
|
||||
for (int a = 0; a < 5; ++a) {
|
||||
const Plane &face = volume.planes[a];
|
||||
if (!Leading((D(s[2]) * face.n[2] + D(s[1]) * face.n[1]) + D(s[0]) * face.n[0]))
|
||||
continue;
|
||||
Plane prism[4];
|
||||
if (!QuadPrism(volume, kFaces + 4 * a, s, prism))
|
||||
continue;
|
||||
float t = length;
|
||||
if (!NearestFacet(list, dir, prism, 4, volume.planes, a, t, out.hit))
|
||||
continue;
|
||||
Band(face, t, out, best);
|
||||
}
|
||||
out.distance = best < kSkin ? 0.0f : best;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Model::Scene::Contact
|
||||
@@ -0,0 +1,99 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// The client mover's contact geometry, transcribed for the scene's own
|
||||
// bodies: the volume a unit occupies, the facets of the world around a move,
|
||||
// and the first contact of the moving volume with them (0x00631440 through
|
||||
// 0x00632F80). Each function is one client function, with its arithmetic in
|
||||
// the client's order and precision (53-bit sums, float stores), so a body
|
||||
// meets a face where the client's mover would. The client's pure geometry
|
||||
// helpers (the polygon clips, the plane builder, the ray distance) are called,
|
||||
// not copied; the functions that read the client's mover state (CMovement,
|
||||
// the lists at 0x00C4E52C) are transcribed over the body's own, which come in
|
||||
// through Facets::query.
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Model::Scene::Contact {
|
||||
|
||||
// A plane: a point q is inside when n·q + d <= 0.
|
||||
struct Plane {
|
||||
float n[3];
|
||||
float d;
|
||||
};
|
||||
|
||||
// A world facet, the 52-byte record of the facet emitter (0x00671CC0): its
|
||||
// plane, then its three vertices.
|
||||
struct Facet {
|
||||
Plane plane;
|
||||
float v[3][3];
|
||||
};
|
||||
static_assert(sizeof(Facet) == 0x34, "the client's facet record");
|
||||
|
||||
// The facet the emitter writes for three vertices: normal (v1 - v0) × (v2 -
|
||||
// v0) normalized, d = -n·v0.
|
||||
Facet MakeFacet(const float (&v0)[3], const float (&v1)[3], const float (&v2)[3]);
|
||||
|
||||
// IsPointOverListedTriangle (0x006335D0): whether p lies at most 1/12
|
||||
// outside each edge of the facet, the edges extruded straight up, so above
|
||||
// or below the facet in plan.
|
||||
bool PointOverFacet(const Facet &facet, const float (&p)[3]);
|
||||
|
||||
// The facet lists a sweep reads and the box they cover: the solid list
|
||||
// (0x00C4E52C), the liquid list a swimmer adds (0x00C4E564), and their cached
|
||||
// box (0x00C4E5A0, min then max). `query` is the world query (0x006721B0)
|
||||
// with the mover's class: it empties `solid` and fills it with the facets in
|
||||
// a box, false when the query fails. `liquidQuery` is the same query with
|
||||
// the swimmer's class 0x30000, water and ocean surfaces, into `liquid`.
|
||||
struct Facets {
|
||||
std::vector<Facet> solid;
|
||||
std::vector<Facet> liquid;
|
||||
float box[6];
|
||||
bool (*query)(void *context, const float (&box)[6], std::vector<Facet> &out);
|
||||
bool (*liquidQuery)(void *context, const float (&box)[6], std::vector<Facet> &out);
|
||||
void *context;
|
||||
};
|
||||
|
||||
// The swimmer's liquid query (0x00633E90, 0x006320E0): `liquidQuery` over the
|
||||
// box, its return not read, then every liquid plane reversed (0x00637330
|
||||
// and FCHS), so a surface faces down and meets a rising swimmer as a ceiling.
|
||||
void QueryLiquid(Facets &facets, const float (&box)[6]);
|
||||
|
||||
// The volume's base box, as the substep's query frame (0x00633840) starts
|
||||
// it: (p.x - r, p.y - r, p.z) to (p.x + r, p.y + r, p.z + H).
|
||||
void BaseBox(const float (&p)[3], float radius, float height, float (&box)[6]);
|
||||
|
||||
// What a sweep met: the travel to the first contact, the facet found (an
|
||||
// index into the list swept, its size when none), and the leading faces of
|
||||
// the volume that touch within 1/720 of it.
|
||||
struct Contact {
|
||||
float distance;
|
||||
uint32_t hit;
|
||||
uint32_t count;
|
||||
Plane faces[9];
|
||||
};
|
||||
|
||||
// CMovement::SweepVolume (0x00632BA0): the volume of a body (a box of
|
||||
// half-width `radius` up to `height`, over a square pyramid whose apex is
|
||||
// `p`) swept along the unit vector `dir` for `length`, against the solid
|
||||
// list or with `liquid` the liquid list. A `swimmer` (flag 0x200000) gathers
|
||||
// the liquid list too, and its volume against that list is three quarters of
|
||||
// `height` tall. The hover's branch is not taken: the scene's bodies do not
|
||||
// hover. False when the facet query fails.
|
||||
bool Sweep(Facets &facets, bool liquid, bool swimmer, const float (&p)[3], float radius, float height,
|
||||
const float (&dir)[3], float length, Contact &out);
|
||||
|
||||
} // namespace Model::Scene::Contact
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user