Compare commits

...
19 Commits
Author SHA1 Message Date
Brues d7103de564 feat(spell): count set bonuses in GetSpellBonusHealing
Set-bonus spells are applied server-side as auras the client never sees
as buffs or known spells, so the derived healing sum missed them —
Lawbringer (3) is "Increase Healing 31" (9317), Judgement (3) is 57
(18037). Add a pass that counts the equipped members of each set from
the item record's set id (OFF_ITEMSTATS_ITEM_SET), reads ItemSet.dbc's
bonus spells and member thresholds, and runs every met bonus through
the same aura decoder — the rule the item tooltip's set-bonus lines use
at 0x52DFB9.

The Ironclad (aura 199) armor conversion stays on
UNIT_FIELD_RESISTANCES[0]: the server's SpellBaseHealingBonusDone uses
GetArmor(), the full total, even though the talent text says "Armor
from items".

Refs #135
2026-11-09 11:28:51 -06:00
Brues c00eb24a8d fix(event): refuse glue's event table and re-validate cached claim slots
CGGameUI::Initialize (0x0048FE00) calls RegisterScriptFunctions — where
our LoadScriptFunctions hook enables claim writes — and only then
SetScriptEventTable(0xBE1198, 0x225), which builds the in-game table.
Between the two the live table at [0xCEEF68] is still glue's 26-entry
one. A third-party DLL that registers a frame from its own
LoadScriptFunctions hook fires our Frame::RegisterEvent (0x00702140)
hook in that window: Grow enlarged the glue table (26 -> 142), the
reservations claimed slots 26..125 in it, the engine freed that buffer
and built the real table, and every cached slot stayed non-negative —
never retried, invisible to IsEventValid, and a Fire would have
signalled whatever engine event sat at that id.

Two guards, both read off the engine's own build:

- GameTableInstalled: the in-game build always passes 0x225 names and
  the other DLLs' SetEventCount clamps only enlarge, so a table smaller
  than EVENT_NAME_TABLE_STATIC_COUNT is glue's. Claim and Grow refuse it.
- Stale: each reservation keeps the SStrDup'd pointer it wrote; when the
  live entry at its slot no longer holds that pointer (or name), the
  table was rebuilt behind us and every slot is forgotten and re-claimed
  before the original RegisterEvent populates any chain.

Fixes #134
2026-11-09 11:05:32 -06:00
Brues b65c9e0319 fix(spell): never auto-unshift out of Spirit of Redemption
3.3.5's and 4.4.2's SpellShapeshiftForm.dbc give Spirit of Redemption
(form 32) flags 0x4100, which include SHAPESHIFT_FORM_FLAG_NO_AUTO_UNSHIFT
(0x100). So the later clients' CGUnit_C::CanAutoUnshift refuses it, and no
cast, interaction or item use cancels it. 1.12's row has flags 0, so with
autoUnshift on, our CanAutoUnshift allowed it. A priest in Spirit of
Redemption who cast a NOT_SHAPESHIFT spell, right-clicked an NPC or object,
used an item or opened the flight map sent CMSG_CANCEL_AURA for it.

The later-client flags that 1.12's rows lack now sit in one table:
CAN_INTERACT on the druid forms and Ghost Wolf (read only while
autoUnshift is on, as before), and NO_AUTO_UNSHIFT on form 32, which
CanAutoUnshift reads unconditionally, as 4.3.4 does. With it, every
autoUnshift site leaves Spirit of Redemption on and keeps 1.12's own
refusal.

The comment on FormCanInteract also records why a locked object still
unshifts. It opens by a spell (Opening 6478 for chests and barrels), and
the server's CheckCast form test refuses that spell on 1.12's spell data,
which keeps SPELL_ATTR_NOT_SHAPESHIFT (3.3.5 dropped it).
2026-11-08 11:29:09 -06:00
Brues de033896b1 feat(spell): let autoUnshift talk and use objects in the druid forms
1.12's SpellShapeshiftForm.dbc gives SHAPESHIFT_FORM_FLAG_CAN_INTERACT
(0x8) only to Shadowform. 3.3.5 and 4.4.2 also give it to Cat, Travel,
Aquatic, Bear, Dire Bear and Ghost Wolf, and Turtle's id 11 (Swift
Travel Form) has it there too. So in the later clients, these forms pass
the NPC right-click gate (CGPlayer_C::HandleOnRightClick, 0x005F0130)
and the game-object use check (0x005F3130) as they are. The form cancel
that comes before the range check almost never runs.

In 1.12, autoUnshift cancelled every druid form at that gate, even when
the NPC was out of interact range. With autoUnshift on, the redirected
IsShapeShifted calls at both gates and at CanInteractNow (0x00606880)
now read a form in that set as able to interact. They report "not
shifted", so the engine's own 0x8 test that follows does not refuse it,
and the cursor shows the NPC's service icon. The cancel stays for a form
that cannot interact in either client.

Neither vmangos nor tortoise-wow tests the form on an NPC or object
interaction. CanInteractWithNPC and the use handlers have no form check;
the only one is item use. So the client gates are the only refusal.
2026-11-07 20:46:48 -06:00
MrbcatandBrues dc8d4caf12 fix(loot): skip corpses that stay lootable after LootAllCorpses (#133)
* fix(loot): skip corpses that stay lootable after LootAllCorpses

A corpse whose remaining loot this player can never take keeps
UNIT_DYNFLAG_LOOTABLE set: quest items for quests they don't have,
condition-gated loot, and the like. The per-corpse re-check still
passes, so a caller that runs C_Loot.LootAllCorpses from a timer
re-opens the same corpse on every walk and re-sends the refused
CMSG_LOOT_ITEM, which never clears.

Loot mode now remembers every corpse it opened and released. Later
walks skip a remembered corpse while it is still visible and
lootable; the entry is dropped once the corpse stops being lootable or
leaves the object manager. A corpse that merely left render range is
dropped too, so it gets one retry when the player comes back.
ScanNearbyLoot stays read-only and keeps reporting the corpse.

1.12 has no per-slot "cannot loot" flag (GetLootSlotInfo has no
`locked` return), so one refused request per corpse is unavoidable;
this only stops it from repeating on every walk.

* docs(loot): describe the LootAllCorpses corpse skip

Explain why a corpse that stays lootable after the walk is skipped by
later walks, and when the skip entry is dropped.

* fix(loot): skip only corpses that open empty in LootAllCorpses

The previous change remembered every corpse the loot walk opened. The
most common reason a corpse stays UNIT_DYNFLAG_LOOTABLE is full bags,
so those corpses were never looted again by the walk, even after the
player freed bag space. Rolls and master-loot assignments in progress
had the same problem.

The server never sends a player a loot slot that player cannot take:
another member's quest item and condition-gated loot are left out of
SMSG_LOOT_RESPONSE, while the corpse stays lootable for as long as any
loot remains. So the stuck corpse opens to an EMPTY window, with no
coin and no populated VAR_LOOT_SLOTS entry. Loot mode now remembers a
corpse only when the scraped window is empty. A window with anything
in it (full bags, a roll, a master-loot slot) is retried.

An entry is forgotten:
- on the first world tick where its unit is gone or no longer
  lootable. This replaces the prune at walk start, which missed a
  corpse that left view and came back, and a creature that respawned
  with the same GUID and died again, between two walks;
- all at once on QUEST_LOG_UPDATE (event 0x134, observed through the
  shared FUN_FIRE_EVENT_NO_ARGS interceptor and never swallowed),
  because quest items are per-player loot and a quest change can put
  an item into a window that was empty.

The memory moves out of the per-walk ScanCtx into its own vector. The
API reference now describes the skip as a contract only.

---------

Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
2026-11-06 23:25:23 -06:00
Brues 20069f3815 docs(api): sort the reference's sections alphabetically
Seven sections were out of place in the body: ChatBubbles (after Mount),
Bindings (after Focus), APIDocumentation, CVar, MerchantFrame, NamePlate
and PvP. The contents list shared the last four. Both are now in the same
case-insensitive order, with one blank line between contents groups
(DeathInfo and Sound had none). The ClassicAPI namespace section had no
contents entry; it has one now.

The PVP_TIMER_UPDATE entry moves from the PvP section to Events, between
PLAYER_SWING_RANGE_UPDATE and QUEST_ACCEPTED. It now names the PvP timer
and links the two timer functions, and the PvP section links to it.
2026-11-06 17:35:16 -06:00
Brues f2fa112f0a feat(mount): add C_MountJournal.GetMountFromSpell and GetMountInfoExtraByID
1.12.1 has no mount table, so a mount's ID is the ID of the spell that
summons it: GetMountFromSpell(458) returns 458. A spell is a mount when
one of its effects applies SPELL_AURA_MOUNTED (78); Spell.dbc is read
directly, so mounts the player has not learned resolve too. On Turtle
every mount item carries spell 46499 "Add Mount to Collection" instead of
its mount spell (the server maps item to spell from its own table), so
only the mount spell gives a mount ID.

GetMountInfoExtraByID returns the nine retail values. The display ID is
the one the server mounts the player on: the mount aura's misc value is a
creature entry, and the client's creature cache holds that creature's
display (SMSG_CREATURE_QUERY_RESPONSE carries the template's first
display, and every mount creature has exactly one). An uncached creature
returns nil and starts the cache load through the new
Creature::Info::RequestLoad, which C_CreatureInfo.RequestLoadCreatureByID
now uses too; CREATURE_DATA_LOAD_RESULT fires when the data lands.
Verified in game: 23219 (Swift Mistsaber) returns 14332.

Mount type and model scene default to 230 and 4. Seven mounts take
retail's values instead (the six Qiraji battle tanks use scene 44, the
Riding Turtle type 231), each checked by name against Turtle's
Spell.dbc. The seat animation is 91 (Mount), the animation 1.12.1's
world mount plays on the rider.
2026-11-06 17:29:11 -06:00
Brues fca967c827 refactor(spell): test a spell's auras through one HasAuraEffect
Six places asked the same question of a Spell.dbc record: does one of its
three effects apply aura N (EffectApplyAuraName, +0x16C, int32[3])? Each
had its own loop: the combat log's damage-shield lookup, crowd-control
classification and its snare test, tracking-spell detection,
C_SpellBook.GetPlayerSpellsByAura, and the aura-source periodic-damage
gate (which also hard-coded 3 for the effect count).

Spell::Lookup now has FindAuraEffect (the index of the first effect that
applies the aura, or -1, for callers that read that effect's other
columns) and HasAuraEffect on top of it, next to the existing HasEffect.
All six call it. Code that pairs an aura with the same effect's misc
value, amount or effect code (Cancel, Interruptible, BonusDamage,
StatAccum, EffectInfo) keeps its own per-effect reads.
2026-11-06 17:29:02 -06:00
Brues 24404b5a23 fix(swing): report only a held main-hand reset at attack start
SMSG_ATTACKSTART re-fired PLAYER_SWING(MainHand) whenever the main-hand
swing was still running. That was meant for a cast reset recorded before
the attack (held back because no swing was coming yet), but the server
also sends SMSG_ATTACKSTART on a target switch mid-attack. Unit::Attack
then resets only the off-hand timer; the main-hand swing keeps going.
The event carried just the time left in that swing, so listeners read a
nearly finished swing as a fresh, short one.

Each swing type now carries a held flag, set only by a reset recorded
without notify and cleared when that type's PLAYER_SWING fires. The
attack-start path reports the main hand only while it is held.

Fixes brues-code/SuperCleveRoidMacros#11
2026-11-06 12:58:12 -06:00
Brues a106d86969 fix(unit): consult the name cache only when no unit object is held
UnitClassBase and UnitRaceBase now follow stock Script_UnitClass
(0x00518350) and Script_UnitRace (0x00518200) branch for branch.
"player" reads the selected-character record; any other token goes
through GetGUIDFromName + ClntObjMgrObjectPtr(TYPEMASK_UNIT) and reads
the held object's descriptor byte (+0x79 class, +0x78 race). The
NameCache record (+0x140 / +0x138, null callback) is peeked only when
no object is held.

Before, the NameCache was also peeked when a held object's byte was 0.
That never changed a result (creatures are not in the NameCache) but
resolved the token twice and did not match the engine's branch.

The comments and API.md also claimed creatures return nil, that class
IDs run 1-9, and that UnitClass has a third return. None of that is
true: creatures go through the same ChrClasses/ChrRaces lookup as in
UnitClass/UnitRace, Druid is 11, and stock returns exactly two values.
2026-11-06 11:52:58 -06:00
Brues fac3be3a0b refactor(lua): take integer enum and constants entries as arrays
RegisterIntegerEnum and RegisterConstants took their entries with a
count beside them, written as sizeof(t) / sizeof(t[0]), std::size, or
a literal. They now take Game::ArrayRef<EnumIntegerEntry>, as
RegisterFrameMethods does, so the count comes from the array's type.
The documentation recorders RecordEnum and RecordConstants take the
same view.

All 34 call sites pass only the array. The six literal counts were
checked against their arrays first: each was the full length, so every
Enum.* and Constants.* table gets the same entries as before. Five
files that included <iterator> only for std::size drop it.
2026-11-06 11:34:27 -06:00
Brues b49c107778 refactor(lua): take frame-method tables as arrays, not pointer and count
Every RegisterFrameMethods call passed its method table and doc table
with a count beside each: sizeof(t) / sizeof(t[0]), std::size, or a
literal. The literals had to be kept in step with the tables by hand.

Game::ArrayRef<T> is a view over a file-scope array that takes the
count from the array's type, the way Doc::FieldList already did for a
function's arguments and returns. FieldList is now ArrayRef<Field>,
Doc::MethodList is ArrayRef<Method>, and RegisterFrameMethods takes
(context, methods, docs). The table and count still go to the engine's
frame-method iterator (FUN_REGISTER_FRAME_METHODS) unchanged, and to
the documentation recorder.

All 84 call sites pass only the arrays. The 16 literal counts were
checked against their tables first: each was the full length, so every
registry gets the same methods as before.
2026-11-06 11:27:22 -06:00
Brues 53b7f79440 fix(model): release dress builders and stopped effects with their frame
/reload crashed with an access violation in the model release
(0x007103A3, inside FUN_MODEL_INSTANCE_RELEASE 0x007103A0) when
SetDisplayInfo had dressed a few panes.

1.12.1 reloads in two steps. CGGameUI::Shutdown (0x00490BD0) deletes
the CSimpleTop, which destroys every frame. A Model frame's teardown,
DestroyModelFrame (0x0076CA50, from the CSimpleModel and
CGCharacterModelBase destructors), releases the frame's M2 scene at
+0x314, and the scene frees every model in it, even one that something
else still holds a reference on. Only then does CGGameUI::Initialize
(0x0048FBF0) reset the Lua state, which runs FrameScript_Initialize
and our reload cleanups. DisplayInfo's cleanup destroyed each kept
compositor there (FUN_CHARCOMP_DESTROY 0x00476000), and the compositor
released its reference to a model that was already freed. VisualKit's
cleanup detached and released stopped one-shot effects that live in
the same freed scene.

Model::Teardown hooks DestroyModelFrame and runs its subscribers
before the original, while the frame's scene and models are intact,
which is where the engine frees what its own frames hold. DisplayInfo
drops the frame's dress job there, and VisualKit records each stopped
effect's pane (in the done callback's user data) and discards that
pane's effects there. Their reload cleanups keep only the
pointer-keyed state.

The ReloadAutoRegister doc now says that the cleanups run after the
frames are gone, so they may forget pointers but never release an
engine object tied to a frame.
2026-11-06 11:16:47 -06:00
Brues 7b21453469 refactor(model): share the engine model calls across the model modules
The model modules each declared their own pointers to the same 1.12.1
engine functions: the instance gates FUN_MODEL_INSTANCE_LOADED
(0x007103D0) and FUN_MODEL_INSTANCE_READY, AddRef / Release
(0x00710390 / 0x007103A0), FUN_M2SCENE_CREATE_MODEL,
FUN_M2MODEL_PLAY_ANIMATION, the done callback, the item visuals, the
render callback, the M2 data lookup FUN_M2_LOOKUP_INDEX (0x00710310),
and the pane's model, scene, SetModelInstance virtual (vtable +0x90)
and SetDisplay. model/M2.h now holds one typed wrapper for each, and
every module calls through it.

- Game::Lua::ResolvePlayerModel / ResolveDressUpModel join the
  per-type resolvers, replacing four ResolvePane copies and five
  inline ResolveTypedObject calls. Game::Lua::TYPE_NONE replaces the
  local kTypeNone in Rotation and frame/CreateFrame.
- Offsets.h has the full INVTYPE list, 0..28, named by the engine's
  own key table at 0x0083DDB0. It replaces the four scattered
  INVTYPE_* entries and the local copies in DressUp and ItemDisplay,
  and the try-on result switch reads by name.
- DisplayInfo keeps its dress jobs in a map by frame, like the other
  modules, in place of the fixed 8-slot pool and its eviction.
- DressUp names the held lane's bytes (item class, inventory type,
  sheathe type) and detaches a lane in one place.
- ItemDisplay::OwnVisual is the one test for a display's own
  ItemVisuals row (GetItemDescVisual 0x0062EC70), used by SetItem and
  by TryOn's enchant glow. Both own-record panes share
  Pane::DisplayRecord, and the SetDisplayInfo records are cleared on
  /reload as the SetItem records are.
2026-11-06 11:11:18 -06:00
shikuljaandBrues ebfd9d034d feat(localization): complete and correct the Russian locale (#130)
* feat(localization): complete and correct the Russian locale

The Russian locale was 99 keys behind enUS, so a Russian client kept the
English strings for ClassicAPI's own options: the AddOn Options title and
its game-menu button, every page name, header, control label and tooltip,
the AoE Loot binding, and the generic strings the options pages gained
(FOCUS_TOKEN_NOT_FOUND, LUA_ERROR, RELOADUI, PLAYER_LIST_DELIMITER,
SYMBOLIC_NEGATIVE_NUMBER, SYMBOLIC_POSITIVE_NUMBER and the money texture
formats). All 327 keys are now present, in enUS order, so the two files
diff key by key.

Focus follows the client's modern terms — «Взять в фокус», «Сбросить
фокус», «Выбрать фокус» and «Взять цель в фокус» — matching the existing
/фокус and /сбросфокуса aliases. The rest uses the 1.12 Russian client's
vocabulary where it has one (модификация, цель, панель команд, время
восстановления, табличка с именем) and «динамическое выделение» /
«мягкая цель» for action targeting. The money strings keep their |4
plural forms, which GetCoinText expects.

Fixes:

- LARGE_NUMBER_SEPERATOR was "", which turned digit grouping off. It is
  now a space, as in the tooltip's 1 234 567.
- FIRST/SECOND_NUMBER_CAP and their _NO_SPACE twins carry the leading
  space, because Abbreviate concatenates the number and the suffix
  directly.
- EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP printed a slot number where
  the missing count belongs; it now takes the missing count.
- SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL was missing a space before
  «или нажмите».
- SET_FRIENDNOTE_LABEL had a |3-5 declension token, which 1.12 renders
  literally.
- ITEM_SLOTS_IGNORED and ITEMS_NOT_IN_INVENTORY had wrong third plural
  forms, ITEM_MOD_HIT_MELEE_RATING_SHORT had a double space, and the flat
  stat labels (Защита, Уклонение, Парирование, Блок) read wrong after a
  signed value, so they use the same «к …» form as the other ITEM_MOD
  labels.

The TOC gains ## Notes-ruRU next to ## Notes.

* fix(localization): restore retail ruRU focus, number and log strings

Follow-up to the Russian locale completion, which had changed several
strings that the retail ruRU client already translates. Each is now
the retail text, or a correction where the new wording was wrong:

- Focus: the FOCUSTARGET / TARGETFOCUS binding labels were swapped
  (FOCUSTARGET runs FocusUnit("target"), TARGETFOCUS runs
  TargetUnit("focus")). All focus strings use retail ruRU's terms
  again: Запомнить цель, Восстановить цель, Отменить запоминание цели,
  Запомненная цель, <фокус отсутствует>.
- EVENTTRACE_LOG_DISCARD is the message logged after the log is
  discarded; it had become an option-style label. Retail: Журнал
  сброшен. EVENTTRACE_BUTTON_DISCARD_FILTER read "cancel all"; retail:
  Сбросить все.
- FIRST/SECOND_NUMBER_CAP and their _NO_SPACE variants go back to
  retail Т / М. AbbreviateNumbers appends the _NO_SPACE suffix
  directly by design ("1.2K"), so " тыс." widened compact numbers.
- LARGE_NUMBER_SEPERATOR goes back to retail "" (as frFR). A plain
  space also let a number wrap across lines. The separate-large-numbers
  tooltip drops its "1 234 567" example, which no longer applies.
- The loose-target option read "whom the soft target follows". The
  normal target is the one that follows, so it is now "обычная цель",
  and the choices name the soft enemy / soft friend.

* fix(localization): pick language rules by locale name, not 4.3.4's index

1.12.1's locale index (VAR_LOCALE_INDEX, 0xC0E080) is the Wow.ini
Language+Country tag's position in the eight-entry name table at
0x8558A4 (enUS koKR frFR deDE zhCN zhTW esES xxYY), stored once at
0x402562 and 0 when nothing matches. It is never 8. The Russian branches
ported from 4.3.4 switched on 4.3.4's own slot numbers (esMX 7, ruRU 8),
so they could never run: a client whose GetLocale() is "ruRU" fell
through to the default plural rule ("5 предмета") and got no ё/е folding
in bag search.

Localization::Locale::Client() reads the same entry Script_GetLocale
(0x48D8B0) pushes, table[index], and Is(code) compares against it. The
language-rule sites now go through it:

- Text::Grammar's plural index (|4 expansion) keys koKR/frFR/zhCN/zhTW
  and ruRU by name.
- Container::Search picks its accent-folding set by name, as a Folding
  value instead of an index switch.
- Localization::Numbers' ClientIsAsian reuses IsAsianLocale on the name.
- AddOns::TocRewrite drops its private copy of the reader, and the
  slot-8 clamp, which guarded an index that cannot occur.

The DBC column reads keep using the index, which is what it is for.

---------

Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
2026-11-06 10:25:00 -06:00
Brues d825bcbdda fix(spell): stand, dismount and unshift only when the cast is sent
autoStand / autoDismount / autoUnshift ran their actions as soon as the
caster-state check (CheckCasterStateForSpell, 0x006094F0) passed. In the
1.12.1 cast request (TryCastSpell, 0x006E4B60) that check comes before
the target arm (0x006E5250), so a cast that then found no target still
dismounted the player and showed "You have no target" (Charge with
nothing targeted). A targeting cursor the player cancelled did the same.

The caster check now only keeps what its rungs noted, and the actions run
at the send. SendCast (0x006E54F0) builds the cast packet; its five
callers all pass the global pending cast: the arm's two immediate sends
and the object / location / trade-slot binders, which the targeting
cursor's click also runs. The arm's only caller is the cast request,
after the caster check, so every send follows a check of the same cast.
Each action runs only if its state still holds, since the player can
stand, dismount or leave the form while the cursor is up. The send
redirects must all apply before the rungs are patched.

Item use had the same ordering gap. Its form refusal (0x005D914D) comes
before the toggle test, which turns off a use aura that is already up
instead of casting (0x005D9157..0x005D9237). Re-using such an item while
shapeshifted dropped the form as well. The refusal now only notes the
unshift; it runs at the bind-on-use test (0x005D91D3), which every path
past the toggle reaches, and the toggle's cancel call clears it.

Fixes #132
2026-11-06 09:56:14 -06:00
Brues 6b36bb10e6 fix(spell): report GetSpellPowerCost in displayed power units
The engine's cost helper (FUN_006E31B0) answers in stored units, and
1.12.1 stores rage in tenths, so Shield Slam's 20 rage came back as
cost = 200. Divide by the engine's own display divisor
(GetPowerDisplayDivisor, 0x006E7130: [0x0086F978 + 4*type], 1 for a
negative type; table {1, 10, 1, 1, 1000}) before pushing, the same way
the engine and our UnitPower / GetSpellInfo / combat-log paths do.

Every power amount surfaced to Lua now goes through one helper,
Unit::Power::DisplayDivisor, which calls that engine function. That
retires the duplicate FUN_GET_POWER_DISPLAY_DIVISOR offset, the two
local typedef copies, and UnitPower's direct read of the divisor table
(VAR_UNIT_POWER_DIVISOR_TABLE, now removed).

Fixes #129
2026-11-05 08:51:59 -06:00
Brues 6628dcca19 fix(loot): keep LootAllCorpses walking past refused and stale corpses
The walk ignored the server's refusal. A failed CMSG_LOOT ("too far
away", not ours, ...) arrives as SMSG_LOOT_RESPONSE with loot type 0;
CGPlayer_C::ReadLootResponse (0x005EB900) shows the error and clears the
held loot target (+0x1D28) without opening a window, and the release
response (0x005EC090) clears it the same way. The walk only advanced on
the loot controller hook or a 180-frame timeout, so each refusal stalled
it for seconds. It now advances as soon as the held target is no longer
the corpse it sent for.

A send the engine refused dropped the whole queue. LootUnit (0x005DF2A0)
sends nothing while the mover flags & 0x20FF are set (moving, turning,
falling), and the walk treated each refusal as "skip this corpse", so
moving during a walk skipped every corpse left. A refused send now stays
the current corpse and is retried each tick; after 3 s the walk ends.

The queue's range check went stale. Range was tested once when the walk
began, but each corpse costs a round trip and the player can move in
between. With CanAutoInteract set, CanLootNow (0x005EC110) skips its own
reach test, so the request went out and the server refused it. Each
corpse is now checked for existence, DYNFLAG_LOOTABLE and reach right
before its send, and skipped when it fails.

The reach test is CanLootNow's own: both units' +0x1F0 reach plus the
engine's leeway, at least its minimum, with both constants read live
(VAR_MELEE_REACH_LEEWAY / VAR_MELEE_REACH_MIN) instead of local copies.
It and the lootable test move to Loot::Request, shared by
GetNearbyLootableUnits and the walk. The step timeout uses Time::Clock
instead of a frame count.
2026-11-04 20:04:35 -06:00
Brues 55d94c80d6 feat(spell): apply autoStand/autoDismount/autoUnshift to interaction, loot, items, attacks and taxis
The three settings only covered casts. Each 1.12.1 gate that refuses an
action for the player's stand state, mount or form now passes when the
matching setting allows it, and the engine's own action (the stand setter
FUN_005ED430, or CMSG_CANCEL_AURA for the mount / form aura) runs just
before the action is sent. With the settings at 0 nothing changes.

- NPC right-click (0x005F0130): the IsShapeShifted call at 0x005F0150
  cancels a form without SpellShapeshiftForm flag 0x8 instead of showing
  ERR_CANT_INTERACT_SHAPESHIFTED. CanInteractNow (0x00606880), whose only
  caller is the hover cursor, passes the same form, so the NPC's service
  cursor shows.
- Game objects: the use check (vtable slot 6, 0x005F3130), also the hover
  cursor's query, passes the mount (0x005F31A8) and the form (0x005F31E1)
  for an object with no Lock row. The default use's no-spell branch
  (0x005F35DF, before CMSG_GAMEOBJ_USE) and the mailbox and text use slots
  dismount (except at a mailbox, FUN_0047CFF0) and cancel the form. A
  locked object opens by a spell, which the cast path already handles.
- Loot: the GetStandState calls of the unit (0x0060BFB7) and corpse
  (0x005D6C3B) right-clicks stand the player up instead of showing
  ERR_LOOT_NOTSTANDING. The unit loot step's mount read (0x0060BF98)
  passes, and LootUnit dismounts before CMSG_LOOT (0x005DF3B3).
- Item use (0x005D8D00): the ERR_NO_ITEMS_WHILE_SHAPESHIFTED refusal at
  0x005D914D cancels the form and continues. Turtle's exe already skips
  that test; the stock exe reaches it.
- Attack: the mount reads in the attack cursor (0x00482659), the
  right-click attack (0x0060C1B4), StartAttack (0x005ECC06) and both tests
  of the attack resolver (0x00613039, ERR_ATTACK_MOUNTED; 0x00613142)
  pass. StartAttack, after all its tests (0x005ECC56), dismounts when the
  target is in melee reach (Combat::Swing::InMeleeRange). Otherwise it
  returns without attacking and shows ERR_OUT_OF_RANGE, except while
  click-to-move (FUN_0060F900) walks the player there.
- Taxi: after the taxi map opener's TAXIMAP_OPENED tutorial call
  (0x004DBAA2), a map with more than one node stands the player up,
  dismounts and cancels the form, each when its setting is on.

Every site is patched only when it holds the expected stock bytes, which
both the Octo and stock 1.12.1 exes do.
2026-11-04 16:44:44 -06:00
148 changed files with 3529 additions and 2345 deletions
+3 -3
View File
@@ -219,9 +219,9 @@ open the game menu and click AddOn Options, then ClassicAPI.
| [Bindings](docs/API.md#actionbuttonusekeydown) | `ActionButtonUseKeyDown` | `0` | `1` uses an action-bar key binding when you press the key. `0` uses it when you release the key. |
| [Combat](docs/API.md#stopautoattackontargetchange) | `stopAutoAttackOnTargetChange` | `0` | `1` stops melee auto-attack, Auto Shot and wand shots when you change target. |
| [Combat log](docs/API.md#kept-entries) | `combatLogRetentionTime` | `300` | The number of seconds that the combat log keeps an entry. |
| [Cast settings](docs/API.md#cast-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast while you sit. |
| | `autoDismount` | `0` | `1` makes you dismount when you cast while you are mounted. |
| | `autoUnshift` | `0` | `1` makes you leave your form when a spell cannot be cast in it. |
| [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, 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. |
+1061 -936
View File
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -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();
+16 -11
View File
@@ -252,6 +252,12 @@ void *ResolveEditBox(void *L, int idx, bool raiseError) {
void *ResolveModel(void *L, int idx, bool raiseError) {
return ResolveTypedObject(L, idx, Offsets::VAR_MODEL_LUA_TYPE_ID, raiseError);
}
void *ResolvePlayerModel(void *L, int idx, bool raiseError) {
return ResolveTypedObject(L, idx, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID, raiseError);
}
void *ResolveDressUpModel(void *L, int idx, bool raiseError) {
return ResolveTypedObject(L, idx, Offsets::VAR_DRESSUPMODEL_LUA_TYPE_ID, raiseError);
}
// Looks up `_G[name]`. If absent, creates a fresh table and binds it.
// Leaves the resulting table on top of the stack.
@@ -361,11 +367,11 @@ void RegisterGlueFunction(const char *name, CFunction func, const Doc::Function
Api::Documentation::RecordGlobal(name, doc);
}
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count,
const Doc::Method *docs, int docCount) {
void RegisterFrameMethods(void *context, ArrayRef<FrameMethodEntry> methods,
Doc::MethodList docs) {
auto fn = reinterpret_cast<RegisterFrameMethods_t>(Offsets::FUN_REGISTER_FRAME_METHODS);
fn(table, count, context);
Api::Documentation::RecordFrameMethods(context, table, count, docs, docCount);
fn(methods.data, methods.count, context);
Api::Documentation::RecordFrameMethods(context, methods, docs);
}
// Registers `func` at `_G[tableName][methodName]`. If the namespace
@@ -431,17 +437,16 @@ static bool SetIntegerTable(const char *parent, const char *sub,
return true;
}
void RegisterIntegerEnum(const char *parent, const char *sub,
const EnumIntegerEntry *entries, int count,
void RegisterIntegerEnum(const char *parent, const char *sub, ArrayRef<EnumIntegerEntry> entries,
const char *docSystem) {
if (SetIntegerTable(parent, sub, entries, count))
Api::Documentation::RecordEnum(parent, sub, entries, count, docSystem);
if (SetIntegerTable(parent, sub, entries.data, entries.count))
Api::Documentation::RecordEnum(parent, sub, entries, docSystem);
}
void RegisterConstants(const char *sub, const EnumIntegerEntry *entries, int count,
void RegisterConstants(const char *sub, ArrayRef<EnumIntegerEntry> entries,
const char *docSystem) {
if (SetIntegerTable("Constants", sub, entries, count))
Api::Documentation::RecordConstants(sub, entries, count, docSystem);
if (SetIntegerTable("Constants", sub, entries.data, entries.count))
Api::Documentation::RecordConstants(sub, entries, docSystem);
}
void SetFieldNumber(void *L, const char *key, double value) {
+33 -16
View File
@@ -147,6 +147,18 @@ int StrCmpI(const char *a, const char *b, int n = 0x7FFFFFFF);
// literals + file-scope arrays): no heap, no dynamic init.
// - Summaries are user-facing text: one sentence, plain words, present
// tense, no patch versions.
// A view over a file-scope `const T[]` that takes its count from the array's
// type, so no call site passes one; `{}` means none.
template <typename T>
struct ArrayRef {
const T *data = nullptr;
int count = 0;
constexpr ArrayRef() = default;
template <size_t N>
constexpr ArrayRef(const T (&a)[N]) : data(a), count(static_cast<int>(N)) {}
};
namespace Doc {
struct Field {
@@ -172,14 +184,7 @@ constexpr Field Vararg(const char *type, const char *doc = nullptr) {
return {"...", type, true, nullptr, doc};
}
// A view over a file-scope `const Field[]`; `{}` means none.
struct FieldList {
const Field *data = nullptr;
int count = 0;
constexpr FieldList() = default;
template <size_t N>
constexpr FieldList(const Field (&a)[N]) : data(a), count(static_cast<int>(N)) {}
};
using FieldList = ArrayRef<Field>;
// A registered function. Positional init: `{summary, args, rets}` for a
// table function, `{summary, args, rets, "XGlobals"}` for a global, and
@@ -200,6 +205,7 @@ struct Method {
const char *name;
const Function *doc;
};
using MethodList = ArrayRef<Method>;
// A table shape a function returns or accepts → `Tables{Type="Structure"}`
// in `system`. Self-registering: nothing binds a structure to Lua, so it
@@ -240,7 +246,9 @@ constexpr int UpvalueIndex(int i) { return GLOBALS_INDEX - i; }
// `lua_call` / `lua_pcall` nresults sentinel meaning "all".
constexpr int MULTRET = -1;
// Type tag values returned by `Type()` (lua_type).
// Type tag values returned by `Type()` (lua_type). TYPE_NONE is an absent
// argument (past the top of the stack).
constexpr int TYPE_NONE = -1;
constexpr int TYPE_NIL = 0;
constexpr int TYPE_BOOLEAN = 1;
constexpr int TYPE_LIGHTUSERDATA = 2;
@@ -479,6 +487,8 @@ void *ResolveTexture(void *L, int idx = 1, bool raiseError = true);
void *ResolveFontString(void *L, int idx = 1, bool raiseError = true);
void *ResolveEditBox(void *L, int idx = 1, bool raiseError = true);
void *ResolveModel(void *L, int idx = 1, bool raiseError = true);
void *ResolvePlayerModel(void *L, int idx = 1, bool raiseError = true);
void *ResolveDressUpModel(void *L, int idx = 1, bool raiseError = true);
// Every name these registrars bind is ALSO bound under `_G.ClassicAPI` —
// `ClassicAPI.GetSpellInfo`, `ClassicAPI.C_Item.IsBound` — as an escape
@@ -523,10 +533,11 @@ struct FrameMethodEntry {
// Registers a batch of methods on a per-frame-type registry (e.g.
// GameTooltipMethodRegistry for `tooltip:Foo()` calls). `context` is the
// registry address — see Offsets::VAR_*_METHOD_REGISTRY. `docs` pairs
// descriptors to the entries by NAME (any order, any subset).
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count,
const Doc::Method *docs = nullptr, int docCount = 0);
// registry address — see Offsets::VAR_*_METHOD_REGISTRY. `methods` and
// `docs` are file-scope arrays; `docs` pairs descriptors to the entries by
// NAME (any order, any subset).
void RegisterFrameMethods(void *context, ArrayRef<FrameMethodEntry> methods,
Doc::MethodList docs = {});
// Registers `func` at `_G[tableName][methodName]`, creating the namespace
// table if it doesn't already exist. This is how modern WoW C_*-style APIs
@@ -557,14 +568,14 @@ struct EnumIntegerEntry {
// `Enum.AddOnSecurityStatus = { Secure=0, Insecure=1, ... }` shapes.
// `docSystem` is the documentation System the enumeration belongs to
// ("SpellBook", "Item", …); the entries themselves are the documentation.
void RegisterIntegerEnum(const char *parent, const char *sub,
const EnumIntegerEntry *entries, int count,
// `entries` is a file-scope array.
void RegisterIntegerEnum(const char *parent, const char *sub, ArrayRef<EnumIntegerEntry> entries,
const char *docSystem = nullptr);
// Registers `Constants[sub] = { entries }`, Blizzard's engine constant
// tables (`Constants.MoneyFormattingConstants`). Documented as a "Constants"
// table of `docSystem`.
void RegisterConstants(const char *sub, const EnumIntegerEntry *entries, int count,
void RegisterConstants(const char *sub, ArrayRef<EnumIntegerEntry> entries,
const char *docSystem);
// Set `t[key] = value` on the table currently at stack[-1] (the most
@@ -777,6 +788,12 @@ bool RunHookRegistrations();
// issue #33, Frame::Attributes). A pure-C++ cache keyed by spellID/itemID is
// NOT fragile and must not register.
//
// It runs AFTER the frames are gone: CGGameUI::Shutdown destroys every frame
// (and each Model frame's scene with every model in it) before
// CGGameUI::Initialize resets the Lua state. So a cleanup here only FORGETS
// pointers; it never releases an engine object tied to a frame. Release those
// when the frame dies (Model::Teardown for Model frames).
//
// Same lifetime + ordering rules as ModuleAutoRegister: static-init chaining,
// undefined cross-TU order — fine, because each callback only clears its OWN
// state. This is the "clear stale state" category only; a save-before-teardown
+192 -43
View File
@@ -2894,23 +2894,14 @@ enum Offsets {
UNIT_POWER_MIN_TYPE = 0,
UNIT_POWER_MAX_TYPE = 4, // happiness; types 5/6 only valid post-WotLK
// Per-power-type display divisor. Raw descriptor values get
// divided by this before they're surfaced through Lua. Vanilla
// 1.12 stores rage as 0..1000 (internally) and divides by 10 to
// show 0..100; happiness is stored at 1000x scale and divides
// by 1000. Engine reads from `Script_UnitMana` at
// `0x006E7130 + type * 4` (= `0x0086F978`). Same trick the
// 3.3.5/4.x clients use; the table is per-client, the design is
// stable.
//
// [0] = 1 MANA
// [1] = 10 RAGE
// [2] = 1 FOCUS
// [3] = 1 ENERGY
// [4] = 1000 HAPPINESS
VAR_UNIT_POWER_DIVISOR_TABLE = 0x0086F978,
// GetPowerDisplayDivisor — `int __fastcall(int powerType)`, the reader of
// the table above (1 for a negative type). benilla-re spell symbols.
// GetPowerDisplayDivisor — `uint32_t __fastcall(int powerType)`: the
// divisor that turns a stored power amount (descriptor POWER/MAXPOWER,
// Spell.dbc ManaCost, the cost helper's answer) into displayed units.
// Returns `[0x0086F978 + 4·type]`, or 1 for a negative type (verified by
// disassembly). Table {1, 10, 1, 1, 1000}: rage is stored in tenths
// (Shield Slam's 20 rage is 200), happiness at 1000x. Same table as
// 3.3.5's `s_powerDisplayMod`. Read it only through
// `Unit::Power::DisplayDivisor`.
FUN_POWER_DISPLAY_DIVISOR = 0x006E7130,
OFF_UNIT_FIELD_LEVEL = 0x70,
// UNIT_FIELD_BYTES_0 dword at +0x78; byte 3 (= +0x7B) is the unit's
@@ -3259,13 +3250,6 @@ enum Offsets {
// 4.3.4 `GetSpellInfo` reports this instead of the cost (with
// isFunnel = true) when it is positive.
FUN_GET_SPELL_POWER_PER_SECOND = 0x006E3300,
// Divisor that turns a stored power amount into its displayed units:
// `__fastcall uint32_t(int powerType)` → `[0x0086F978 + 4·type]`, 1 for a
// negative type. Table {1, 10, 1, 1, 1000} — rage is stored in tenths
// (Heroic Strike's 15 rage is 150 in Spell.dbc). Verified by disassembly;
// same table as 3.3.5's `s_powerDisplayMod`.
FUN_GET_POWER_DISPLAY_DIVISOR = 0x006E7130,
// The engine's spell-castability verdict for the LOCAL player — the
// function behind `IsUsableAction` for a player spell slot (the spell
// branch of the per-slot recompute `FUN_004E5050`) and behind the
@@ -3361,6 +3345,19 @@ enum Offsets {
CALL_CAST_CHECK_TARGET_RANGE = 0x006E4F26,
FUN_SPELL_TARGET_ARM = 0x006E5250,
CALL_CAST_TARGET_ARM = 0x006E5038,
// SendCast — `void __fastcall(void *pendingCast /*ecx*/, uint8_t flag
// /*dl*/)`, plain ret: builds and sends CMSG_CAST_SPELL / CMSG_USE_ITEM /
// CMSG_PET_CAST_SPELL (4.3.4's Spell_C_SendPendingCast). Its five callers
// (xrefs) all pass the global pending cast (`mov ecx,0xCEAC48`): the
// target arm's two immediate sends, and the object / location / trade-
// slot binders the targeting cursor's click also runs. The arm's one
// caller is the cast request, after the caster check.
FUN_SPELL_SEND_CAST = 0x006E54F0,
CALL_TARGET_ARM_SEND_CAST = 0x006E5349,
CALL_TARGET_ARM_SEND_CAST_GUID = 0x006E5467,
CALL_BIND_TARGET_OBJECT_SEND_CAST = 0x006E60D7,
CALL_BIND_TARGET_LOCATION_SEND_CAST = 0x006E6177,
CALL_BIND_TARGET_TRADE_SLOT_SEND_CAST = 0x006E694F,
// GetSpellHostility — `int __thiscall(const uint8_t *record)`: 2 harmful,
// 1 helpful, 0 neither, from the Targets word and the implicit targets
// (benilla-re spell/handlers#hostility-classifier).
@@ -3390,6 +3387,125 @@ enum Offsets {
// state locally; a no-op while mounted, on a taxi, dead, channeling or
// moving (benilla-re objectlayer/standstate#the-sit-gate).
FUN_PLAYER_SEND_STAND_STATE = 0x005ED430,
// CGUnit_C::IsShapeShifted — `char __thiscall(const void *unit)`, no
// args: the form byte names a SpellShapeshiftForm row without flag bit
// 0 (a stance is not a shapeshift). Disassembled.
FUN_UNIT_IS_SHAPESHIFTED = 0x0060E9F0,
// The two interaction form gates, each `call FUN_UNIT_IS_SHAPESHIFTED`
// (ECX = the player) followed by a test for SHAPESHIFT_FORM_FLAG_CAN_-
// INTERACT; shifted without it refuses (disassembled):
// the NPC right-click (CGPlayer_C::HandleOnRightClick, 0x005F0130):
// ERR_CANT_INTERACT_SHAPESHIFTED, before the range check and walk;
// the game-object use check (the handler's vtable slot 6, 0x005F3130,
// 4.3.4's CGGameObject_C_TypeBase::CanUseNow; every type but door and
// chair): error 0x1A0, only for an object with no Lock row.
CALL_NPC_INTERACT_IS_SHAPESHIFTED = 0x005F0150,
CALL_GAMEOBJECT_CAN_USE_IS_SHAPESHIFTED = 0x005F31E1,
// CGUnit_C::CanInteractNow (0x00606880, `this` = the player, ECX = EDI)
// has the same form test at this call. Its only caller is the hover
// cursor (0x00482310, a .text call scan), which shows the NPC's service
// cursor only when it passes; 4.3.4's CursorTrackUnit asks the same.
CALL_UNIT_CAN_INTERACT_NOW_IS_SHAPESHIFTED = 0x006068B4,
// The use check's mount rung, the same no-Lock-row branch: `mov ecx,
// [eax+0x1FC]` (8B 88 FC 01 00 00, the player's mount display), then
// `test ecx,ecx; jle pass`, then the mailbox test; refuses 0x19F
// (ERR_NOT_WHILE_MOUNTED, no 1.12 text). EDI = the player; EAX is dead.
SITE_GAMEOBJECT_CAN_USE_MOUNT_READ = 0x005F31A8,
// 4.3.4's IsUsableMounted — `bool __thiscall(const void *const *fields)`,
// ECX = &object->descriptor: GAMEOBJECT_TYPE_ID is 19, the mailbox.
FUN_GAMEOBJECT_USABLE_MOUNTED = 0x0047CFF0,
OFF_GAMEOBJECT_DESCRIPTOR = 0x110,
// A type handler's object pointer (benilla-re objectlayer/gameobjects).
OFF_GAMEOBJECT_HANDLER_OBJECT = 0x4,
// The default use (slot 7, 0x005F33E0, 4.3.4's TypeBase::Use): its
// unlocked, no-spell branch sends CMSG_GAMEOBJ_USE, opening with `mov
// edi,0x7FF9E4` (BF E4 F9 7F 00, the CDataStore vtable). [EBP-4] = the
// handler; EAX, ECX and EDX are dead there.
SITE_GAMEOBJECT_USE_SEND = 0x005F35DF,
// The two types whose slot 7 replaces the default use (vtable dump):
// the mailbox (opens the mail frame) and text (opens the page). Both
// `char __thiscall(void *handler)`, no args, return 1.
SLOT_GAMEOBJECT_MAILBOX_USE = 0x0080BDBC,
FUN_GAMEOBJECT_MAILBOX_USE = 0x005F6820,
SLOT_GAMEOBJECT_TEXT_USE = 0x0080B954,
FUN_GAMEOBJECT_TEXT_USE = 0x005F58C0,
// The rest of 4.3.4's CanAutoStand / CanAutoDismount / CanAutoUnshift
// sites, each found as the matching 1.12 gate (disassembled):
//
// Stand-state reads before a loot, `call [reg+0xA4]` (GetStandState, 6
// bytes) with ECX = the player, then `test eax,eax; jnz` to
// ERR_LOOT_NOTSTANDING (4.3.4 CGUnit_C::OnRightClick / CGCorpse_C::
// OnRightClick): the unit right-click's loot step (FF 90 A4 00 00 00)
// and the corpse right-click (0x005D6BF0, FF 92 A4 00 00 00).
SITE_UNIT_LOOT_STAND_CALL = 0x0060BFB7,
SITE_CORPSE_LOOT_STAND_CALL = 0x005D6C3B,
// The unit right-click's loot step skips a mounted player: `mov eax,
// [ecx+0x1FC]` (8B 81 FC 01 00 00), `test eax,eax; jg skin`. ESI = the
// player; ECX and EDX are dead. 4.3.4 has no such test there and its
// LootUnit dismounts instead.
SITE_UNIT_LOOT_MOUNT_READ = 0x0060BF98,
// CGPlayer_C::LootUnit (0x005DF2A0) past its reach / walk-up, before the
// loot window close and CMSG_LOOT: `push 0; xor dl,dl; mov cl,1` (6A 00
// 32 D2 B1 01) for FUN_0048F200(1, 0, 0). ESI = the player; EAX is
// dead. 4.3.4's LootUnit dismounts at this point.
SITE_LOOT_UNIT_SEND = 0x005DF3B3,
// The item use's form refusal (0x005D8D00, 4.3.4 CGItem_C::Use):
// `push 0x15E` (68 5E 01 00 00, ERR_NO_ITEMS_WHILE_SHAPESHIFTED), then
// `jmp` to the error tail. ESI = the player; EAX, ECX, EDX are dead.
// ITEM_USE_FORM_PASSED is where the form test's pass jumps. Turtle's exe
// makes the refusal unreachable (`jmp` over the form test at
// 0x005D9139); the stock 1.12.1 exe reaches it.
SITE_ITEM_USE_FORM_REFUSAL = 0x005D914D,
ITEM_USE_FORM_PASSED = 0x005D9157,
// Past the form test, the item use's toggle test (4.3.4 runs it before
// the form cancel): a use spell with an activeIconID whose aura is on
// the player is cancelled instead of cast, `mov ecx,[ebp-0xC]` (spell)
// then CALL_ITEM_USE_TOGGLE_CANCEL → FUN_CANCEL_AURA_SEND, return 0.
// Every other path joins at SITE_ITEM_USE_BIND_TEST, the bind-on-use
// test: `mov ecx,[ebp-8]; cmp dword [ecx+0x194],3` (8B 4D F8 83 B9 94
// 01 00 00 03), then `jne`. There EDI = the spell (read after), EBX and
// EBP live; EAX, ECX, EDX are reloaded before use (disassembled).
CALL_ITEM_USE_TOGGLE_CANCEL = 0x005D9237,
SITE_ITEM_USE_BIND_TEST = 0x005D91D3,
// The mount reads in 4.3.4's CGUnit_C::CanAttackNow, inlined in 1.12
// (each followed by `test; jg refuse`):
// the unit right-click's attack step (8B 82 FC 01 00 00), player ESI,
// EAX out, ECX and EDX dead;
// the hover cursor's attack cursor (8B 82 FC 01 00 00), player EDI,
// EAX out, ECX and EDX dead;
// StartAttack (0x005ECB70, 8B 81 FC 01 00 00), player ESI, EAX out,
// ECX and EDX dead;
// the attack resolver (0x00612DF0, 4.3.4 VerifyAttackIconPressed's
// ERR_ATTACK_MOUNTED, 8B 81 FC 01 00 00), player EDI, EAX out;
// the resolver's can-attack step (8B 90 FC 01 00 00), player ECX
// (live after), EDX out, EAX dead.
SITE_RIGHT_CLICK_ATTACK_MOUNT_READ = 0x0060C1B4,
SITE_CURSOR_ATTACK_MOUNT_READ = 0x00482659,
SITE_START_ATTACK_MOUNT_READ = 0x005ECC06,
SITE_ATTACK_RESOLVER_MOUNT_READ = 0x00613039,
SITE_ATTACK_RESOLVER_CAN_ATTACK_MOUNT_READ = 0x00613142,
// StartAttack once every test passed, before the swing: `mov eax,[esi+
// 0xC8C]` (8B 86 8C 0C 00 00), ESI = the player, EDI = the target; ECX
// and EDX are dead. START_ATTACK_RETURN is its plain epilogue. 4.3.4's
// CombatModeEnter dismounts here.
SITE_START_ATTACK_PASSED = 0x005ECC56,
START_ATTACK_RETURN = 0x005ECD9A,
// ERR_OUT_OF_RANGE's message id (benilla-re tables/messages.tsv).
GAME_ERROR_OUT_OF_RANGE = 125,
// CGPlayer_C::CanAutoInteract — `bool __thiscall(const void *player)`,
// no args: a live active mover with the click-to-move cvar on
// (disassembled). 4.3.4's right-click passes it as CombatModeEnter's
// `silent`, so the out-of-reach error stays quiet while the player walks.
FUN_PLAYER_CAN_AUTO_INTERACT = 0x0060F900,
// The taxi map opener (0x004DB710) ends like 4.3.4's: TAXIMAP_OPENED,
// then tutorial 34 through FUN_TRIGGER_TUTORIAL (`void __fastcall(int
// id, int)`) at this call; 4.3.4 then stands, dismounts and leaves the
// form when the map has more than one node (VAR_TAXI_MAP_NODE_COUNT,
// the count the opener's TaxiNodeSetCurrent loop runs to).
CALL_TAXI_MAP_OPENED_TUTORIAL = 0x004DBAA2,
FUN_TRIGGER_TUTORIAL = 0x004B5390,
VAR_TAXI_MAP_NODE_COUNT = 0x00BB4AA8,
// Effective spell/channel duration (ms):
// `__fastcall int(const void *spellRecord, int pet /*0 = player,
@@ -4034,6 +4150,13 @@ enum Offsets {
// Slot 36 of the Model and pane vtables; DressUpModel's own slot 36
// (0x00504350) calls it first.
FUN_SIMPLEMODEL_SET_MODEL_INSTANCE = 0x0076CD30,
// Every Model frame's member teardown (fastcall frame), called by the
// CSimpleModel deleting dtor 0x0076CA20 and CGCharacterModelBase's dtor
// 0x00505870 (PlayerModel, DressUpModel). It releases the frame's M2
// scene (+0x314), which frees the models in it even when something else
// still holds a reference (seen live: a kept compositor's release faulted
// on the freed model after /reload).
FUN_SIMPLEMODEL_DESTROY = 0x0076CA50,
// The Model frame's per-paint update — `__thiscall(frame)`, slot 38.
FUN_SIMPLEMODEL_UPDATE_MODEL = 0x0076D1A0,
@@ -6052,15 +6175,37 @@ enum Offsets {
// (21) set only the main-hand bit, off-hand-only (22) only the off-hand.
VAR_INVTYPE_SLOT_MASKS = 0x00809200,
// The two `m_inventoryType` values that mean "consumed when used", so
// both the item-consumable test (`C_Item.IsConsumableItem`) and the
// action-slot one (`IsConsumableAction`) single them out. Indices into
// the table above: 24 = `INVTYPE_AMMO`, 25 = `INVTYPE_THROWN`.
// `m_inventoryType` (`OFF_ITEMSTATS_INVENTORY_TYPE`) values, named by
// their key in the string table above (read from WoW.exe).
INVTYPE_NON_EQUIP = 0,
INVTYPE_HEAD = 1,
INVTYPE_NECK = 2,
INVTYPE_SHOULDER = 3,
INVTYPE_BODY = 4,
INVTYPE_CHEST = 5,
INVTYPE_WAIST = 6,
INVTYPE_LEGS = 7,
INVTYPE_FEET = 8,
INVTYPE_WRIST = 9,
INVTYPE_HAND = 10,
INVTYPE_FINGER = 11,
INVTYPE_TRINKET = 12,
INVTYPE_WEAPON = 13,
INVTYPE_SHIELD = 14,
INVTYPE_RANGED = 15,
INVTYPE_CLOAK = 16,
INVTYPE_2HWEAPON = 17,
INVTYPE_BAG = 18,
INVTYPE_TABARD = 19,
INVTYPE_ROBE = 20,
INVTYPE_WEAPONMAINHAND = 21,
INVTYPE_WEAPONOFFHAND = 22,
INVTYPE_HOLDABLE = 23,
INVTYPE_AMMO = 24,
INVTYPE_THROWN = 25,
// Index 16 of the table above (read from WoW.exe). The bag search skips
// a cloak's subclass name ("Cloth"), as 4.3.4's does.
INVTYPE_CLOAK = 16,
INVTYPE_RANGEDRIGHT = 26,
INVTYPE_QUIVER = 27,
INVTYPE_RELIC = 28,
// Faction "displayed list" — the engine maintains a sorted/visible list
// of factions the player has rep with. `Script_GetNumFactions` (at
@@ -8534,17 +8679,20 @@ enum Offsets {
// unit-event observers. Matches nampower's `CGRaidInfo_m_raidtargets`.
VAR_RAID_TARGET_GUIDS = 0x00B71368,
VAR_LOCALE_INDEX = 0x00C0E080, // 0..8, picks one of the 9 localized strings
// 0..7: start-up stores the Wow.ini Language+Country tag's position in
// VAR_LOCALE_NAME_TABLE (0x402562), 0 when it matches none (enGB).
// Picks a DBC localized-string column. NOT 4.3.4's nine-slot locale
// order — language rules go by name (Localization::Locale).
VAR_LOCALE_INDEX = 0x00C0E080,
// Locale-name string table — a parallel `const char *[8]` indexed by
// VAR_LOCALE_INDEX, holding the exact codes `GetLocale()` returns:
// enUS / koKR / frFR / deDE / zhCN / zhTW / esES / xxYY. This is the
// source `Script_GetLocale` (FUN_0048d8b0) reads:
// enUS / koKR / frFR / deDE / zhCN / zhTW / esES / xxYY on a stock
// client. This is the source `Script_GetLocale` (FUN_0048d8b0) reads:
// lua_pushstring(L, (&PTR_DAT_008558a4)[DAT_00c0e080]);
// Verified by resolving all 8 entries from the binary. Index range is
// 0..7 (VAR_LOCALE_INDEX is documented 0..8, but slot 8 overreads into
// the string pool — clamp to 0..7 before reading). Used by the TOC
// `[TextLocale]` / `[AllowLoadTextLocale]` directives (AddOns::TocRewrite).
// and the list SetAddOnMetadata (0x51D580) matches a TOC key's
// `-xxXX` suffix against. Verified by resolving all 8 entries from the
// binary. Read through Localization::Locale::Client().
VAR_LOCALE_NAME_TABLE = 0x008558A4,
LUA_IS_NUMBER = 0x6F34D0,
@@ -10819,10 +10967,6 @@ enum Offsets {
INVSLOT_OFFHAND = 17,
INVSLOT_MAINHAND = 16,
// INVTYPE (`OFF_ITEMSTATS_INVENTORY_TYPE`) of a two-handed weapon,
// which occupies the main-hand slot and blocks the off-hand.
INVTYPE_2HWEAPON = 17,
// Character-pane equipment slot range: 1..19 (head=1, neck=2, …,
// tabard=19). Used by `C_Item.IsEquippedItem` to walk every slot
// looking for a matching item.
@@ -11927,11 +12071,16 @@ enum Offsets {
// column 11: the parsed 1.12 DBC gives 0x7 for the warrior stances, 0x1
// Stealth, 0x9 Shadowform, matching 3.3.5's column. 0x100 is the form
// flag 3.3.5 / 4.3.4 `CGUnit_C::CanAutoUnshift` refuses on (set there
// only on Spirit of Redemption and the Ghoul forms); no 1.12 row has it.
// only on Spirit of Redemption and the Ghoul forms); no 1.12 row has it,
// so Spell::AutoPrepare adds it to Spirit of Redemption (form 32).
VAR_SPELL_SHAPESHIFT_FORM_RECORDS = 0x00C0D774,
VAR_SPELL_SHAPESHIFT_FORM_COUNT = 0x00C0D778,
OFF_SPELL_SHAPESHIFT_FORM_FLAGS = 0x2C,
SHAPESHIFT_FORM_FLAG_NO_AUTO_UNSHIFT = 0x100,
// Flag 0x8: the form may talk to NPCs and use objects (Shadowform's 0x9).
// Tested by both interaction gates (`test byte [eax+0x2C],0x8` at
// 0x005F017F / 0x005F3210) and 4.3.4's HandleOnRightClick / CanUseNow.
SHAPESHIFT_FORM_FLAG_CAN_INTERACT = 0x8,
// Control-loss aura effect types (EffectApplyAuraName values), used by
// C_LossOfControl to classify a debuff. Values are authoritative from the
+1 -3
View File
@@ -170,9 +170,7 @@ static void RegisterLuaFunctions() {
&Script_HasSpellActionButtons, &kHasSpellActionButtons);
Game::Lua::RegisterTableFunction("C_ActionBar", "IsOnBarOrSpecialBar",
&Script_IsOnBarOrSpecialBar, &kIsOnBarOrSpecialBar);
Game::Lua::RegisterIntegerEnum("Enum", "ActionBarSet", kActionBarSet,
static_cast<int>(sizeof(kActionBarSet) / sizeof(kActionBarSet[0])),
"ActionBar");
Game::Lua::RegisterIntegerEnum("Enum", "ActionBarSet", kActionBarSet, "ActionBar");
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -5
View File
@@ -408,11 +408,7 @@ static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnInterfaceVersion",
&Script_GetAddOnInterfaceVersion,
&kGetAddOnInterfaceVersion);
Game::Lua::RegisterIntegerEnum(
"Enum", "AddOnSecurityStatus",
kAddOnSecurityStatusEntries,
sizeof(kAddOnSecurityStatusEntries) /
sizeof(kAddOnSecurityStatusEntries[0]));
Game::Lua::RegisterIntegerEnum("Enum", "AddOnSecurityStatus", kAddOnSecurityStatusEntries);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+4 -13
View File
@@ -14,6 +14,7 @@
#include "addons/EngineIO.h"
#include "addons/TocRewrite.h"
#include "addons/Toc.h"
#include "localization/Locale.h"
#include "Offsets.h"
#include "turtle/Detect.h"
@@ -34,17 +35,7 @@ using AddOns::Toc::Lower;
bool IsSpace(char c) { return c == ' ' || c == '\t'; }
// The client locale code ("enUS", "frFR", …) — the exact string
// GetLocale() returns (Offsets.h VAR_LOCALE_NAME_TABLE derivation).
const char *ClientLocale() {
uint32_t idx = *reinterpret_cast<const uint32_t *>(
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
if (idx > 7) idx = 0; // slot 8 overreads the string pool — clamp
const char *const *table = reinterpret_cast<const char *const *>(
static_cast<uintptr_t>(Offsets::VAR_LOCALE_NAME_TABLE));
const char *s = table[idx];
return (s != nullptr) ? s : "enUS";
}
using Localization::Locale::Client;
// True iff the comma-separated `list` (spaces allowed) contains `token` (CI).
bool ListContains(const char *list, size_t listLen, const char *token) {
@@ -66,7 +57,7 @@ bool ListContains(const char *list, size_t listLen, const char *token) {
const char *VariableValue(const char *head, size_t headLen) {
if (EqCI(head, headLen, "Family")) return "Classic";
if (EqCI(head, headLen, "Game")) return "Vanilla";
if (EqCI(head, headLen, "TextLocale")) return ClientLocale();
if (EqCI(head, headLen, "TextLocale")) return Client();
return nullptr;
}
@@ -158,7 +149,7 @@ bool EvalCondition(const char *head, size_t headLen, const char *args,
if (EqCI(head, headLen, "ExcludeLoadGameType"))
return !ListContainsGameType(args, argsLen);
if (EqCI(head, headLen, "AllowLoadTextLocale"))
return ListContains(args, argsLen, ClientLocale());
return ListContains(args, argsLen, Client());
if (EqCI(head, headLen, "AllowLoad"))
return ListContains(args, argsLen, "game"); // addon files: in-game only
return false;
+2 -3
View File
@@ -560,9 +560,8 @@ void RegisterLuaFunctions() {
RegisterClass(Anim::s_rotationClass);
RegisterClass(Anim::s_pathClass);
RegisterClass(ControlPoint::s_class);
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), kRegionMethods,
static_cast<int>(sizeof(kRegionMethods) / sizeof(kRegionMethods[0])),
kRegionDocs, static_cast<int>(sizeof(kRegionDocs) / sizeof(kRegionDocs[0])));
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), kRegionMethods, kRegionDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+12 -7
View File
@@ -665,10 +665,15 @@ void RecordTable(const char *table, const char *name, const Function *doc) {
Record(KIND_TABLE, table, name, nullptr, doc);
}
void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry *table,
int count, const Method *docs, int docCount) {
void RecordFrameMethods(const void *registry,
Game::ArrayRef<Game::Lua::FrameMethodEntry> methods,
Game::Doc::MethodList docList) {
if (g_pass == ENV_NONE)
return;
const Game::Lua::FrameMethodEntry *table = methods.data;
const int count = methods.count;
const Method *docs = docList.data;
const int docCount = docList.count;
for (int i = 0; i < count; ++i) {
const Function *doc = nullptr;
for (int d = 0; d < docCount; ++d) {
@@ -720,14 +725,14 @@ void RecordValueTable(Kind kind, const char *parent, const char *sub,
} // namespace
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
int count, const char *system) {
RecordValueTable(KIND_ENUM, parent, sub, entries, count, system);
void RecordEnum(const char *parent, const char *sub,
Game::ArrayRef<Game::Lua::EnumIntegerEntry> entries, const char *system) {
RecordValueTable(KIND_ENUM, parent, sub, entries.data, entries.count, system);
}
void RecordConstants(const char *sub, const Game::Lua::EnumIntegerEntry *entries, int count,
void RecordConstants(const char *sub, Game::ArrayRef<Game::Lua::EnumIntegerEntry> entries,
const char *system) {
RecordValueTable(KIND_CONSTANTS, "Constants", sub, entries, count, system);
RecordValueTable(KIND_CONSTANTS, "Constants", sub, entries.data, entries.count, system);
}
} // namespace Api::Documentation
+6 -5
View File
@@ -32,14 +32,15 @@ void EndPass();
void RecordGlobal(const char *name, const Game::Doc::Function *doc);
void RecordTable(const char *table, const char *name, const Game::Doc::Function *doc);
void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry *table,
int count, const Game::Doc::Method *docs, int docCount);
void RecordFrameMethods(const void *registry,
Game::ArrayRef<Game::Lua::FrameMethodEntry> methods,
Game::Doc::MethodList docs);
// Names the widget system of a method hash this DLL owns (a frame class it
// adds), alongside the engine registries the exporter already knows.
void NameWidgetSystem(const void *registry, const char *system);
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
int count, const char *system);
void RecordConstants(const char *sub, const Game::Lua::EnumIntegerEntry *entries, int count,
void RecordEnum(const char *parent, const char *sub,
Game::ArrayRef<Game::Lua::EnumIntegerEntry> entries, const char *system);
void RecordConstants(const char *sub, Game::ArrayRef<Game::Lua::EnumIntegerEntry> entries,
const char *system);
} // namespace Api::Documentation
+2 -9
View File
@@ -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
+4 -6
View File
@@ -457,12 +457,10 @@ static void RegisterLuaFunctions() {
&Script_GetAutoCompleteResults, &kGetAutoCompleteResults);
Game::Lua::RegisterTableFunction("C_AutoComplete", "IsRecognizedName", &Script_IsRecognizedName,
&kIsRecognizedName);
Game::Lua::RegisterIntegerEnum("Enum", "AutoCompleteEntryFlag", kEntryFlagEntries,
sizeof(kEntryFlagEntries) / sizeof(kEntryFlagEntries[0]),
"AutoComplete");
Game::Lua::RegisterIntegerEnum("Enum", "AutoCompletePriority", kPriorityEntries,
sizeof(kPriorityEntries) / sizeof(kPriorityEntries[0]),
"AutoComplete");
Game::Lua::RegisterIntegerEnum(
"Enum", "AutoCompleteEntryFlag", kEntryFlagEntries, "AutoComplete");
Game::Lua::RegisterIntegerEnum(
"Enum", "AutoCompletePriority", kPriorityEntries, "AutoComplete");
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+21 -9
View File
@@ -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);
@@ -497,9 +511,7 @@ const Game::Lua::EnumIntegerEntry kSwingTypeEntries[] = {
};
void RegisterLuaFunctions() {
Game::Lua::RegisterIntegerEnum(
"Enum", "PlayerSwingType", kSwingTypeEntries,
sizeof(kSwingTypeEntries) / sizeof(kSwingTypeEntries[0]), "SwingTimer");
Game::Lua::RegisterIntegerEnum("Enum", "PlayerSwingType", kSwingTypeEntries, "SwingTimer");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -14
View File
@@ -45,6 +45,7 @@
#include "time/Clock.h"
#include "unit/Focus.h"
#include "unit/Identity.h"
#include "unit/Power.h"
#include <ctime>
@@ -617,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;
@@ -684,9 +674,7 @@ void SetItem(Entry &e, int itemId) {
}
int DisplayAmount(int amount, int powerType) {
using Divisor_t = int(__fastcall *)(int powerType);
const int divisor = reinterpret_cast<Divisor_t>(
static_cast<uintptr_t>(Offsets::FUN_POWER_DISPLAY_DIVISOR))(powerType);
const int divisor = static_cast<int>(Unit::Power::DisplayDivisor(powerType));
return divisor != 0 ? amount / divisor : amount;
}
-4
View File
@@ -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);
+2 -1
View File
@@ -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;
+2 -6
View File
@@ -145,12 +145,8 @@ void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("ConsoleGetAllCommands", &Script_GetAllCommands);
Game::Lua::RegisterGlobalFunction("ConsolePrintAllMatchingCommands",
&Script_ConsolePrintAllMatchingCommands);
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCommandType", kCommandTypeEntries,
sizeof(kCommandTypeEntries) /
sizeof(kCommandTypeEntries[0]));
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCategory", kCategoryEntries,
sizeof(kCategoryEntries) /
sizeof(kCategoryEntries[0]));
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCommandType", kCommandTypeEntries);
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCategory", kCategoryEntries);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -2
View File
@@ -42,8 +42,7 @@ const Game::Lua::EnumIntegerEntry kEntries[] = {
};
void RegisterLuaFunctions() {
Game::Lua::RegisterIntegerEnum("Enum", "BagIndex", kEntries,
sizeof(kEntries) / sizeof(kEntries[0]), "BagIndexConstants");
Game::Lua::RegisterIntegerEnum("Enum", "BagIndex", kEntries, "BagIndexConstants");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+31 -13
View File
@@ -55,6 +55,7 @@
#include "item/Location.h"
#include "item/Record.h"
#include "item/TypeInfo.h"
#include "localization/Locale.h"
#include "unit/Identity.h"
#include <algorithm>
@@ -86,12 +87,29 @@ char16_t ToLower(char16_t c) {
return c;
}
// Appends the lowercased `c` with 4.3.4's accent folding for `locale`
// (frFR 2, deDE 3, esES/esMX 6/7, ruRU 8); æ, œ and ß become two letters.
// Returns the new length.
size_t AppendFolded(int locale, char16_t c, char16_t *out, size_t n) {
switch (locale) {
case 2: // frFR
// 4.3.4's accent-folding sets. 4.3.4 picks one by its locale index (frFR 2,
// deDE 3, esES/esMX 6/7, ruRU 8); 1.12's index has other slots, so the set
// is picked by locale name.
enum class Folding { None, French, German, Spanish, Russian };
Folding ClientFolding() {
using Localization::Locale::Is;
if (Is("frFR"))
return Folding::French;
if (Is("deDE"))
return Folding::German;
if (Is("esES") || Is("esMX"))
return Folding::Spanish;
if (Is("ruRU"))
return Folding::Russian;
return Folding::None;
}
// Appends the lowercased `c` with `folding`'s accent folding; æ, œ and ß
// become two letters. Returns the new length.
size_t AppendFolded(Folding folding, char16_t c, char16_t *out, size_t n) {
switch (folding) {
case Folding::French:
switch (c) {
case 0xE0: case 0xE2: case 0xE4: c = u'a'; break;
case 0xE6: out[n++] = u'a'; c = u'e'; break;
@@ -103,14 +121,13 @@ size_t AppendFolded(int locale, char16_t c, char16_t *out, size_t n) {
case 0xF9: case 0xFA: case 0xFB: case 0xFC: c = u'u'; break;
}
break;
case 3: // deDE
case Folding::German:
if (c == 0xDF) {
out[n++] = u's';
c = u's';
}
break;
case 6: // esES
case 7: // esMX
case Folding::Spanish:
switch (c) {
case 0xE1: c = u'a'; break;
case 0xE9: c = u'e'; break;
@@ -120,10 +137,12 @@ size_t AppendFolded(int locale, char16_t c, char16_t *out, size_t n) {
case 0xF1: c = u'n'; break;
}
break;
case 8: // ruRU
case Folding::Russian:
if (c == 0x451)
c = 0x435;
break;
case Folding::None:
break;
}
out[n++] = c;
return n;
@@ -141,11 +160,10 @@ size_t Fold(const char *text, char16_t (&out)[kFoldCap]) {
// One unit short of the buffer: a full conversion stores no terminator.
reinterpret_cast<Convert_t>(Offsets::FUN_SUNI_CONVERT_UTF8_TO_16)(
wide, kFoldCap - 1, text, 0x7FFFFFFF, nullptr, nullptr);
const int locale =
*reinterpret_cast<const int *>(static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
const Folding folding = ClientFolding();
size_t n = 0;
for (size_t i = 0; wide[i] != 0 && n + 2 < kFoldCap; ++i)
n = AppendFolded(locale, ToLower(wide[i]), out, n);
n = AppendFolded(folding, ToLower(wide[i]), out, n);
out[n] = 0;
return n;
}
+6 -4
View File
@@ -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
+6
View File
@@ -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
+2 -4
View File
@@ -27,10 +27,8 @@ const Game::Lua::EnumIntegerEntry kMoneyFormattingConstants[] = {
};
void RegisterLuaFunctions() {
Game::Lua::RegisterConstants("MoneyFormattingConstants", kMoneyFormattingConstants,
sizeof(kMoneyFormattingConstants) /
sizeof(kMoneyFormattingConstants[0]),
"MoneyConstants");
Game::Lua::RegisterConstants(
"MoneyFormattingConstants", kMoneyFormattingConstants, "MoneyConstants");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -119,9 +119,7 @@ void OnTick() {
const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
void RegisterLuaFunctions() {
Game::Lua::RegisterIntegerEnum(
"Enum", "UICursorType", kCursorTypeEntries,
sizeof(kCursorTypeEntries) / sizeof(kCursorTypeEntries[0]));
Game::Lua::RegisterIntegerEnum("Enum", "UICursorType", kCursorTypeEntries);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -3
View File
@@ -206,9 +206,8 @@ void RegisterLuaFunctions() {
&Script_GetSelfResurrectOptions, &kGetSelfResurrectOptions);
Game::Lua::RegisterTableFunction("C_DeathInfo", "UseSelfResurrectOption",
&Script_UseSelfResurrectOption, &kUseSelfResurrectOption);
Game::Lua::RegisterIntegerEnum("Enum", "SelfResurrectOptionType", kOptionTypeEntries,
sizeof(kOptionTypeEntries) / sizeof(kOptionTypeEntries[0]),
"DeathInfo");
Game::Lua::RegisterIntegerEnum(
"Enum", "SelfResurrectOptionType", kOptionTypeEntries, "DeathInfo");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -47,7 +47,6 @@
#include <cmath>
#include <cstdio>
#include <iterator>
namespace Duration::Object {
@@ -451,8 +450,7 @@ const Game::Doc::Function kCreateDuration{"Creates a zero duration object that c
void RegisterLuaFunctions() {
CreateMetatable(Lua::State());
Lua::RegisterTableFunction("C_DurationUtil", "CreateDuration", &Script_CreateDuration, &kCreateDuration);
Lua::RegisterIntegerEnum("Enum", "DurationTimeModifier", kModifiers, static_cast<int>(std::size(kModifiers)),
"LuaDurationObjectShared");
Lua::RegisterIntegerEnum("Enum", "DurationTimeModifier", kModifiers, "LuaDurationObjectShared");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -3
View File
@@ -58,7 +58,6 @@
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iterator>
#include <string>
#include <vector>
@@ -780,8 +779,8 @@ void RegisterLuaFunctions() {
CreateMetatable(Lua::State());
Lua::RegisterTableFunction("C_DurationUtil", "CreateDurationTextBinding", &Script_CreateDurationTextBinding,
&kCreateDurationTextBinding);
Lua::RegisterIntegerEnum("Enum", "DurationTextBindingProperty", kProperties,
static_cast<int>(std::size(kProperties)), "DurationTextBindingShared");
Lua::RegisterIntegerEnum(
"Enum", "DurationTextBindingProperty", kProperties, "DurationTextBindingShared");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -2
View File
@@ -125,8 +125,8 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -83,9 +83,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -86,9 +86,7 @@ void RegisterLuaFunctions() {
Common::RedirectCall(Offsets::CALL_EDITBOX_IME_COMPOSITION_INSERT, Offsets::FUN_EDITBOX_INSERT,
reinterpret_cast<const void *>(&CompositionInsert));
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -2
View File
@@ -30,8 +30,7 @@ const Game::Lua::EnumIntegerEntry kEntries[] = {
};
void RegisterLuaFunctions() {
Game::Lua::RegisterIntegerEnum("Enum", "InputContext", kEntries,
sizeof(kEntries) / sizeof(kEntries[0]), "InputConstants");
Game::Lua::RegisterIntegerEnum("Enum", "InputContext", kEntries, "InputConstants");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -234,9 +234,7 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY),
g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
@@ -1,6 +1,7 @@
## Interface: 11200
## Title: !!!ClassicAPI
## Notes: Library to emulate aspects of the modern WoW API on 1.12.1 / Lua 5.0.
## Notes-ruRU: Библиотека, эмулирующая аспекты современного API WoW на 1.12.1 / Lua 5.0.
## Version: DEV
## Author: Brues
## X-Website: https://github.com/brues-code/ClassicAPI
@@ -13,11 +13,11 @@ CLASSICAPI_ACTION_TARGETING = "Action Targeting";
CLASSICAPI_ADVANCED = "Advanced";
CLASSICAPI_ALL_SOFT_TARGET_ICONS = "Icons for All Soft Targets";
CLASSICAPI_AUTO_DISMOUNT = "Auto Dismount";
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell while you are mounted.";
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell, use an object, loot, attack, or open the flight map while you are mounted.";
CLASSICAPI_AUTO_STAND = "Auto Stand";
CLASSICAPI_AUTO_STAND_TOOLTIP = "Stand up when you cast a spell while you sit.";
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.";
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";
+150 -52
View File
@@ -4,17 +4,109 @@ ACTION_BUTTON_USE_KEY_DOWN = "Способности при нажатии кл
ACTION_TARGETING_OPTION = "Разрешить динамическое выделение";
ADDON_MEM_KB_ABBR = "(%.0f КБ) %s";
ADDON_MEM_MB_ABBR = "(%.2f МБ) %s";
ADDON_OPTIONS = "Параметры модификаций";
BINDING_NAME_AOELOOT = "Сбор добычи по области";
BINDING_NAME_FOCUSTARGET = "Запомненная цель";
BINDING_NAME_INTERACTMOUSEOVER = "Взаимодействовать указателем мыши";
BINDING_NAME_INTERACTMOUSEOVER = "Взаимодействовать с объектом под курсором";
BINDING_NAME_INTERACTTARGET = "Взаимодействовать с целью";
BINDING_NAME_TARGETFOCUS = "Восстановить цель";
CLASSICAPI_ACTION_BARS = "Панели команд";
CLASSICAPI_ACTION_TARGETING = "Динамическое выделение";
CLASSICAPI_ADVANCED = "Дополнительно";
CLASSICAPI_ALL_SOFT_TARGET_ICONS = "Значки для всех мягких целей";
CLASSICAPI_AUTO_DISMOUNT = "Автоматически спешиваться";
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Спешиваться при применении заклинания, использовании объекта, сборе добычи, атаке или открытии карты полетов, если вы едете верхом.";
CLASSICAPI_AUTO_STAND = "Автоматически вставать";
CLASSICAPI_AUTO_STAND_TOOLTIP = "Вставать при применении заклинания, сборе добычи или открытии карты полетов, если вы сидите.";
CLASSICAPI_AUTO_UNSHIFT = "Автоматически снимать облик";
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Покидать свой облик при применении заклинания, которое в нем недоступно, использовании предмета, а также при открытии карты полетов. Также позволяет говорить с NPC и использовать объекты в обликах друида и призрачного волка.";
CLASSICAPI_BAGS_AND_ITEMS = "Сумки и предметы";
CLASSICAPI_CASTING_AND_ATTACKING = "Заклинания и атаки";
CLASSICAPI_COMBAT_LOG_RETENTION = "Хранение журнала боя";
CLASSICAPI_COMBAT_LOG_RETENTION_TOOLTIP = "Сколько секунд журнал боя хранит записи для модификаций.";
CLASSICAPI_CURSOR = "Курсор";
CLASSICAPI_DIRECTION_ANY = "Любое направление";
CLASSICAPI_DIRECTION_CONE = "В пределах 30 градусов";
CLASSICAPI_DIRECTION_FRONT = "Прямо перед собой";
CLASSICAPI_ENEMIES = "Противники";
CLASSICAPI_ENEMY_DIRECTION = "Направление на противника";
CLASSICAPI_ENEMY_ICON = "Показывать значок противника";
CLASSICAPI_ENEMY_ICON_TOOLTIP = "Показывает значок над противником, которого выбирает динамическое выделение.";
CLASSICAPI_ENEMY_RANGE = "Дистанция до противников";
CLASSICAPI_EXPANDED_CURSORS = "Показывать расширенные курсоры";
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "При наведении на NPC или объект курсор показывает, что с ним можно сделать. Например, наковальню для ремонта, ! или ? для заданий и крючок для вашего поплавка.";
CLASSICAPI_FRIEND_DIRECTION = "Направление на союзника";
CLASSICAPI_FRIEND_ICON = "Показывать значок союзника";
CLASSICAPI_FRIEND_ICON_TOOLTIP = "Показывает значок над дружественной целью, которую выбирает дружественное динамическое выделение.";
CLASSICAPI_FRIEND_RANGE = "Дистанция до союзников";
CLASSICAPI_FRIEND_TARGETING = "Включить дружественное динамическое выделение";
CLASSICAPI_FRIEND_TARGETING_TOOLTIP = "Выбирает ближайшую дружественную цель в поле зрения. Если у вас нет цели, полезные заклинания применяются к ней.";
CLASSICAPI_FRIENDS = "Союзники";
CLASSICAPI_GAME_OBJECT_ICON = "Показывать значок взаимодействия на объектах";
CLASSICAPI_GAME_OBJECT_ICON_TOOLTIP = "Также показывает значок взаимодействия над объектами, которые нельзя выбрать целью, например почтовыми ящиками и сундуками.";
CLASSICAPI_HIDE_EQUIPMENT_SETS = "Скрывать комплекты экипировки на предметах";
CLASSICAPI_HIDE_EQUIPMENT_SETS_TOOLTIP = "Убирает строку с комплектами экипировки из подсказок предметов.";
CLASSICAPI_ICON_SIZE = "Размер значков";
CLASSICAPI_ICON_SIZE_TOOLTIP = "Размер значков взаимодействия, противников и союзников.";
CLASSICAPI_IGNORE_BACKPACK = "Не сортировать рюкзак";
CLASSICAPI_IGNORE_BACKPACK_TOOLTIP = "При сортировке сумок предметы в рюкзаке остаются на своих местах.";
CLASSICAPI_IGNORE_BANK = "Не сортировать основной банк";
CLASSICAPI_IGNORE_BANK_TOOLTIP = "При сортировке банка предметы в основных ячейках банка остаются на своих местах.";
CLASSICAPI_INTERACT_DIRECTION = "Направление взаимодействия";
CLASSICAPI_INTERACT_DIRECTION_TOOLTIP = "Где должен находиться NPC или объект, чтобы его выбрала клавиша взаимодействия. Направление определяется камерой, а не персонажем.";
CLASSICAPI_INTERACT_ICON = "Показывать значок взаимодействия";
CLASSICAPI_INTERACT_ICON_TOOLTIP = "Показывает значок над NPC или объектом, которые использует клавиша взаимодействия.";
CLASSICAPI_INTERACT_KEY = "Клавиша взаимодействия";
CLASSICAPI_INTERACT_RANGE = "Дистанция взаимодействия";
CLASSICAPI_INTERFACE = "Интерфейс";
CLASSICAPI_KEEP_ACTION_TARGETING = "Сохранять динамическое выделение";
CLASSICAPI_KEEP_ACTION_TARGETING_TOOLTIP = "Динамическое выделение продолжает выбирать противников и союзников, пока у вас есть цель, выбранная вручную.";
CLASSICAPI_LOOSE_TARGET_FOLLOWS = "За кем следует обычная цель";
CLASSICAPI_LOOSE_TARGET_FOLLOWS_ENEMY = "За мягкой целью противника";
CLASSICAPI_LOOSE_TARGET_FOLLOWS_FRIEND = "За мягкой целью союзника";
CLASSICAPI_LOOSE_TARGET_FOLLOWS_NONE = "Ни за кем";
CLASSICAPI_LOOSE_TARGET_FOLLOWS_TOOLTIP = "Цель, выбранная динамическим выделением, меняется вместе с мягкой целью противника или союзника. Цель, выбранная вручную, сохраняется.";
CLASSICAPI_LOW_PRIORITY_ICONS = "Показывать значки низкого приоритета";
CLASSICAPI_LOW_PRIORITY_ICONS_TOOLTIP = "Также показывает значок над целью, с которой нельзя ничего сделать, например над членом группы. Значок выглядит как обычный указатель.";
CLASSICAPI_MATCH_INSIDE_NAMES = "Учитывать правки внутри имени";
CLASSICAPI_MATCH_INSIDE_NAMES_TOOLTIP = "Когда вы вводите текст в середине имени, предлагаемые имена также учитывают буквы после курсора.";
CLASSICAPI_MATCH_THE_COMMAND = "Учитывать команду";
CLASSICAPI_MATCH_THE_COMMAND_TOOLTIP = "Предлагает только имена, подходящие для команды, например участников гильдии для /gpromote. Если выключено, предлагаются все известные вам имена.";
CLASSICAPI_MOTION = "Движение";
CLASSICAPI_NAME_SUGGESTIONS = "Подсказки имен";
CLASSICAPI_NAMEPLATES = "Таблички с именами";
CLASSICAPI_NUMBERS = "Числа";
CLASSICAPI_OPTIONS_ABOUT = "Настройки возможностей, которые ClassicAPI добавляет в игру. Каждый параметр меняет одну настройку, и изменение применяется сразу.";
CLASSICAPI_OTHER_NAMEPLATE_ALPHA = "Непрозрачность чужих табличек";
CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP = "Непрозрачность табличек с именами целей, которые не являются вашей целью.";
CLASSICAPI_PREFER_YOUR_TARGET = "Отдавать предпочтение вашей цели";
CLASSICAPI_PREFER_YOUR_TARGET_TOOLTIP = "Если выбранную вами цель можно взять, динамическое выделение выбирает ее. Если параметр выключен, всегда выбирается ближайшая цель.";
CLASSICAPI_RANGE_TOOLTIP = "Максимальное расстояние в ярдах. Оно не может превышать значение параметра targetNearestDistance.";
CLASSICAPI_RECENT_WHISPERS_FIRST = "Сначала недавние шепоты";
CLASSICAPI_RECENT_WHISPERS_FIRST_TOOLTIP = "Предлагаемые имена начинаются с игроков, которым вы шептали совсем недавно.";
CLASSICAPI_REMEMBER_PLAYERS = "Запоминать игроков";
CLASSICAPI_REMEMBER_PLAYERS_TOOLTIP = "Сохраняет имя, класс, расу и пол встречавшихся вам игроков, чтобы модификации могли показывать их в следующих сеансах. Данные общие для всех ваших персонажей на этом сервере.";
CLASSICAPI_SCRIPT_PROFILE = "Профилирование скриптов";
CLASSICAPI_SCRIPT_PROFILE_TOOLTIP = "Измеряет время процессора, которое используют модификации. Изменение вступает в силу после перезагрузки интерфейса.";
CLASSICAPI_SEPARATE_LARGE_NUMBERS = "Разделять большие числа";
CLASSICAPI_SEPARATE_LARGE_NUMBERS_TOOLTIP = "Ставит разделитель между группами из трех цифр, когда модификации форматируют числа с помощью BreakUpLargeNumbers.";
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT = "Сортировать сумки справа налево";
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT_TOOLTIP = "При сортировке предметы перемещаются в конец последней сумки, а хлам — в начало рюкзака.";
CLASSICAPI_STRICT_INTERACT_RANGE = "Строгая дистанция взаимодействия";
CLASSICAPI_STRICT_INTERACT_RANGE_TOOLTIP = "Никогда не выбирает NPC или объект за пределами дистанции взаимодействия. Если параметр выключен, объект, доступный для использования сейчас, выбирается даже на большем расстоянии.";
CLASSICAPI_TARGET_NAMEPLATE_ALPHA = "Непрозрачность таблички цели";
CLASSICAPI_TARGET_NAMEPLATE_ALPHA_TOOLTIP = "Непрозрачность таблички с именем вашей цели.";
CLASSICAPI_UI_SHAKE_STRENGTH = "Сила дрожания интерфейса";
CLASSICAPI_UI_SHAKE_STRENGTH_TOOLTIP = "Определяет, насколько сильно эффекты дрожания смещают рамки. 0 отключает дрожание.";
CLASSICAPI_UNIT_DIRECTION_TOOLTIP = "Где должна находиться цель, чтобы ее выбрало динамическое выделение. Направление определяется камерой, а не персонажем.";
CLASSICAPI_WHEN_YOU_HAVE_A_TARGET = "Когда у вас есть цель";
COMBAT_LABEL = "Бой";
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Отсчет времени восстановления";
CONFIRM_DELETE_EQUIPMENT_SET = "Вы действительно хотите удалить комплект экипировки \"%s\"?";
CONFIRM_SAVE_EQUIPMENT_SET = "Хотите сохранить комплект экипировки \"%s\"?";
COPPER_AMOUNT = "%d |4медная монета:медные монеты:медных монет;";
COPPER_AMOUNT_SYMBOL = "м";
COPPER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:2:3:0:1|t";
DEBUFF_SYMBOL_CURSE = "Пр";
DEBUFF_SYMBOL_DISEASE = "Бз";
DEBUFF_SYMBOL_MAGIC = "Мг";
@@ -24,74 +116,66 @@ ENABLE_INTERACT_TEXT = "Включить клавишу взаимодейств
EQUIPMENT_MANAGER = "Комплекты экипировки";
EQUIPMENT_MANAGER_IGNORE_SLOT = "Пропустить ячейку";
EQUIPMENT_MANAGER_IS_DISABLED = "Управление экипировкой отключено.";
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(предмет для ячейки \"%d\" отсутствует)|r";
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(не хватает: %d)|r";
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "Поместить в сумку";
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "Учитывать ячейку";
EQUIPMENT_SETS = "Комплекты экипировки: |cFFFFFFFF%s|r";
EQUIPMENT_SETS_CANT_RENAME = "Это название уже используется для другого комплекта.";
EQUIPMENT_SETS_TOO_MANY = "Вы не можете создавать новые комплекты экипировки.";
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Выбрать специализацию:";
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Назначить специализации:";
EQUIPMENT_SET_EDIT = "Изменить название/значок";
EQUIPMENT_SET_SETTINGS = "Параметры";
EQUIPSET_EQUIP = "Надеть";
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Не удалось заменить комплект обмундирования. Инвентарь переполнен.";
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Некоторые предметы из набора \"%s\" не будут экипированы, поскольку вы участвуете в бою.";
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Недостает одного или более предметов из набора \"%s\".";
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Не удалось сменить экипировку. Инвентарь переполнен.";
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Некоторые предметы из комплекта \"%s\" нельзя сменить, поскольку вы участвуете в бою.";
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Один или несколько предметов из комплекта \"%s\" отсутствуют и не были надеты.";
EVENTTRACE_APPLY_DEFAULT_FILTER = "Применить фильтр по умолчанию";
EVENTTRACE_ARG_FMT = "Арг. %d:";
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Снять выделение";
EVENTTRACE_ARG_FMT = "Аргумент %d:";
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Снять все";
EVENTTRACE_BUTTON_DISCARD_FILTER = "Сбросить все";
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Выделить все";
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Выбрать все";
EVENTTRACE_BUTTON_MARKER = "Маркер";
EVENTTRACE_BUTTON_PAUSE = "Пауза";
EVENTTRACE_BUTTON_PLAY = "Играть";
EVENTTRACE_BUTTON_PLAY = "Продолжить";
EVENTTRACE_FILTER_HEADER = "Фильтр";
EVENTTRACE_HEADER = "Журнал события";
EVENTTRACE_LOG_CR_EVENTS = "Отображать глобальные события по регистрации";
EVENTTRACE_HEADER = "Журнал событий";
EVENTTRACE_LOG_CR_EVENTS = "Показывать события глобального реестра";
EVENTTRACE_LOG_DISCARD = "Журнал сброшен";
EVENTTRACE_LOG_HEADER = "Журнал";
EVENTTRACE_LOG_PAUSE = "Регистрация участников события приостановлена";
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Фиксация событий прекращается в свернутом состоянии";
EVENTTRACE_LOG_START = "Регистрация участников события началась";
EVENTTRACE_LOG_WHEN_HIDDEN = "Фиксировать события в свернутом состоянии";
EVENTTRACE_LOG_PAUSE = "Запись событий приостановлена";
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Запись событий приостановлена, пока окно скрыто";
EVENTTRACE_LOG_START = "Запись событий начата";
EVENTTRACE_LOG_WHEN_HIDDEN = "Записывать события, когда окно скрыто";
EVENTTRACE_MARKER = "Маркер";
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
EVENTTRACE_OPTIONS = "Параметры";
EVENTTRACE_RESULTS = "Результаты: %d";
EVENTTRACE_SHOW_ARGUMENTS = "Отображать аргументы события";
EVENTTRACE_SHOW_TIMESTAMP = "Отображать метку времени события";
EVENTTRACE_RESULTS = "Результатов: %d";
EVENTTRACE_SHOW_ARGUMENTS = "Показывать аргументы событий";
EVENTTRACE_SHOW_TIMESTAMP = "Показывать время событий";
EVENTTRACE_TIMESTAMP = "Время:";
CLEAR_FOCUS = "Отменить запоминание цели";
INTERACT_ICONS_DEFAULT = "Только NPC (по умолчанию)";
INTERACT_ICONS_OPTION = "Значки клавиш взаимодействия";
INTERACT_ICONS_SHOW_ALL = "Показать все";
INTERACT_ICONS_OPTION = "Значки клавиши взаимодействия";
INTERACT_ICONS_SHOW_ALL = "Показывать все";
INTERACT_ICONS_SHOW_NONE = "Не показывать";
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Способности будут задействоваться, когда пользователь нажимает, а не отпускает клавишу.";
OPTION_TOOLTIP_ACTION_TARGETING = "Система захвата целей, динамически выбирающая цели в поле зрения. Работает параллельно со стандартной системой наведения.";
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Отображение обратного отсчета для времени восстановления на панели команд.";
OPTION_TOOLTIP_ENABLE_INTERACT = "Позволяет взаимодействовать с NPC и объектами по нажатию клавиши.";
OPTION_TOOLTIP_INTERACT_ICONS = "Определяет, какие значки появляются над NPC и объектами, если включен параметр клавиши взаимодействия.";
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Прекращение автоматической атаки при смене цели.";
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Способности на панели команд срабатывают при нажатии клавиши, а не при ее отпускании.";
OPTION_TOOLTIP_ACTION_TARGETING = "Система выделения, которая динамически выбирает противников в зависимости от направления взгляда. Работает вместе со стандартной системой выделения.";
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Показывать числовой отсчет времени восстановления на панели команд.";
OPTION_TOOLTIP_ENABLE_INTERACT = "Позволяет взаимодействовать с NPC и объектами нажатием клавиши.";
OPTION_TOOLTIP_INTERACT_ICONS = "Определяет, какие значки появляются над NPC и объектами, когда включена клавиша взаимодействия.";
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Прекращать автоатаку при смене цели.";
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
KEY_BINDING_TOOLTIP = "<ПКМ, чтобы отвязать>";
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Назначьте клавишу \"%s\"или нажмите %s для отмены";
KEY_BINDING_TOOLTIP = "<ПКМ, чтобы снять привязку>";
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Назначьте клавишу для \"%s\" или нажмите %s для отмены";
SET_FOCUS = "Запомнить цель";
FOCUS_TOKEN_NOT_FOUND = "<фокус отсутствует>";
FOCUSTARGET = "Запомнить цель";
ITEMS_VARIABLE_QUANTITY = "%d |4предмет:предмета:предметов;";
ITEMS_EQUIPPED = "%d |4предмет экипирован:предмета экипировано:предметов экипировано;";
ITEMS_IN_INVENTORY = "%d |4предмет:предмета:предметов; в сумках";
ITEM_SLOTS_IGNORED = "%d |4ячейка пропущена:ячейки пропущены:ячейки пропущены;";
ITEM_MISSING = "Предмет для ячейки %s отсутствует";
ITEMS_NOT_IN_INVENTORY = "%d |4предмет отсутствует:предмета отсутствует:предметов отсутствуют;";
STOP_AUTO_ATTACK = "Прекращение автоатаки";
TARGETFOCUS = "Восстановить цель";
-- BINDING_NAME_AOELOOT = "AoE Loot";
COPPER_AMOUNT = "%d |4медная монета:медные монеты:медных монет;";
COPPER_AMOUNT_SYMBOL = "м";
GEARSETS_POPUP_TEXT = "Введите название комплекта (не более 16 символов):";
GENERIC_FRACTION_STRING = "%d/%d";
GOLD_AMOUNT = "%d |4золотая:золотые:золотых;";
GOLD_AMOUNT_SYMBOL = "з";
GOLD_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:0:1:0:1|t";
GOLD_AMOUNT_TEXTURE_STRING = "%s|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:0:1:0:1|t";
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
ICON_FILTER_ALL = "Все значки";
ICON_FILTER_ITEM = "Предметы";
@@ -109,17 +193,17 @@ ITEM_MOD_ATTACK_POWER_SHORT = "к силе атаки";
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "к силе атаки дальнего боя";
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "к показателю критического удара в ближнем бою";
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "к показателю критического удара в дальнем бою";
ITEM_MOD_HIT_MELEE_RATING_SHORT = "к меткости в ближнем бою";
ITEM_MOD_HIT_MELEE_RATING_SHORT = "к меткости в ближнем бою";
ITEM_MOD_HIT_RANGED_RATING_SHORT = "к меткости в дальнем бою";
ITEM_MOD_HIT_SPELL_RATING_SHORT = "к меткости заклинаний";
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "к критическому эффекту заклинаний";
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "к дополнительному урону";
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "к дополнительному исцелению";
ITEM_MOD_MANA_REGENERATION_SHORT = "к восполнению маны";
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Защита";
ITEM_MOD_DODGE_RATING_SHORT = "Уклонение";
ITEM_MOD_PARRY_RATING_SHORT = "Парирование";
ITEM_MOD_BLOCK_RATING_SHORT = "Блок";
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "к защите";
ITEM_MOD_DODGE_RATING_SHORT = "к уклонению";
ITEM_MOD_PARRY_RATING_SHORT = "к парированию";
ITEM_MOD_BLOCK_RATING_SHORT = "к блокированию";
ITEM_MOD_BLOCK_VALUE_SHORT = "к показателю блокирования";
ITEM_MOD_HOLY_SPELL_DAMAGE_DONE_SHORT = "к урону от магии света";
ITEM_MOD_FIRE_SPELL_DAMAGE_DONE_SHORT = "к урону от магии огня";
@@ -131,6 +215,12 @@ ITEM_MOD_SPELL_PENETRATION_SHORT = "к проникающей способнос
ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "к пробиванию брони";
ITEM_MOD_HEALTH_REGEN_SHORT = "к восполнению здоровья";
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "к урону в секунду";
ITEMS_EQUIPPED = "%d |4предмет экипирован:предмета экипировано:предметов экипировано;";
ITEMS_IN_INVENTORY = "%d |4предмет:предмета:предметов; в инвентаре";
ITEMS_NOT_IN_INVENTORY = "%d |4предмет отсутствует:предмета отсутствует:предметов отсутствует;";
ITEMS_VARIABLE_QUANTITY = "%d |4предмет:предмета:предметов;";
ITEM_MISSING = "%s отсутствует";
ITEM_SLOTS_IGNORED = "%d |4ячейка пропущена:ячейки пропущены:ячеек пропущено;";
LARGE_NUMBER_SEPERATOR = "";
DECIMAL_SEPERATOR = ",";
FIRST_NUMBER = "тыс.";
@@ -141,14 +231,18 @@ SECOND_NUMBER_CAP = "М";
SECOND_NUMBER_CAP_NO_SPACE = "М";
THIRD_NUMBER = "млрд";
FOURTH_NUMBER = "трлн";
LUA_ERROR = "Ошибка Lua";
MINUTES_SECONDS = "%.2d:%.2d";
PAPERDOLL_EQUIPMENTMANAGER = "Комплекты экипировки";
PAPERDOLL_NEWEQUIPMENTSET = "Новый комплект";
PERCENTAGE_STRING = "%d%%";
SET_FRIENDNOTE_LABEL = "Заметка о |3-5(%s):";
PLAYER_LIST_DELIMITER = ", ";
RELOADUI = "Перезагрузить интерфейс";
SET_FRIENDNOTE_LABEL = "Задать заметку для %s:";
SET_NOTE = "Добавить заметку";
SILVER_AMOUNT = "%d |4серебряная:серебряные:серебряных;";
SILVER_AMOUNT_SYMBOL = "с";
SILVER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:1:2:0:1|t";
SLASH_CANCELAURA1 = "/снятьауру";
SLASH_CANCELAURA2 = "/cancelaura";
SLASH_CANCELFORM1 = "/нормоблик";
@@ -222,10 +316,14 @@ SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
SLASH_USE1 = "/use";
SLASH_USE2 = "/использовать";
SOCIAL_LABEL = "Общение";
LINK_TRADESKILL_TOOLTIP = "Щелкните здесь, чтобы создать ссылку на вашу профессию.";
TEXT_TO_SPEECH = "Текст в речь";
TEXT_TO_SPEECH_ADJUST_RATE = "Изменить скорость речи.";
STOP_AUTO_ATTACK = "Прекращение автоатаки";
SYMBOLIC_NEGATIVE_NUMBER = "-%s";
SYMBOLIC_POSITIVE_NUMBER = "+%s";
TARGETFOCUS = "Восстановить цель";
LINK_TRADESKILL_TOOLTIP = "Щелкните здесь, чтобы создать ссылку на свою профессию.";
TEXT_TO_SPEECH = "Синтез речи";
TEXT_TO_SPEECH_ADJUST_RATE = "Изменить скорость речи";
TEXT_TO_SPEECH_ADJUST_VOLUME = "Громкость речи";
TEXT_TO_SPEECH_VOICE_OPTION = "Параметры голоса";
TOTAL_MEM_KB_ABBR = "Память: %.0f КБ";
TOTAL_MEM_MB_ABBR = "Модификации занимают в памяти: %.2f МБ";
TEXT_TO_SPEECH_VOICE_OPTION = "Выбор голоса";
TOTAL_MEM_KB_ABBR = "Память модификаций: %.0f КБ";
TOTAL_MEM_MB_ABBR = "Память модификаций: %.2f МБ";
@@ -15,11 +15,11 @@ CLASSICAPI_ACTION_TARGETING = "动作瞄准";
CLASSICAPI_ADVANCED = "高级";
CLASSICAPI_ALL_SOFT_TARGET_ICONS = "所有软目标图标";
CLASSICAPI_AUTO_DISMOUNT = "自动下马";
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "骑乘状态下施放法术时自动下马。";
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "骑乘状态下施放法术、使用物体、拾取、攻击或打开飞行地图时自动下马。";
CLASSICAPI_AUTO_STAND = "自动站起";
CLASSICAPI_AUTO_STAND_TOOLTIP = "坐下状态下施放法术时自动站起。";
CLASSICAPI_AUTO_STAND_TOOLTIP = "坐下状态下施放法术、拾取或打开飞行地图时自动站起。";
CLASSICAPI_AUTO_UNSHIFT = "自动取消形态";
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "施放当前形态无法施放的法术时自动取消该形态。";
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "施放当前形态无法施放的法术、使用物品或打开飞行地图时自动取消该形态。同时允许在德鲁伊形态和幽魂之狼形态下与NPC对话和使用物体。";
CLASSICAPI_BAGS_AND_ITEMS = "背包与物品";
CLASSICAPI_CASTING_AND_ATTACKING = "施法与攻击";
CLASSICAPI_COMBAT_LOG_RETENTION = "战斗记录保留时间";
+1 -2
View File
@@ -217,8 +217,7 @@ void Register() {
{"Standard", static_cast<int>(Variant::Standard)},
{"UrlSafe", static_cast<int>(Variant::UrlSafe)},
};
Game::Lua::RegisterIntegerEnum("Enum", "Base64Variant",
kBase64Variant, 2);
Game::Lua::RegisterIntegerEnum("Enum", "Base64Variant", kBase64Variant);
}
} // namespace
+1 -2
View File
@@ -305,8 +305,7 @@ void Register() {
{"Zlib", static_cast<int>(Method::Zlib)},
{"Gzip", static_cast<int>(Method::Gzip)},
};
Game::Lua::RegisterIntegerEnum("Enum", "CompressionMethod",
kCompressionMethod, 3);
Game::Lua::RegisterIntegerEnum("Enum", "CompressionMethod", kCompressionMethod);
}
} // namespace
+1 -3
View File
@@ -224,9 +224,7 @@ static void RegisterLuaFunctions() {
// 4.3.4's switch for the item-tooltip line; dedups by name on /reload.
g_hideSetsCVar = CVar::Factory::Register("dontShowEquipmentSetsOnItems", "Don't show which equipment sets an item is associated with", Offsets::CVAR_CATEGORY_GAME, "0");
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY),
g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY), g_methods);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+81 -19
View File
@@ -55,6 +55,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 +67,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;
@@ -147,23 +151,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 +193,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 +255,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;
@@ -320,6 +372,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 +392,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 +481,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 +500,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
View File
@@ -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
+5 -4
View File
@@ -144,10 +144,11 @@ const Game::Doc::Method kFontDocs[] = {
};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY),
kFontStringMethods, 2, kFontStringDocs, 2);
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_FONT_METHOD_REGISTRY), kFontMethods,
2, kFontDocs, 2);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY), kFontStringMethods,
kFontStringDocs);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FONT_METHOD_REGISTRY), kFontMethods, kFontDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -2
View File
@@ -181,8 +181,8 @@ const Game::Doc::Method kHtmlDocs[] = {
{"GetIndentedWordWrap", &kHtmlGet},
};
template <typename P> void Register(uintptr_t registry, const Game::Doc::Method *docs) {
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(registry), P::methods, 2, docs, 2);
template <typename P> void Register(uintptr_t registry, Game::Doc::MethodList docs) {
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(registry), P::methods, docs);
}
// A font string takes the bit from its font only when its settable mask
+1 -3
View File
@@ -378,9 +378,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -2
View File
@@ -96,8 +96,8 @@ const Game::Doc::Method kDocs[] = {
};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY), kMethods, 2,
kDocs, 2);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY), kMethods, kDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -324,9 +324,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -1165,9 +1165,7 @@ const Game::Lua::FrameMethodEntry g_frameMethods[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY),
g_frameMethods,
static_cast<int>(sizeof(g_frameMethods) / sizeof(g_frameMethods[0])));
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_frameMethods);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -117,9 +117,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -186,9 +186,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -2
View File
@@ -102,8 +102,7 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), g_methods);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -453,9 +453,7 @@ void RegisterLuaFunctions() {
Common::ReplaceWithCall(Offsets::CALL_FRAME_DRAW_SELF_CHILD, kDrawChildCall, sizeof kDrawChildCall,
reinterpret_cast<const void *>(&DrawChildThunk));
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -1588,9 +1588,7 @@ void RegisterLuaFunctions() {
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, static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])),
g_methodDocs, static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
Game::Lua::RegisterFrameMethods(g_hash, g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -4
View File
@@ -53,9 +53,6 @@ using SetAttr_t = void(__thiscall *)(void *node, const char *name, const char *v
using Build_t = void *(__fastcall *)(const void *node, void *parent, void *status);
using StatusReset_t = void(__thiscall *)(void *self);
// LUA_TNONE: lua_type of an index past the top.
constexpr int kTypeNone = -1;
// 4.3.4's usage line; vanilla's (0x00872F60) has no `id`.
constexpr const char *kUsage =
"Usage: CreateFrame(\"frameType\" [, \"name\"] [, parent] [, \"template\"] [, id])";
@@ -102,7 +99,7 @@ int __fastcall CreateFrame_h(void *L) {
const int parentType = Type(L, 3);
if (!IsString(L, 1) ||
(parentType != kTypeNone && parentType != TYPE_NIL && parentType != TYPE_TABLE)) {
(parentType != TYPE_NONE && parentType != TYPE_NIL && parentType != TYPE_TABLE)) {
Error(L, kUsage);
return 0;
}
+1 -3
View File
@@ -111,9 +111,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -3
View File
@@ -221,9 +221,8 @@ const Game::Doc::Method g_methodDocs[] = {
};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -67,9 +67,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -113,9 +113,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+5 -4
View File
@@ -108,10 +108,11 @@ const Game::Doc::Method kFontStringDocs[] = {{"SetFormattedText", &kFontStringDo
const Game::Doc::Method kButtonDocs[] = {{"SetFormattedText", &kButtonDoc}};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY),
kFontStringMethods, 1, kFontStringDocs, 1);
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kButtonMethods,
1, kButtonDocs, 1);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY), kFontStringMethods,
kFontStringDocs);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kButtonMethods, kButtonDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -2
View File
@@ -198,8 +198,7 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_methods);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -2
View File
@@ -95,8 +95,7 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
void RegisterLuaFunctions() {
for (const uintptr_t registry : {static_cast<uintptr_t>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
static_cast<uintptr_t>(Offsets::VAR_SIMPLEHTML_METHOD_REGISTRY)}) {
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(registry), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(registry), g_methods);
}
}
+1 -3
View File
@@ -139,9 +139,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+5 -16
View File
@@ -569,26 +569,15 @@ const Game::Lua::FrameMethodEntry g_fontStringMethods[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY),
g_regionMethods,
static_cast<int>(sizeof(g_regionMethods) / sizeof(g_regionMethods[0])));
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_regionMethods);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY),
g_frameMethods,
static_cast<int>(sizeof(g_frameMethods) / sizeof(g_frameMethods[0])));
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_frameMethods);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY),
g_buttonMethods,
static_cast<int>(sizeof(g_buttonMethods) / sizeof(g_buttonMethods[0])));
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), g_buttonMethods);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_TEXTURE_METHOD_REGISTRY),
g_textureMethods,
static_cast<int>(sizeof(g_textureMethods) / sizeof(g_textureMethods[0])));
reinterpret_cast<void *>(Offsets::VAR_TEXTURE_METHOD_REGISTRY), g_textureMethods);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY),
g_fontStringMethods,
static_cast<int>(sizeof(g_fontStringMethods) /
sizeof(g_fontStringMethods[0])));
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY), g_fontStringMethods);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -3
View File
@@ -380,9 +380,8 @@ void RegisterLuaFunctions() {
reinterpret_cast<const void *>(&RemovalThunk));
Common::ReplaceWithCall(Offsets::ADDR_UI_FOCUS_PASS_PROBE, kProbe, sizeof kProbe,
reinterpret_cast<const void *>(&ProbeThunk));
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
Game::Lua::RegisterGlobalFunction("GetMouseFoci", &Script_GetMouseFoci, &kGetMouseFoci);
}
+2 -2
View File
@@ -148,8 +148,8 @@ const Game::Doc::Method kDocs[] = {
};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kMethods, 2,
kDocs, 2);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kMethods, kDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -3
View File
@@ -389,9 +389,8 @@ void RegisterLuaFunctions() {
reinterpret_cast<const void *>(&PressDispatchThunk));
Common::ReplaceWithCall(Offsets::ADDR_UI_RELEASE_DISPATCH, kReleaseDispatch, sizeof kReleaseDispatch,
reinterpret_cast<const void *>(&ReleaseDispatchThunk));
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -2
View File
@@ -104,8 +104,8 @@ const Game::Doc::Function kDoc{"Sets which mouse buttons run the OnMouseDown and
const Game::Doc::Method kDocs[] = {{"RegisterForMouse", &kDoc}};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kMethods, 1,
kDocs, 1);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kMethods, kDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -209,9 +209,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -5
View File
@@ -108,12 +108,9 @@ const Game::Doc::Method g_frameDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_regionMethods,
static_cast<int>(sizeof(g_regionMethods) / sizeof(g_regionMethods[0])), g_regionDocs,
static_cast<int>(sizeof(g_regionDocs) / sizeof(g_regionDocs[0])));
g_regionDocs);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_frameMethods,
static_cast<int>(sizeof(g_frameMethods) / sizeof(g_frameMethods[0])), g_frameDocs,
static_cast<int>(sizeof(g_frameDocs) / sizeof(g_frameDocs[0])));
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_frameMethods, g_frameDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -2
View File
@@ -49,8 +49,8 @@ const Game::Doc::Function kDoc{"Enables or disables the button, as Enable() or D
const Game::Doc::Method kDocs[] = {{"SetEnabled", &kDoc}};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kMethods, 1,
kDocs, 1);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kMethods, kDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -203,9 +203,7 @@ void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("AddSourceLocationExclude", &Script_AddSourceLocationExclude,
&kAddSourceLocationExclude);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -2
View File
@@ -351,8 +351,7 @@ const Game::Lua::FrameMethodEntry g_frameMethods[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_frameMethods,
static_cast<int>(sizeof(g_frameMethods) / sizeof(g_frameMethods[0])));
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_frameMethods);
}
// Frames survive /reload but the engine rebuilds the event table with empty
+1 -3
View File
@@ -114,9 +114,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -126,9 +126,7 @@ static const Game::Lua::FrameMethodEntry g_methods[] = {
static void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY),
g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY), g_methods);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+1 -3
View File
@@ -366,9 +366,7 @@ static const Game::Lua::FrameMethodEntry g_methods[] = {
static void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY),
g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY), g_methods);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+3 -9
View File
@@ -309,15 +309,9 @@ void RegisterLuaFunctions() {
&Script_GetItemInventorySlotKey);
Game::Lua::RegisterTableFunction("C_Item", "GetItemInventorySlotInfo",
&Script_GetItemInventorySlotInfo);
Game::Lua::RegisterIntegerEnum(
"Enum", "InventoryType", kInventoryTypeEntries,
sizeof(kInventoryTypeEntries) / sizeof(kInventoryTypeEntries[0]));
Game::Lua::RegisterIntegerEnum(
"Enum", "ItemClass", kItemClassEntries,
sizeof(kItemClassEntries) / sizeof(kItemClassEntries[0]));
Game::Lua::RegisterIntegerEnum(
"Enum", "ItemQuality", kItemQualityEntries,
sizeof(kItemQualityEntries) / sizeof(kItemQualityEntries[0]));
Game::Lua::RegisterIntegerEnum("Enum", "InventoryType", kInventoryTypeEntries);
Game::Lua::RegisterIntegerEnum("Enum", "ItemClass", kItemClassEntries);
Game::Lua::RegisterIntegerEnum("Enum", "ItemQuality", kItemQualityEntries);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+33
View File
@@ -0,0 +1,33 @@
// 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 "localization/Locale.h"
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
#include <cstring>
namespace Localization::Locale {
// Script_GetLocale (FUN_0048d8b0): VAR_LOCALE_NAME_TABLE[VAR_LOCALE_INDEX].
const char *Client() {
const uint32_t index = Game::Read<uint32_t>(Offsets::VAR_LOCALE_INDEX);
return reinterpret_cast<const char *const *>(
static_cast<uintptr_t>(Offsets::VAR_LOCALE_NAME_TABLE))[index];
}
bool Is(const char *code) { return std::strcmp(Client(), code) == 0; }
} // namespace Localization::Locale
+32
View File
@@ -0,0 +1,32 @@
// 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 locale by NAME, as GetLocale() reports it.
//
// Language rules (plurals, accent folding, number abbreviations) must pick
// by this name, never by VAR_LOCALE_INDEX. The 1.12 index is the Wow.ini
// tag's position in a table of eight codes (enUS koKR frFR deDE zhCN zhTW
// esES xxYY), not 4.3.4's nine-slot order, so a 4.3.4 index such as 8 for
// ruRU never occurs here. A Russian client is one whose table holds "ruRU",
// in whatever slot. The index stays right for picking a DBC string column.
namespace Localization::Locale {
// The locale code GetLocale() returns ("enUS", "frFR", ...).
const char *Client();
// Whether the client locale is `code`.
bool Is(const char *code);
} // namespace Localization::Locale
+3 -6
View File
@@ -62,6 +62,7 @@
#include "Game.h"
#include "Offsets.h"
#include "cvar/Factory.h"
#include "localization/Locale.h"
#include <cmath>
#include <cstdio>
@@ -131,17 +132,13 @@ constexpr Scale kScalesAsian[] = {
CVar::Factory::Handle g_breakUpLargeNumbers = nullptr;
// VAR_LOCALE_INDEX order: enUS, koKR, frFR, deDE, zhCN, zhTW, esES, esMX, ruRU.
bool ClientIsAsian() {
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
return locale == 1 || locale == 4 || locale == 5;
}
bool IsAsianLocale(const char *locale) {
return std::strcmp(locale, "koKR") == 0 || std::strcmp(locale, "zhCN") == 0 ||
std::strcmp(locale, "zhTW") == 0;
}
bool ClientIsAsian() { return IsAsianLocale(Localization::Locale::Client()); }
// `_G[name]` when it is a string, else "".
std::string GlobalString(void *L, const char *name) {
Lua::PushString(L, name);
+5 -98
View File
@@ -15,6 +15,7 @@
#include "../Offsets.h"
#include "../guid/Guid.h"
#include "../object/Resolve.h"
#include "Request.h"
#include <cstdint>
@@ -22,22 +23,6 @@ namespace Loot::Nearby {
namespace {
// Engine interact-range constants, read out of `.rdata` once and
// pinned here as `constexpr`. Both are referenced by `FUN_005EC110`
// (the master interact-state checker) and propagated through
// `FUN_005DF130` / `FUN_005DF2A0` (CMSG_LOOT senders). Yards units;
// values verified at runtime addresses `DAT_0080B058` and
// `DAT_0080A1E8`.
//
// INTERACT_REACH (1.333…) is added to `target_radius + player_radius`
// for the unclamped range; modeled as the "reach beyond your touch"
// constant.
// MIN_INTERACT_RANGE (5.0) clamps the result so even tiny
// targets with near-zero bounding radius still get the standard
// 5-yard right-click reach.
constexpr float INTERACT_REACH = 1.3333333f;
constexpr float MIN_INTERACT_RANGE = 5.0f;
// `ClntObjMgrEnumVisibleObjects(callback, context)` — walks the engine's
// visible-object table. The callback shape's `void *unusedEdx` slot
// absorbs the second `__fastcall` register so MSVC lays out the trailing
@@ -49,84 +34,6 @@ using ClntObjMgrEnumVisibleObjectsCallback_t = int(__fastcall *)(void *ctx,
using ClntObjMgrEnumVisibleObjects_t =
int(__fastcall *)(ClntObjMgrEnumVisibleObjectsCallback_t cb, void *ctx);
// `__thiscall(self, outBuf) → const C3Vector *` at vtable slot 5
// (byte offset `0x14`). Both CGUnit_C and CGPlayer_C populate the
// caller-provided 12-byte buffer with the unit's world position and
// return it. Same call shape `FUN_005EC110` uses to read the
// player / target positions when deciding interact range.
struct C3Vector {
float x, y, z;
};
using GetPosition_t = const C3Vector *(__thiscall *)(const void *self,
C3Vector *outBuf);
constexpr int VTBL_GET_POSITION_OFFSET = 0x14;
// Reads `(unit.m_objectFields[+0x224] & UNIT_DYNFLAG_LOOTABLE) != 0`
// — the server-driven bit it sets when the local player has loot
// rights on this unit. Works uniformly for live and dead CGUnit
// subclasses; the bit is simply 0 for non-lootable targets.
//
// Don't call `FUN_CGCORPSE_IS_LOOTABLE` here even though the engine
// itself uses it in `FUN_005D6BF0`: that helper reads `+0x78` of
// m_objectFields, which is the CORPSE dynamic-flags slot for
// CGCorpse_C but the BYTES_0 (race / class / gender / power type)
// slot for CGUnit_C. Routing live units through it returns the low
// bit of *race* — yields false positives for any Human / Dwarf /
// Undead / Gnome player and false negatives for dead mobs.
bool IsLootable(const void *unit) {
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
const uint32_t flags =
Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS);
return (flags & Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
}
// Reads the bounding-radius float from a unit's m_objectFields. The
// engine uses this as the contribution from both sides (player + target)
// in its interact-range computation.
float BoundingRadius(const void *unit) {
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
return Game::Read<float>(fields, Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
}
// Invokes the unit's virtual `GetPosition` (vtable slot at offset
// `0x14`). The engine's signature is `__thiscall(this, outBuf) →
// outBuf`; we pass our own 12-byte stack buffer and read x/y/z out
// of the returned pointer.
const C3Vector *GetPosition(const void *unit, C3Vector *outBuf) {
auto vtable = *reinterpret_cast<void *const *const *>(unit);
auto fn = reinterpret_cast<GetPosition_t>(
*reinterpret_cast<const void *const *>(
reinterpret_cast<const uint8_t *>(vtable) +
VTBL_GET_POSITION_OFFSET));
return fn(unit, outBuf);
}
// Mirrors the range portion of `FUN_005EC110`. We skip the engine's
// player-state checks (mounted / casting / prior-interaction GUID
// gate) because they fire intermittently during normal play and would
// make the enumerator return an empty list whenever the player has
// already clicked something — surprising for a "what's lootable
// nearby" reader. The pure distance test is what controls cursor
// highlight, which is the contract the user asked for.
bool InInteractRange(const void *player, const void *target) {
C3Vector pBuf{};
C3Vector tBuf{};
const C3Vector *pPos = GetPosition(player, &pBuf);
const C3Vector *tPos = GetPosition(target, &tBuf);
if (pPos == nullptr || tPos == nullptr)
return false;
const float dx = pPos->x - tPos->x;
const float dy = pPos->y - tPos->y;
const float dz = pPos->z - tPos->z;
const float distSq = dx * dx + dy * dy + dz * dz;
float range = BoundingRadius(player) + BoundingRadius(target) + INTERACT_REACH;
if (range < MIN_INTERACT_RANGE)
range = MIN_INTERACT_RANGE;
return distSq <= range * range;
}
// Only `TYPEMASK_UNIT` carries lootable-mob state in 1.12 — dead
// creatures stay as CGCreature_C objects with the lootable bit flipped
// on, rather than transitioning to a separate corpse object. Dead
@@ -135,7 +42,7 @@ bool InInteractRange(const void *player, const void *target) {
struct ScanCtx {
void *L;
const void *player; // resolved once outside the iterator
void *player; // resolved once outside the iterator
int idx; // 1-based slot of the next entry to append
};
@@ -144,10 +51,10 @@ int __fastcall ScanCallback(ScanCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
if (obj == nullptr)
return 1; // GUID isn't a unit; continue
if (!IsLootable(obj))
if (!Request::IsLootable(obj))
return 1; // unit isn't lootable to us; continue
if (!InInteractRange(ctx->player, obj))
if (!Request::InReach(ctx->player, obj))
return 1; // out of click-loot range; continue
char guidBuf[Guid::STRING_SIZE];
@@ -180,7 +87,7 @@ int __fastcall Script_GetNearbyLootableUnits(void *L) {
// Resolve the local player once — the canonical CGPlayer_C the
// engine's own interact-range checks use, distinct from the
// `VAR_LOCAL_PLAYER_PTR` global. Bail if unavailable.
const void *player = Game::ResolveUnitToken("player");
void *player = Game::ResolveUnitToken("player");
if (player == nullptr)
return 1;
+35 -2
View File
@@ -15,6 +15,7 @@
#include "../Game.h"
#include "../Offsets.h"
#include "../unit/Position.h"
namespace Loot::Request {
@@ -31,8 +32,11 @@ using CloseLootInner_t = void(__fastcall *)(int sendRelease,
char earlyReturnOnGameObject,
char showError);
uint64_t HeldTarget(const void *player) {
return Game::Read<uint64_t>(player, Offsets::OFF_PLAYER_LOOT_TARGET_GUID);
// The per-unit reach `CanLootNow` adds for each side (`[m_objectFields +
// 0x1F0]`).
float Reach(const void *unit) {
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
return Game::Read<float>(fields, Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
}
} // namespace
@@ -42,6 +46,35 @@ bool IsMounted(const void *player) {
return Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_MOUNTDISPLAYID) != 0;
}
bool IsLootable(const void *unit) {
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
return (Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS) &
Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
}
bool InReach(void *player, void *target) {
float p[3];
float t[3];
if (!Unit::Position::Read(player, p) || !Unit::Position::Read(target, t))
return false;
// `CanLootNow`'s reach for a unit (`0x005EC142`..`0x005EC17E`), with the
// engine's own constants read live.
float reach = Reach(target) + Reach(player) +
Game::Read<float>(static_cast<uintptr_t>(Offsets::VAR_MELEE_REACH_LEEWAY));
const float minReach =
Game::Read<float>(static_cast<uintptr_t>(Offsets::VAR_MELEE_REACH_MIN));
if (reach < minReach)
reach = minReach;
const float dx = p[0] - t[0];
const float dy = p[1] - t[1];
const float dz = p[2] - t[2];
return dx * dx + dy * dy + dz * dz <= reach * reach;
}
uint64_t HeldTarget(const void *player) {
return Game::Read<uint64_t>(player, Offsets::OFF_PLAYER_LOOT_TARGET_GUID);
}
bool Send(void *player, void *target, uint64_t guid) {
if (IsMounted(player))
return false;
+17 -2
View File
@@ -28,11 +28,26 @@ namespace Loot::Request {
// (`CGUnit_C::Interact`, `0x0060BEA0`) applies before its loot leg.
bool IsMounted(const void *player);
// Whether the server grants us loot on `unit` (`UNIT_DYNFLAG_LOOTABLE`).
bool IsLootable(const void *unit);
// Whether `target` is within the reach `CanLootNow` (`0x005EC110`) demands
// when the player is not walked up: both units' `+0x1F0` reach plus the
// engine's live leeway, at least its live minimum, against the 3D distance.
// `Send` never walks, so a target outside this reach is one the server
// refuses with "too far away".
bool InReach(void *player, void *target);
// The player's held loot target. The engine writes it when the request goes
// out and clears it when the server answers with a failure or a release, so
// a caller waiting on `guid` sees the refusal as this no longer being `guid`.
uint64_t HeldTarget(const void *player);
// Sends the loot request the way a right-click would: refused while mounted
// (the click's gate, which `FUN_CMSG_LOOT_UNIT` itself lacks), never
// auto-walks. Returns whether the request went out — false when mounted or
// when the engine's own pre-checks refused (another loot held, falling, not
// in control, ...), in which case no response is coming.
// when the engine's own pre-checks refused (another loot held, moving,
// falling, not in control, ...), in which case no response is coming.
bool Send(void *player, void *target, uint64_t guid);
// For a caller that gave up waiting: if the player still holds `guid` as
+121 -75
View File
@@ -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"
@@ -42,8 +51,10 @@
#include "../guid/Guid.h"
#include "../object/Resolve.h"
#include "../tick/WorldTick.h"
#include "../time/Clock.h"
#include "Request.h"
#include <algorithm>
#include <cstdint>
#include <string>
#include <vector>
@@ -55,12 +66,11 @@ namespace {
constexpr const char *kEventCompleted = "LOOT_SCAN_COMPLETED";
const Event::Custom::AutoReserve _reserve{kEventCompleted};
// Per-step timeout for the controller hook to fire on a queued
// CMSG_LOOT. WorldTick fires at the client frame rate (~30-60 Hz),
// so 180 ticks ≈ 3-6s wall clock. Long enough for high-latency
// servers, short enough that a single dropped response doesn't hang
// the whole scan.
constexpr int kStepTimeoutTicks = 180;
// How long one step may wait: for the server to answer a CMSG_LOOT it
// never answers, or for the engine to accept a send it keeps refusing
// (the player is moving). Long enough for high-latency servers, short
// enough that a dropped response doesn't hang the walk.
constexpr uint32_t kStepTimeoutMs = 3000;
// Event IDs the engine fires through `FUN_FIRE_EVENT_NO_ARGS` for
// loot-window transitions. The hook swallows these only when the
@@ -68,9 +78,12 @@ constexpr int kStepTimeoutTicks = 180;
// 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,
WaitingToSend, // the engine refused the send for now; retried each tick
WaitingForResponse, // CMSG_LOOT sent; awaiting our controller hook
};
@@ -90,7 +103,7 @@ struct ScanCtx {
State state = State::Idle;
void *player = nullptr;
uint64_t currentGuid = 0;
int ticksSinceTransition = 0;
uint32_t stepStartMs = 0;
bool suppressEvents = false; // read by the FireEvent hook
bool lootMode = false; // LootAllCorpses (true) vs ScanNearbyLoot (false)
std::vector<uint64_t> queue;
@@ -99,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,
@@ -128,54 +156,18 @@ using LootMoney_t = void(__fastcall *)(void *player);
using LootController_t = void(__fastcall *)(int lootStructPtr,
int coin,
int unk);
struct C3Vector {
float x, y, z;
};
using GetPosition_t = const C3Vector *(__thiscall *)(const void *self,
C3Vector *outBuf);
constexpr int VTBL_GET_POSITION_OFFSET = 0x14;
constexpr float INTERACT_REACH = 1.3333333f;
constexpr float MIN_INTERACT_RANGE = 5.0f;
// === Range / lootability helpers (identical to Loot::Nearby) ===
bool IsLootableUnit(const void *unit) {
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
const uint32_t flags =
Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS);
return (flags & Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
}
float BoundingRadius(const void *unit) {
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
return Game::Read<float>(fields, Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
}
const C3Vector *GetPosition(const void *unit, C3Vector *outBuf) {
auto vtable = *reinterpret_cast<void *const *const *>(unit);
auto fn = reinterpret_cast<GetPosition_t>(
*reinterpret_cast<const void *const *>(
reinterpret_cast<const uint8_t *>(vtable) +
VTBL_GET_POSITION_OFFSET));
return fn(unit, outBuf);
}
bool InInteractRange(const void *player, const void *target) {
C3Vector pBuf{};
C3Vector tBuf{};
const C3Vector *pPos = GetPosition(player, &pBuf);
const C3Vector *tPos = GetPosition(target, &tBuf);
if (pPos == nullptr || tPos == nullptr)
return false;
const float dx = pPos->x - tPos->x;
const float dy = pPos->y - tPos->y;
const float dz = pPos->z - tPos->z;
const float distSq = dx * dx + dy * dy + dz * dz;
float range = BoundingRadius(player) + BoundingRadius(target) + INTERACT_REACH;
if (range < MIN_INTERACT_RANGE)
range = MIN_INTERACT_RANGE;
return distSq <= range * range;
// 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 ===
@@ -278,17 +270,37 @@ void TryStartNext() {
StartLoot(guid);
}
void StartLoot(uint64_t guid) {
void *target = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0);
// Not sent (mounted mid-walk, or the engine's pre-checks refused): no
// response is coming, so skip without waiting out the timeout.
if (target == nullptr || !Request::Send(s_scan.player, target, guid)) {
void BeginStep(State state) {
s_scan.state = state;
s_scan.stepStartMs = Time::Clock::NowMs();
}
// Sends the current corpse's loot request. The corpse is checked again
// here, not only when the queue was built: the walk takes a round trip per
// corpse, and by now the corpse may be gone, looted, or out of reach
// because the player moved. The server refuses an out-of-reach corpse
// with an error, so skip it instead.
//
// When the engine refuses the send itself (the player is moving or
// falling, ...), the corpse stays current and `OnTick` retries it: the
// refusal is about the player, not the corpse, so skipping would drop
// every corpse left in the queue.
void SendCurrent() {
void *target =
Object::ByGuid(Offsets::TYPEMASK_UNIT, s_scan.currentGuid, nullptr, 0);
if (target == nullptr || !Request::IsLootable(target) ||
!Request::InReach(s_scan.player, target)) {
TryStartNext();
return;
}
if (Request::Send(s_scan.player, target, s_scan.currentGuid))
BeginStep(State::WaitingForResponse);
}
void StartLoot(uint64_t guid) {
s_scan.currentGuid = guid;
s_scan.state = State::WaitingForResponse;
s_scan.ticksSinceTransition = 0;
BeginStep(State::WaitingToSend);
SendCurrent();
}
// === Hooks ===
@@ -353,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
@@ -383,18 +399,45 @@ const Game::HookAutoRegister _hookController{
const Event::SignalHook::AutoSubscribe _lootIntercept{&SuppressLootEvents};
// === Tick (timeout only) ===
// 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;
++s_scan.ticksSinceTransition;
if (s_scan.ticksSinceTransition >= kStepTimeoutTicks) {
// Controller hook never fired — the server answered with a
// failure, or never answered. A failure already cleared the
// held target; an unanswered request still holds it, which
// would block all looting, so release it. Skip this corpse.
s_scan.suppressEvents = false;
const bool timedOut = Time::Clock::Elapsed(s_scan.stepStartMs) >= kStepTimeoutMs;
if (s_scan.state == State::WaitingToSend) {
// Still refused: the player has not stopped moving, or mounted,
// or lost control. Every corpse left would be refused the same
// way, so end the walk.
if (timedOut) {
Complete();
return;
}
SendCurrent();
return;
}
// The server refused the corpse ("too far away", not ours, ...): its
// failure response and its release response both clear the held
// target, and no window opens. Go on to the next corpse now.
if (Request::HeldTarget(s_scan.player) != s_scan.currentGuid) {
TryStartNext();
return;
}
if (timedOut) {
// No answer at all. The held target would block all looting, so
// release it, and skip this corpse.
Request::ReleaseIfUnanswered(s_scan.player, s_scan.currentGuid);
TryStartNext();
}
@@ -405,27 +448,30 @@ const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
// === Enumeration ===
struct EnumCtx {
const void *player;
void *player;
std::vector<uint64_t> *out;
bool skipKnownEmpty;
};
int __fastcall EnumCallback(EnumCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
void *obj = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0);
if (obj == nullptr)
return 1;
if (!IsLootableUnit(obj))
if (!Request::IsLootable(obj))
return 1;
if (!InInteractRange(ctx->player, obj))
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
@@ -451,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
+31 -48
View File
@@ -48,6 +48,7 @@
#include "Game.h"
#include "Offsets.h"
#include "model/M2.h"
#include "time/Clock.h"
#include <cstdint>
@@ -58,11 +59,7 @@ namespace {
using PaneFn_t = void(__thiscall *)(void *pane);
using SetModelInstance_t = void(__thiscall *)(void *frame, void *model);
using PlayAnimation_t = void(__thiscall *)(void *model, int keyBone, uint32_t id, int variation,
int offset, float speed, int blend, int primary);
using GetKeyBoneAnim_t = uint32_t(__thiscall *)(void *model, int keyBone);
using LookupIndex_t = uint16_t(__fastcall *)(uint32_t id, const void *table);
using ModelLoaded_t = int(__thiscall *)(void *model, int tryLoad, int recurseChildren);
struct PaneState {
bool doBlend = false;
@@ -78,21 +75,13 @@ std::unordered_map<void *, PaneState> g_panes;
PaneFn_t g_origUpdateModel = nullptr;
SetModelInstance_t g_origSetModelInstance = nullptr;
void *PaneModel(void *pane) {
return Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
}
bool IsLoaded(void *model) {
return reinterpret_cast<ModelLoaded_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(model, 0, 0) != 0;
}
uint32_t KeyBoneAnim(void *model) {
return reinterpret_cast<GetKeyBoneAnim_t>(Offsets::FUN_M2MODEL_GET_KEY_BONE_ANIM)(model, -1);
}
void Arm(void *model, const PaneState &s) {
reinterpret_cast<PlayAnimation_t>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(
model, -1, s.animation, s.variation, 0, s.frozen ? 0.0f : 1.0f, s.doBlend ? 1 : 0, 1);
M2::PlayAnimation(model, -1, s.animation, s.variation, 0, s.frozen ? 0.0f : 1.0f,
s.doBlend ? 1 : 0, 1);
// PlayAnimation stores the id only when it found a sequence to play.
if (!s.frozen || KeyBoneAnim(model) != s.animation)
return;
@@ -109,11 +98,11 @@ void __fastcall UpdateModel_h(void *pane, void * /*edx*/) {
}
PaneState &s = it->second;
reinterpret_cast<PaneFn_t>(Offsets::FUN_SIMPLEMODEL_UPDATE_MODEL)(pane);
void *model = PaneModel(pane);
void *model = Pane::Model(pane);
const uint32_t deadline = Game::Read<uint32_t>(pane, Offsets::OFF_PLAYERMODEL_TURN_DEADLINE);
if (Game::Read<int>(pane, Offsets::OFF_PLAYERMODEL_TURNING) == 0 &&
(deadline == 0 || Time::Clock::Reached(deadline)) && model != nullptr && IsLoaded(model) &&
(s.pending || KeyBoneAnim(model) != s.animation)) {
(deadline == 0 || Time::Clock::Reached(deadline)) && model != nullptr &&
M2::IsLoaded(model, 0, 0) && (s.pending || KeyBoneAnim(model) != s.animation)) {
Arm(model, s);
s.pending = false;
Game::Ref<uint32_t>(pane, Offsets::OFF_PLAYERMODEL_TURN_DEADLINE) = 0;
@@ -135,29 +124,33 @@ const Game::HookAutoRegister _setModelHook{Offsets::FUN_SIMPLEMODEL_SET_MODEL_IN
reinterpret_cast<void *>(&SetModelInstance_h),
reinterpret_cast<void **>(&g_origSetModelInstance)};
void *ResolvePane(void *L) {
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
// SetAnimation / FreezeAnimation: the pane rests on `animation`, re-armed on
// the next update.
void Rest(void *pane, uint32_t animation, int variation, bool frozen, uint32_t frame) {
PaneState &s = g_panes[pane];
s.resting = true;
s.pending = true;
s.frozen = frozen;
s.animation = animation;
s.variation = variation;
s.frame = frame;
}
int __fastcall Script_SetAnimation(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: SetAnimation(animation [, variation])");
return 0;
}
PaneState &s = g_panes[pane];
s.resting = true;
s.pending = true;
s.frozen = false;
s.animation = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
s.variation = Game::Lua::IsNumber(L, 3) ? static_cast<int>(Game::Lua::ToNumber(L, 3)) : 0;
Rest(pane, static_cast<uint32_t>(Game::Lua::ToNumber(L, 2)),
Game::Lua::IsNumber(L, 3) ? static_cast<int>(Game::Lua::ToNumber(L, 3)) : 0, false, 0);
return 0;
}
int __fastcall Script_FreezeAnimation(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3) || !Game::Lua::IsNumber(L, 4)) {
@@ -165,24 +158,20 @@ int __fastcall Script_FreezeAnimation(void *L) {
return 0;
}
const double frame = Game::Lua::ToNumber(L, 4);
PaneState &s = g_panes[pane];
s.resting = true;
s.pending = true;
s.frozen = true;
s.animation = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
s.variation = static_cast<int>(Game::Lua::ToNumber(L, 3));
s.frame = frame > 0.0 ? static_cast<uint32_t>(frame) : 0;
Rest(pane, static_cast<uint32_t>(Game::Lua::ToNumber(L, 2)),
static_cast<int>(Game::Lua::ToNumber(L, 3)), true,
frame > 0.0 ? static_cast<uint32_t>(frame) : 0);
return 0;
}
int __fastcall Script_SetDoBlend(void *L) {
if (void *pane = ResolvePane(L))
if (void *pane = Game::Lua::ResolvePlayerModel(L))
g_panes[pane].doBlend = Game::Lua::ToBoolean(L, 2) != 0;
return 0;
}
int __fastcall Script_GetDoBlend(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
const auto it = g_panes.find(pane);
@@ -191,7 +180,7 @@ int __fastcall Script_GetDoBlend(void *L) {
}
int __fastcall Script_HasAnimation(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
@@ -200,15 +189,10 @@ int __fastcall Script_HasAnimation(void *L) {
}
const auto id = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
bool has = false;
void *model = PaneModel(pane);
if (model != nullptr && IsLoaded(model)) {
const auto *data = Game::Read<const uint8_t *>(
Game::Read<const uint8_t *>(model, Offsets::OFF_M2MODEL_SHARED),
Offsets::OFF_M2SHARED_DATA);
const uint16_t index = reinterpret_cast<LookupIndex_t>(Offsets::FUN_M2_LOOKUP_INDEX)(
id, data + Offsets::OFF_M2DATA_ANIMATION_LOOKUP);
has = index < Game::Read<uint32_t>(data, Offsets::OFF_M2DATA_SEQUENCE_COUNT);
}
void *model = Pane::Model(pane);
if (model != nullptr && M2::IsLoaded(model, 0, 0))
has = M2::LookupIndex(model, Offsets::OFF_M2DATA_ANIMATION_LOOKUP, id) <
Game::Read<uint32_t>(M2::Data(model), Offsets::OFF_M2DATA_SEQUENCE_COUNT);
Game::Lua::PushBool(L, has);
return 1;
}
@@ -265,8 +249,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+15 -33
View File
@@ -41,6 +41,7 @@
#include "Game.h"
#include "Offsets.h"
#include "model/M2.h"
#include <cmath>
#include <cstdint>
@@ -55,19 +56,13 @@ struct Settings {
};
std::unordered_map<void *, Settings> g_settings;
using GetVector_t = void(__fastcall *)(void *camera, uint32_t id, float *out);
using GetScalar_t = float(__fastcall *)(void *camera, uint32_t id);
using SetVector_t = void(__fastcall *)(void *camera, uint32_t id, const float *value,
uint32_t keep);
using SetScalar_t = void(__fastcall *)(void *camera, uint32_t id, float value);
using GetCameraCount_t = uint32_t(__thiscall *)(void *model);
using GetCamera_t = void *(__thiscall *)(void *model, uint32_t index);
using LookupIndex_t = uint16_t(__fastcall *)(uint32_t id, const void *table);
using SetAnimated_t = void(__thiscall *)(void *model, int animated);
using GetScene_t = void *(__thiscall *)(void *frame);
using SceneAnimate_t = void(__thiscall *)(void *scene, const float *cameraPos);
using UpdateModel_t = void(__thiscall *)(void *frame);
using ModelLoaded_t = int(__thiscall *)(void *model, int tryLoad, int recurseChildren);
using ConfigureCamera_t = void(__thiscall *)(void *pane);
} // namespace
@@ -76,27 +71,21 @@ using ConfigureCamera_t = void(__thiscall *)(void *pane);
void *CameraById(void *model, uint32_t id) {
const uint32_t count =
reinterpret_cast<GetCameraCount_t>(Offsets::FUN_M2MODEL_GET_CAMERA_COUNT)(model);
const uint8_t *data = Game::Read<const uint8_t *>(
Game::Read<const uint8_t *>(model, Offsets::OFF_M2MODEL_SHARED), Offsets::OFF_M2SHARED_DATA);
const uint16_t index = reinterpret_cast<LookupIndex_t>(Offsets::FUN_M2_LOOKUP_INDEX)(
id, data + Offsets::OFF_M2DATA_CAMERA_ID_LOOKUP);
const uint16_t index = M2::LookupIndex(model, Offsets::OFF_M2DATA_CAMERA_ID_LOOKUP, id);
if (index >= count)
return nullptr;
return reinterpret_cast<GetCamera_t>(Offsets::FUN_M2MODEL_GET_CAMERA)(model, index);
}
namespace {
struct Vec3 {
float x, y, z;
};
Vec3 GetVector(void *camera, uint32_t id) {
using Fn = void(__fastcall *)(void *camera, uint32_t id, float *out);
Vec3 v{};
reinterpret_cast<GetVector_t>(Offsets::FUN_CAMERA_GET_VECTOR)(camera, id, &v.x);
reinterpret_cast<Fn>(Offsets::FUN_CAMERA_GET_VECTOR)(camera, id, &v.x);
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);
}
@@ -117,10 +106,9 @@ void AnimateUnrotated(void *pane, void *model) {
facing = 0.0f;
const void *vtable = Game::Read<const void *>(pane, 0);
Game::Read<UpdateModel_t>(vtable, Offsets::VTBL_SIMPLEMODEL_UPDATE_MODEL)(pane);
reinterpret_cast<SetAnimated_t>(Offsets::FUN_M2MODEL_SET_ANIMATED)(model, 1);
M2::SetAnimated(model, true);
const float origin[3] = {0.0f, 0.0f, 0.0f};
void *scene = reinterpret_cast<GetScene_t>(Offsets::FUN_SIMPLEMODEL_GET_SCENE)(pane);
reinterpret_cast<SceneAnimate_t>(Offsets::FUN_M2SCENE_ANIMATE)(scene, origin);
M2::AnimateScene(Pane::Scene(pane), origin);
facing = saved;
}
@@ -131,7 +119,7 @@ Vec3 Lerp(const Vec3 &from, const Vec3 &to, float t) {
// 4.3.4 ConfigureCamera's last two steps, on the camera vanilla installed.
void ApplySettings(void *pane, const Settings &s) {
void *model = Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
void *model = Pane::Model(pane);
void *camera = Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODEL_CAMERA);
if (model == nullptr || camera == nullptr)
return;
@@ -190,19 +178,14 @@ const Game::HookAutoRegister _cameraHook{
// be loaded: ConfigureCamera reads its cameras and bounds, and an unloaded
// model is configured by the ready hook when it arrives, settings included.
void Reconfigure(void *pane) {
void *model = Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
if (model == nullptr ||
!reinterpret_cast<ModelLoaded_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(model, 0, 0))
void *model = Pane::Model(pane);
if (model == nullptr || !M2::IsLoaded(model, 0, 0))
return;
reinterpret_cast<ConfigureCamera_t>(Offsets::FUN_PLAYERMODEL_CONFIGURE_CAMERA)(pane);
}
void *ResolvePane(void *L) {
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
}
int __fastcall Script_SetPortraitZoom(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
@@ -219,7 +202,7 @@ int __fastcall Script_SetPortraitZoom(void *L) {
}
int __fastcall Script_SetCamDistanceScale(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
@@ -241,7 +224,7 @@ int __fastcall Script_SetCamDistanceScale(void *L) {
}
int __fastcall Script_RefreshCamera(void *L) {
if (void *pane = ResolvePane(L))
if (void *pane = Game::Lua::ResolvePlayerModel(L))
Reconfigure(pane);
return 0;
}
@@ -274,8 +257,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+7
View File
@@ -14,7 +14,14 @@
namespace Model::Camera {
struct Vec3 {
float x, y, z;
};
// 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).
Vec3 GetVector(void *camera, uint32_t id);
} // namespace Model::Camera
+54 -106
View File
@@ -55,6 +55,8 @@
#include "dbc/Lookup.h"
#include "model/DisplayInfo.h"
#include "model/ItemDisplay.h"
#include "model/M2.h"
#include "model/Teardown.h"
#include "tick/WorldTick.h"
#include <cstdint>
@@ -96,8 +98,6 @@ 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 ModelAddRef_t = void(__fastcall *)(void *instance);
using ModelLoaded_t = int(__thiscall *)(void *instance, int tryLoad, int recurseChildren);
// 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
@@ -107,100 +107,62 @@ using ModelLoaded_t = int(__thiscall *)(void *instance, int tryLoad, int recurse
// lifetime, and it holds the ref on the model instance — until the frame
// loads another display or the reload teardown.
struct DressJob {
void *frame = nullptr; // CSimpleModelFFX that requested the dress
void *instance = nullptr; // model instance the job is bound to
void *instance = nullptr; // model instance the job is bound to
const uint8_t *extraRec = nullptr; // CreatureDisplayInfoExtra (DBC memory, stable)
void *builder = nullptr; // engine compositor (owned)
void *builder = nullptr; // engine compositor (owned)
bool configured = false;
bool done = false;
int ticks = 0;
};
// Model frames are few and permanent; SetDisplayInfo users fewer still.
DressJob g_jobs[8];
std::unordered_map<void *, DressJob> g_jobs; // by frame
// Give up on a model that never finishes loading (bad path, missing file).
constexpr int kMaxDressTicks = 600;
void CancelJob(DressJob &job) {
DestroyDresser(job.builder);
job = DressJob{};
void CancelJob(void *frame) {
const auto it = g_jobs.find(frame);
if (it == g_jobs.end())
return;
DestroyDresser(it->second.builder);
g_jobs.erase(it);
}
DressJob *FindJob(void *frame) {
for (auto &job : g_jobs)
if (job.frame == frame)
return &job;
return nullptr;
}
// Claim a slot for `frame`: its own existing slot, a free one, or — all busy —
// a finished one (the dressed model keeps its geosets and texture refs; only
// the kept-alive builder is dropped early).
DressJob *ClaimJob(void *frame) {
DressJob *slot = FindJob(frame);
if (slot == nullptr)
for (auto &job : g_jobs)
if (job.frame == nullptr) {
slot = &job;
break;
}
if (slot == nullptr)
for (auto &job : g_jobs)
if (job.done) {
slot = &job;
break;
}
if (slot == nullptr)
slot = &g_jobs[0];
CancelJob(*slot);
return slot;
}
// Queue a dress for a character display just loaded into `frame`.
void ScheduleDress(void *frame, const uint8_t *extraRec) {
DressJob *job = ClaimJob(frame);
job->frame = frame;
job->instance = Game::Read<void *>(frame,
Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
job->extraRec = extraRec;
}
// The frame's scene is about to free the model its builder holds.
const Model::Teardown::AutoSubscribe _teardown{&CancelJob};
// One WorldTick step for one job: wait for the load, then configure + feed
// equipment (the engine's order: SetInfo, then the 10 Extra equipment slots),
// then pump until done. Field-for-field mirror of FUN_005fb200's NPC branch.
void StepJob(DressJob &job) {
void *instance = Game::Read<void *>(job.frame,
Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
// equipment, then pump until done. False once the job is dead.
bool StepJob(void *frame, DressJob &job) {
void *instance = Pane::Model(frame);
if (job.instance == nullptr)
job.instance = instance;
if (instance == nullptr || instance != job.instance || ++job.ticks > kMaxDressTicks) {
CancelJob(job); // model swapped out from under us / never loaded
return;
}
if (instance == nullptr || instance != job.instance || ++job.ticks > kMaxDressTicks)
return false; // model swapped out from under us / never loaded
if (!job.configured) {
if (!reinterpret_cast<ModelLoaded_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(instance, 0, 0))
return;
if (!M2::IsLoaded(instance, 0, 0))
return true;
job.builder = CreateDresser(instance, job.extraRec);
if (job.builder == nullptr) {
CancelJob(job);
return;
}
if (job.builder == nullptr)
return false;
// The helm/shoulder feeds attach child model instances.
LightChildren(job.frame, instance);
LightChildren(frame, instance);
job.configured = true;
}
if (PumpDresser(job.builder))
job.done = true; // builder kept — it owns the model ref for the dressed model
return true;
}
void OnWorldTick() {
for (auto &job : g_jobs)
if (job.frame != nullptr && !job.done)
StepJob(job);
for (auto it = g_jobs.begin(); it != g_jobs.end();) {
if (it->second.done || StepJob(it->first, it->second)) {
++it;
continue;
}
DestroyDresser(it->second.builder);
it = g_jobs.erase(it);
}
}
const Tick::WorldTick::AutoSubscribe g_tick{&OnWorldTick};
@@ -216,8 +178,7 @@ AttachDisplayModel_t g_origAttach = nullptr;
void __fastcall AttachDisplayModel_h(void *pane, void * /*edx*/, const uint8_t *record) {
// Any pending or kept dress for this pane is about to point at the wrong
// model — drop it before the original swaps the instance.
if (DressJob *job = FindJob(pane))
CancelJob(*job);
CancelJob(pane);
if (Model::ItemDisplay::Attach(pane, record))
return;
@@ -227,7 +188,7 @@ void __fastcall AttachDisplayModel_h(void *pane, void * /*edx*/, const uint8_t *
return;
if (const uint8_t *extraRec =
DressRecord(Game::Read<uint32_t>(record, Offsets::OFF_CREATURE_DISPLAYID)))
ScheduleDress(pane, extraRec);
g_jobs[pane] = DressJob{Pane::Model(pane), extraRec};
}
const Game::HookAutoRegister _attachHook{
@@ -237,31 +198,20 @@ const Game::HookAutoRegister _attachHook{
// ---- Lua methods --------------------------------------------------------------
using SetDisplay_t = void(__thiscall *)(void *pane, const uint8_t *record);
void SetDisplay(void *pane, const uint8_t *record) {
reinterpret_cast<SetDisplay_t>(Offsets::FUN_PLAYERMODEL_SET_DISPLAY)(pane, record);
}
// The record a pane shows for SetDisplayInfo: only the display-ID field is
// ever read. One per pane, never freed — the pane keeps the pointer and
// rebuilds from it on every show, and panes live as long as the UI.
struct DisplayRecord {
uint8_t bytes[Offsets::OFF_CREATURE_DISPLAYID + sizeof(uint32_t)];
};
std::unordered_map<void *, DisplayRecord> g_displayRecords;
// The record each pane shows for SetDisplayInfo.
std::unordered_map<void *, Pane::DisplayRecord> g_displayRecords;
void ShowDisplayID(void *pane, uint32_t displayID) {
if (displayID == 0)
return;
DisplayRecord &rec = g_displayRecords[pane];
std::memset(rec.bytes, 0, sizeof(rec.bytes));
std::memcpy(rec.bytes + Offsets::OFF_CREATURE_DISPLAYID, &displayID, sizeof(displayID));
SetDisplay(pane, rec.bytes);
Pane::DisplayRecord &rec = g_displayRecords[pane];
rec = {};
Game::Ref<uint32_t>(rec.bytes, Offsets::OFF_CREATURE_DISPLAYID) = displayID;
Pane::SetDisplay(pane, rec.bytes);
}
int __fastcall Script_SetDisplayInfo(void *L) {
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
@@ -273,7 +223,7 @@ int __fastcall Script_SetDisplayInfo(void *L) {
}
int __fastcall Script_SetCreature(void *L) {
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
@@ -287,7 +237,7 @@ int __fastcall Script_SetCreature(void *L) {
if (Game::Lua::IsNumber(L, 3))
ShowDisplayID(pane, static_cast<uint32_t>(Game::Lua::ToNumber(L, 3)));
else
SetDisplay(pane, record);
Pane::SetDisplay(pane, record);
return 0;
}
@@ -316,12 +266,19 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
// Frames are rebuilt on /reload and a new frame can reuse an old address. The
// dress jobs are gone by now: each went with its frame (Model::Teardown).
void PrepareForReload() {
g_displayRecords.clear();
}
const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
} // namespace
// The engine's own early-out: an Extra record with no bake name gets no
@@ -371,7 +328,7 @@ void *CreateDresser(void *instance, const uint8_t *extraRec) {
std::memcpy(info.geosetDefaults, kGeosetGroupDefaults, sizeof(kGeosetGroupDefaults));
// The builder owns this ref (releases it on destroy / re-configure).
reinterpret_cast<ModelAddRef_t>(Offsets::FUN_MODEL_INSTANCE_ADDREF)(instance);
M2::AddRef(instance);
if (!reinterpret_cast<CharCompSetInfo_t>(Offsets::FUN_CHARCOMP_SET_INFO)(builder, &info)) {
DestroyDresser(builder); // releases the model ref already copied in
return nullptr;
@@ -391,14 +348,12 @@ 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) {
using SetRenderCb_t = void(__thiscall *)(void *instance, const void *cb, void *userData);
auto setRenderCb = reinterpret_cast<SetRenderCb_t>(Offsets::FUN_MODEL_INSTANCE_SET_RENDER_CB);
const void *lightFogCb =
reinterpret_cast<const void *>(Offsets::FUN_SIMPLEMODELFFX_LIGHT_FOG_CB);
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))
setRenderCb(child, lightFogCb, frame);
M2::SetRenderCallback(child, lightFogCb, frame);
}
bool PumpDresser(void *dresser) {
@@ -410,11 +365,4 @@ void DestroyDresser(void *dresser) {
reinterpret_cast<CharCompDestroy_t>(Offsets::FUN_CHARCOMP_DESTROY)(dresser);
}
void PrepareForReload() {
for (auto &job : g_jobs)
CancelJob(job);
}
static const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
} // namespace Model::DisplayInfo
-5
View File
@@ -31,9 +31,4 @@ void DestroyDresser(void *dresser);
// `frame` gives its own model, so the children are lit like it.
void LightChildren(void *frame, void *instance);
// The reload teardown destroys every Model frame (and with it the model
// instances our pending character-dress jobs point at). Drop all jobs and
// destroy their engine compositors before the engine starts tearing down.
void PrepareForReload();
} // namespace Model::DisplayInfo
+79 -60
View File
@@ -73,6 +73,8 @@
#include "item/ID.h"
#include "item/Location.h"
#include "item/Record.h"
#include "model/ItemDisplay.h"
#include "model/M2.h"
#include "object/Resolve.h"
#include <cstdint>
@@ -94,9 +96,7 @@ 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 ModelLoaded_t = int(__thiscall *)(void *model, int tryLoad, int recurseChildren);
using FindChild_t = void *(__thiscall *)(void *model, int attachId);
using AttachItemVisuals_t = void(__fastcall *)(void *model, uint32_t visualID);
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, ...);
@@ -104,8 +104,6 @@ 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;
constexpr uint8_t kInvTypeShield = 14;
constexpr uint8_t kInvTypeRangedRight = 26;
// Retail's Enum.ItemTryOnReason.
constexpr int kTryOnSuccess = 0;
@@ -113,6 +111,16 @@ 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;
@@ -140,12 +148,8 @@ 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;
void *ResolvePane(void *L) {
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_DRESSUPMODEL_LUA_TYPE_ID);
}
void *PaneModel(void *pane) {
return Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
HeldLane &LaneOf(void *pane, int i) {
return Game::Ref<HeldLane>(pane, kLanes[i].offset);
}
int LaneIndex(uint32_t bodySlot) {
@@ -153,12 +157,10 @@ int LaneIndex(uint32_t bodySlot) {
}
// The glow `enchantID` gives an item shown with `display`: none when the
// display has its own ItemVisuals row (GetItemDescVisual 0x0062EC70).
// display has its own.
uint32_t EnchantVisual(const void *display, uint32_t enchantID) {
if (display == nullptr ||
DBC::Record(Offsets::VAR_ITEMVISUALS_RECORDS, Offsets::VAR_ITEMVISUALS_COUNT,
Game::Read<uint32_t>(display, Offsets::OFF_ITEMDISPLAYINFO_ITEM_VISUAL)) !=
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);
@@ -174,27 +176,33 @@ void HangVisual(void *model, int attachId, uint32_t visual) {
return;
if (void *item = reinterpret_cast<FindChild_t>(Offsets::FUN_M2MODEL_FIND_CHILD)(model,
attachId))
reinterpret_cast<AttachItemVisuals_t>(Offsets::FUN_ATTACH_ITEM_VISUALS)(item, visual);
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 = PaneModel(pane);
void *model = Pane::Model(pane);
if (model == nullptr)
return;
for (int i = 0; i < 2; ++i) {
const uint8_t *lane = Game::Ptr<uint8_t>(pane, kLanes[i].offset);
if (lane[0] == 0 || s.laneDisplay[i] == nullptr)
const HeldLane &lane = LaneOf(pane, i);
if (lane.itemClass == 0 || s.laneDisplay[i] == nullptr)
continue;
const bool shield = lane[3] == kInvTypeShield;
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(
model, kLanes[i].bodySlot, lane[4], shield);
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[4], s.sheathed, shield,
lane[3] == kInvTypeRangedRight);
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]);
}
}
@@ -265,8 +273,9 @@ 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];
s.laneDisplay[LaneIndex(bodySlot)] = nullptr;
s.laneVisual[LaneIndex(bodySlot)] = 0;
const int lane = LaneIndex(bodySlot);
s.laneDisplay[lane] = nullptr;
s.laneVisual[lane] = 0;
}
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(model, bodySlot,
sheatheType, shield);
@@ -294,14 +303,12 @@ __declspec(naked) void HandChoiceThunk() {
}
// ClearModel's lane step: detach the held item and zero the lane.
void ClearLane(void *pane, uintptr_t laneOffset, uint32_t bodySlot) {
uint8_t *lane = Game::Ptr<uint8_t>(pane, laneOffset);
if (lane[0] == 0)
void ClearLane(void *pane, int i) {
HeldLane &lane = LaneOf(pane, i);
if (lane.itemClass == 0)
return;
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(
Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE), bodySlot, lane[4],
lane[3] == kInvTypeShield);
std::memset(lane, 0, 8);
DetachLane(Pane::Model(pane), i, lane);
lane = HeldLane{};
}
// Component slot for a 0-based inventory slot, or -1 (4.3.4 UndressSlot).
@@ -323,7 +330,7 @@ int ComponentSlot(int slot0) {
}
int __fastcall Script_UndressSlot(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolveDressUpModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
@@ -332,13 +339,12 @@ int __fastcall Script_UndressSlot(void *L) {
}
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 2)) - 1;
void *component = Game::Read<void *>(pane, Offsets::OFF_DRESSUPMODEL_COMPONENT);
if (Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE) == nullptr ||
component == nullptr)
if (Pane::Model(pane) == nullptr || component == nullptr)
return 0;
if (slot0 == 15) {
ClearLane(pane, Offsets::OFF_DRESSUPMODEL_MAIN_HAND_LANE, kBodySlotMainHand);
} else if (slot0 == 16) {
ClearLane(pane, Offsets::OFF_DRESSUPMODEL_OFF_HAND_LANE, kBodySlotOffHand);
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)
@@ -399,8 +405,8 @@ uint32_t NamedHand(const char *name) {
name) != 0)
continue;
switch (Game::Read<int>(row, Offsets::OFF_PAPERDOLLITEMFRAME_SLOT_NUMBER)) {
case 16: return kBodySlotMainHand;
case 17: return kBodySlotOffHand;
case Offsets::INVSLOT_MAINHAND: return kBodySlotMainHand;
case Offsets::INVSLOT_OFFHAND: return kBodySlotOffHand;
default: return 0;
}
}
@@ -413,17 +419,35 @@ int TryOnResult(void *pane, uint32_t itemID) {
if (record == nullptr)
return kTryOnDataPending;
if (Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID) == 0 ||
PaneModel(pane) == nullptr)
Pane::Model(pane) == nullptr)
return kTryOnNotEquippable;
switch (Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE)) {
case 1: case 3: case 4: case 5: case 6: case 7: case 8: case 9: case 10:
case 16: case 19: case 20:
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 13: case 14: case 15: case 17: case 21: case 22: case 23: case 25: case 26:
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
default:
return kTryOnNotEquippable;
@@ -431,7 +455,7 @@ int TryOnResult(void *pane, uint32_t itemID) {
}
int __fastcall Script_TryOn(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolveDressUpModel(L);
if (pane == nullptr)
return 0;
// Read before the name search below, which stomps the Lua stack.
@@ -484,18 +508,16 @@ int __fastcall Script_Dress(void *L) {
}
int __fastcall Script_IsGeoReady(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolveDressUpModel(L);
if (pane == nullptr)
return 0;
void *model = PaneModel(pane);
Game::Lua::PushBool(L, model != nullptr &&
reinterpret_cast<ModelLoaded_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(
model, 0, 1) != 0);
void *model = Pane::Model(pane);
Game::Lua::PushBool(L, model != nullptr && M2::IsLoaded(model, 0, 1));
return 1;
}
int __fastcall Script_SetSheathed(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolveDressUpModel(L);
if (pane == nullptr)
return 0;
PaneState &s = g_panes[pane];
@@ -507,7 +529,7 @@ int __fastcall Script_SetSheathed(void *L) {
}
int __fastcall Script_GetSheathed(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolveDressUpModel(L);
if (pane == nullptr)
return 0;
const auto it = g_panes.find(pane);
@@ -516,13 +538,13 @@ int __fastcall Script_GetSheathed(void *L) {
}
int __fastcall Script_SetAutoDress(void *L) {
if (void *pane = ResolvePane(L))
if (void *pane = Game::Lua::ResolveDressUpModel(L))
g_panes[pane].autoDress = Game::Lua::ToBoolean(L, 2) != 0;
return 0;
}
int __fastcall Script_GetAutoDress(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolveDressUpModel(L);
if (pane == nullptr)
return 0;
const auto it = g_panes.find(pane);
@@ -618,11 +640,8 @@ void RegisterLuaFunctions() {
reinterpret_cast<const void *>(&SetModelInstance));
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_DRESSUPMODEL_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
Game::Lua::RegisterIntegerEnum(
"Enum", "ItemTryOnReason", kItemTryOnReason,
static_cast<int>(sizeof(kItemTryOnReason) / sizeof(kItemTryOnReason[0])), "UIModelInfo");
g_methodDocs);
Game::Lua::RegisterIntegerEnum("Enum", "ItemTryOnReason", kItemTryOnReason, "UIModelInfo");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+2 -2
View File
@@ -47,8 +47,8 @@ const Game::Doc::Method g_methodDocs[] = {
};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_MODEL_METHOD_REGISTRY),
g_methods, 1, g_methodDocs, 1);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_MODEL_METHOD_REGISTRY), g_methods, g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+37 -55
View File
@@ -53,6 +53,7 @@
#include "item/Record.h"
#include "model/DisplayInfo.h"
#include "model/ItemDisplay.h"
#include "model/M2.h"
#include "texture/Load.h"
#include <cstdint>
@@ -64,24 +65,9 @@ namespace Model::ItemDisplay {
namespace {
using PaneRecordFn_t = void(__thiscall *)(void *pane, const uint8_t *record);
using SetModelInstance_t = void(__thiscall *)(void *frame, void *model);
using GetScene_t = void *(__thiscall *)(void *frame);
using CreateModel_t = void *(__thiscall *)(void *scene, const char *path, uint32_t flags);
using ModelRelease_t = void(__fastcall *)(void *model);
using TextureLoad_t = void *(__fastcall *)(const char *path, void *status, uint32_t flags,
int arg4, int arg5);
using ReplaceTexture_t = void(__thiscall *)(void *model, uint32_t type, void *texture);
using AttachItemVisuals_t = void(__fastcall *)(void *model, uint32_t visualID);
using SetAnimDone_t = void(__thiscall *)(void *model, const void *callback, uint32_t userLow,
uint32_t userHigh);
using PlayAnimation_t = void(__thiscall *)(void *model, int keyBone, uint32_t id, int variation,
int offset, float speed, int blend, int primary);
constexpr uint32_t kInvTypeHead = 1;
constexpr uint32_t kInvTypeShoulders = 3;
constexpr uint32_t kInvTypeShield = 14;
constexpr uint32_t kInvTypeAmmo = 24;
constexpr uint32_t kInvTypeQuiver = 27;
// The texture type an item model takes its skin as.
constexpr uint32_t kItemSkinTexture = 2;
@@ -90,27 +76,25 @@ constexpr uint32_t kItemTextureFilter = 3;
constexpr int kSexLetters = 3;
constexpr size_t kPathSize = 260;
// The record a pane shows for SetItem. The pane reads only the display-ID
// field, which stays 0. One per pane, never freed while the UI lives: the pane
// keeps the pointer and builds from it on every show.
// The record a pane shows for SetItem; its display ID stays 0.
struct ItemRecord {
uint8_t display[Offsets::OFF_CREATURE_DISPLAYID + sizeof(uint32_t)];
uint32_t itemID;
uint32_t visual;
Pane::DisplayRecord display;
uint32_t itemID = 0;
uint32_t visual = 0;
};
std::unordered_map<void *, ItemRecord> g_records;
const char *Directory(uint32_t inventoryType) {
switch (inventoryType) {
case kInvTypeHead:
case Offsets::INVTYPE_HEAD:
return "Item\\ObjectComponents\\Head\\";
case kInvTypeShoulders:
case Offsets::INVTYPE_SHOULDER:
return "Item\\ObjectComponents\\Shoulder\\";
case kInvTypeShield:
case Offsets::INVTYPE_SHIELD:
return "Item\\ObjectComponents\\Shield\\";
case kInvTypeAmmo:
case Offsets::INVTYPE_AMMO:
return "Item\\ObjectComponents\\Ammo\\";
case kInvTypeQuiver:
case Offsets::INVTYPE_QUIVER:
return "Item\\ObjectComponents\\Quiver\\";
default:
return "Item\\ObjectComponents\\Weapon\\";
@@ -166,15 +150,13 @@ void *CreateItemModel(void *pane, const ItemRecord &rec) {
const char *dir = Directory(inventoryType);
char path[kPathSize];
if (inventoryType == kInvTypeHead) {
if (inventoryType == Offsets::INVTYPE_HEAD) {
if (!HeadPath(path, dir, name))
return nullptr;
} else {
std::snprintf(path, sizeof(path), "%s%s", dir, name);
}
void *scene = reinterpret_cast<GetScene_t>(Offsets::FUN_SIMPLEMODEL_GET_SCENE)(pane);
void *model =
reinterpret_cast<CreateModel_t>(Offsets::FUN_M2SCENE_CREATE_MODEL)(scene, path, 0);
void *model = M2::CreateModel(Pane::Scene(pane), path);
if (model == nullptr)
return nullptr;
@@ -186,7 +168,7 @@ void *CreateItemModel(void *pane, const ItemRecord &rec) {
path, reinterpret_cast<void *>(Offsets::VAR_ITEM_MODEL_TEXTURE_STATUS),
Texture::Flags(kItemTextureFilter, false, false), 1, 0);
if (handle == nullptr) {
reinterpret_cast<ModelRelease_t>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(model);
M2::Release(model);
return nullptr;
}
reinterpret_cast<ReplaceTexture_t>(Offsets::FUN_M2MODEL_REPLACE_TEXTURE)(
@@ -194,13 +176,10 @@ void *CreateItemModel(void *pane, const ItemRecord &rec) {
Texture::ReleaseHandle(handle);
}
const uint32_t own = Game::Read<uint32_t>(display, Offsets::OFF_ITEMDISPLAYINFO_ITEM_VISUAL);
const uint32_t visual = DBC::Record(Offsets::VAR_ITEMVISUALS_RECORDS,
Offsets::VAR_ITEMVISUALS_COUNT, own) != nullptr
? own
: rec.visual;
const uint32_t own = OwnVisual(display);
const uint32_t visual = own != 0 ? own : rec.visual;
if (visual != 0) {
reinterpret_cast<AttachItemVisuals_t>(Offsets::FUN_ATTACH_ITEM_VISUALS)(model, visual);
M2::AttachItemVisuals(model, visual);
Model::DisplayInfo::LightChildren(pane, model);
}
return model;
@@ -210,17 +189,15 @@ void *CreateItemModel(void *pane, const ItemRecord &rec) {
// for the item shows nothing.
void Show(void *pane, const ItemRecord &rec) {
void *model = CreateItemModel(pane, rec);
Game::Read<SetModelInstance_t>(Game::Read<const void *>(pane, 0),
Offsets::VTBL_SIMPLEMODEL_SET_MODEL_INSTANCE)(pane, model);
Pane::SetModel(pane, model);
if (model == nullptr)
return;
const auto user = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(pane));
reinterpret_cast<SetAnimDone_t>(Offsets::FUN_M2MODEL_SET_ANIM_DONE_CALLBACK)(
model, reinterpret_cast<const void *>(Offsets::FUN_PLAYERMODEL_ON_ANIM_DONE), user,
static_cast<int32_t>(user) < 0 ? 0xFFFFFFFFu : 0u);
reinterpret_cast<PlayAnimation_t>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(model, -1, 0, -1, 0,
1.0f, 1, 1);
reinterpret_cast<ModelRelease_t>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(model);
M2::SetAnimDoneCallback(model,
reinterpret_cast<const void *>(Offsets::FUN_PLAYERMODEL_ON_ANIM_DONE),
user, static_cast<int32_t>(user) < 0 ? 0xFFFFFFFFu : 0u);
M2::PlayAnimation(model, -1, 0, -1, 0, 1.0f, 1, 1);
M2::Release(model);
}
// A queued item has loaded: build each visible pane that still shows it,
@@ -229,18 +206,16 @@ void OnItemLoaded(uint32_t itemID, bool success) {
if (!success)
return;
for (auto &[pane, rec] : g_records)
if (rec.itemID == itemID &&
Game::Read<const uint8_t *>(pane, Offsets::OFF_PLAYERMODEL_DISPLAY_RECORD) ==
rec.display &&
Game::Read<int>(pane, Offsets::OFF_FRAME_VISIBLE) != 0)
if (rec.itemID == itemID && Pane::DisplayRecordOf(pane) == rec.display.bytes &&
Pane::IsVisible(pane))
reinterpret_cast<PaneRecordFn_t>(Offsets::FUN_PLAYERMODEL_ATTACH_DISPLAY_MODEL)(
pane, rec.display);
pane, rec.display.bytes);
}
const ::Item::Data::LoadSubscriber _loadSub{&OnItemLoaded};
int __fastcall Script_SetItem(void *L) {
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
@@ -252,7 +227,7 @@ int __fastcall Script_SetItem(void *L) {
rec.itemID = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
if (Game::Lua::IsNumber(L, 4))
rec.visual = static_cast<uint32_t>(Game::Lua::ToNumber(L, 4));
reinterpret_cast<PaneRecordFn_t>(Offsets::FUN_PLAYERMODEL_SET_DISPLAY)(pane, rec.display);
Pane::SetDisplay(pane, rec.display.bytes);
return 0;
}
@@ -274,8 +249,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
@@ -289,9 +263,17 @@ const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
} // namespace
uint32_t OwnVisual(const uint8_t *display) {
const uint32_t own = Game::Read<uint32_t>(display, Offsets::OFF_ITEMDISPLAYINFO_ITEM_VISUAL);
return DBC::Record(Offsets::VAR_ITEMVISUALS_RECORDS, Offsets::VAR_ITEMVISUALS_COUNT, own) !=
nullptr
? own
: 0;
}
bool Attach(void *pane, const uint8_t *record) {
const auto it = g_records.find(pane);
if (record == nullptr || it == g_records.end() || record != it->second.display)
if (record == nullptr || it == g_records.end() || record != it->second.display.bytes)
return false;
if (::Item::PeekRecord(it->second.itemID) == nullptr)
::Item::Data::WarmCache(it->second.itemID);
+5
View File
@@ -22,4 +22,9 @@ namespace Model::ItemDisplay {
// other record returns false and is the engine's to build.
bool Attach(void *pane, const uint8_t *record);
// The ItemVisuals row an ItemDisplayInfo `display` hangs itself, or 0 when it
// names none that exists. An enchant's glow shows only when this is 0
// (GetItemDescVisual 0x0062EC70).
uint32_t OwnVisual(const uint8_t *display);
} // namespace Model::ItemDisplay
+14 -23
View File
@@ -30,6 +30,7 @@
#include "Game.h"
#include "Offsets.h"
#include "model/M2.h"
#include <cstdint>
#include <unordered_set>
@@ -40,7 +41,6 @@ namespace {
using PaneFn_t = void(__thiscall *)(void *pane);
using SetUnitGuid_t = void(__thiscall *)(void *pane, uint64_t guid);
using SetDisplay_t = void(__thiscall *)(void *pane, const uint8_t *record);
using SetModelInstance_t = void(__thiscall *)(void *pane, void *model);
PaneFn_t g_origHide = nullptr;
PaneFn_t g_origShow = nullptr;
@@ -53,19 +53,16 @@ bool Keeps(void *pane) {
return g_keeping.count(pane) != 0;
}
bool HasModel(void *pane) {
return Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE) != nullptr;
}
// The pane Hide's own drop: slot 36 with null.
void DropModel(void *pane) {
Game::Read<SetModelInstance_t>(Game::Read<const void *>(pane, 0),
Offsets::VTBL_SIMPLEMODEL_SET_MODEL_INSTANCE)(pane, nullptr);
// Hide's own drop (slot 36 with null), for a hidden pane still holding a
// model.
void DropIfHidden(void *pane) {
if (!Pane::IsVisible(pane) && Pane::Model(pane) != nullptr)
Pane::SetModel(pane, nullptr);
}
void DropIfKeptWhileHidden(void *pane) {
if (Game::Read<int>(pane, Offsets::OFF_FRAME_VISIBLE) == 0 && HasModel(pane) && Keeps(pane))
DropModel(pane);
if (Keeps(pane))
DropIfHidden(pane);
}
void __fastcall Hide_h(void *pane, void * /*edx*/) {
@@ -77,7 +74,7 @@ void __fastcall Hide_h(void *pane, void * /*edx*/) {
}
void __fastcall Show_h(void *pane, void * /*edx*/) {
if (Keeps(pane) && HasModel(pane)) {
if (Keeps(pane) && Pane::Model(pane) != nullptr) {
reinterpret_cast<PaneFn_t>(Offsets::FUN_SIMPLEFRAME_SHOW)(pane);
return;
}
@@ -107,26 +104,21 @@ const Game::HookAutoRegister _setDisplayHook{Offsets::FUN_PLAYERMODEL_SET_DISPLA
reinterpret_cast<void *>(&SetDisplay_h),
reinterpret_cast<void **>(&g_origSetDisplay)};
void *ResolvePane(void *L) {
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
}
int __fastcall Script_SetKeepModelOnHide(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (Game::Lua::ToBoolean(L, 2) != 0) {
g_keeping.insert(pane);
return 0;
}
if (g_keeping.erase(pane) != 0 && Game::Read<int>(pane, Offsets::OFF_FRAME_VISIBLE) == 0 &&
HasModel(pane))
DropModel(pane);
if (g_keeping.erase(pane) != 0)
DropIfHidden(pane);
return 0;
}
int __fastcall Script_GetKeepModelOnHide(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
Game::Lua::PushBool(L, Keeps(pane));
@@ -156,8 +148,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+148
View File
@@ -0,0 +1,148 @@
// 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/>.
// Typed calls into the engine's M2 model instances and the Model frames
// ("panes") that show them, shared by every model/ module.
#pragma once
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
namespace Model::M2 {
inline bool IsLoaded(void *model, int tryLoad, int recurseChildren) {
using Fn = int(__thiscall *)(void *, int, int);
return reinterpret_cast<Fn>(Offsets::FUN_MODEL_INSTANCE_LOADED)(model, tryLoad,
recurseChildren) != 0;
}
// The draw gate: also makes the device textures when `wait` is set.
inline bool IsReady(void *model, int wait, int recurseChildren) {
using Fn = int(__thiscall *)(void *, int, int);
return reinterpret_cast<Fn>(Offsets::FUN_MODEL_INSTANCE_READY)(model, wait,
recurseChildren) != 0;
}
inline void AddRef(void *model) {
reinterpret_cast<void(__fastcall *)(void *)>(Offsets::FUN_MODEL_INSTANCE_ADDREF)(model);
}
inline void Release(void *model) {
reinterpret_cast<void(__fastcall *)(void *)>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(model);
}
// A new model in `scene` that the caller owns one reference on, or null.
inline void *CreateModel(void *scene, const char *path) {
using Fn = void *(__thiscall *)(void *, const char *, uint32_t);
return reinterpret_cast<Fn>(Offsets::FUN_M2SCENE_CREATE_MODEL)(scene, path, 0);
}
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);
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(model, keyBone, id, variation,
offset, speed, blend, primary);
}
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);
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_ANIM_DONE_CALLBACK)(model, callback, userLow,
userHigh);
}
inline void SetVisible(void *model, bool visible) {
using Fn = void(__thiscall *)(void *, int);
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_VISIBLE)(model, visible ? 1 : 0);
}
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);
}
inline void AnimateScene(void *scene, const float *cameraPos) {
using Fn = void(__thiscall *)(void *, const float *);
reinterpret_cast<Fn>(Offsets::FUN_M2SCENE_ANIMATE)(scene, cameraPos);
}
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);
}
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);
}
// The model's shared M2 data (the file's header and tables).
inline const uint8_t *Data(void *model) {
return Game::Read<const uint8_t *>(Game::Read<const uint8_t *>(model, Offsets::OFF_M2MODEL_SHARED),
Offsets::OFF_M2SHARED_DATA);
}
// The index `id` maps to in one of the data's lookup tables
// (OFF_M2DATA_*_LOOKUP); the caller bounds it by that table's count.
inline uint16_t LookupIndex(void *model, uintptr_t tableOffset, uint32_t id) {
using Fn = uint16_t(__fastcall *)(uint32_t, const void *);
return reinterpret_cast<Fn>(Offsets::FUN_M2_LOOKUP_INDEX)(id, Data(model) + tableOffset);
}
} // namespace Model::M2
namespace Model::Pane {
// The model instance a Model frame shows, or null.
inline void *Model(const void *pane) {
return Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
}
inline void *Scene(void *pane) {
using Fn = void *(__thiscall *)(void *);
return reinterpret_cast<Fn>(Offsets::FUN_SIMPLEMODEL_GET_SCENE)(pane);
}
// The frame's SetModelInstance virtual; null drops its model.
inline void SetModel(void *pane, void *model) {
using Fn = void(__thiscall *)(void *, void *);
Game::Read<Fn>(Game::Read<const void *>(pane, 0),
Offsets::VTBL_SIMPLEMODEL_SET_MODEL_INSTANCE)(pane, model);
}
// PlayerModel's SetDisplay: stores the display record the pane builds from.
inline void SetDisplay(void *pane, const uint8_t *record) {
using Fn = void(__thiscall *)(void *, const uint8_t *);
reinterpret_cast<Fn>(Offsets::FUN_PLAYERMODEL_SET_DISPLAY)(pane, record);
}
inline bool IsVisible(const void *pane) {
return Game::Read<int>(pane, Offsets::OFF_FRAME_VISIBLE) != 0;
}
// The display record the pane shows (SetDisplay's), or null.
inline const uint8_t *DisplayRecordOf(const void *pane) {
return Game::Read<const uint8_t *>(pane, Offsets::OFF_PLAYERMODEL_DISPLAY_RECORD);
}
// A display record of our own for SetDisplay. The pane reads only its
// display-ID field, keeps the pointer and builds from it on every show, so
// the record lives as long as the pane.
struct DisplayRecord {
uint8_t bytes[Offsets::OFF_CREATURE_DISPLAYID + sizeof(uint32_t)] = {};
};
} // namespace Model::Pane
+18 -33
View File
@@ -54,6 +54,7 @@
#include "dbc/Lookup.h"
#include "model/Camera.h"
#include "model/DisplayInfo.h"
#include "model/M2.h"
#include "texture/Load.h"
#include "tick/FrameTick.h"
@@ -71,19 +72,11 @@ constexpr uint32_t kTexels = kSize * kSize;
using CreateScene_t = void *(__cdecl *)();
using ReleaseScene_t = void(__thiscall *)(void *scene);
using CreateModel_t = void *(__thiscall *)(void *scene, const char *path, uint32_t flags);
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 ReleaseModel_t = void(__fastcall *)(void *model);
using ModelState_t = int(__thiscall *)(void *model, int wait, int recurseChildren);
using SetFlag_t = void(__thiscall *)(void *model, int on);
using PlayAnimation_t = void(__thiscall *)(void *model, int, int, int, int, float, int, int);
using SetRenderCb_t = void(__thiscall *)(void *model, const void *cb, void *userData);
using DetachAll_t = void(__fastcall *)(void *model);
using AnimateScene_t = void(__thiscall *)(void *scene, const float *cameraPos);
using DrawScene_t = void(__thiscall *)(void *scene, uint32_t pass);
using GetVector_t = void(__fastcall *)(void *camera, uint32_t id, float *out);
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);
@@ -130,7 +123,7 @@ std::vector<Portrait *> g_loading;
void ReleaseModel(Portrait &p) {
Model::DisplayInfo::DestroyDresser(p.dresser);
reinterpret_cast<ReleaseModel_t>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(p.model);
M2::Release(p.model);
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(p.scene);
p.dresser = p.model = p.scene = nullptr;
p.dressRecord = nullptr;
@@ -166,8 +159,7 @@ bool Bake(Portrait &p) {
}
reinterpret_cast<DetachAll_t>(Offsets::FUN_DETACH_ALL_ITEM_MODELS)(copy);
void *camera = Model::Camera::CameraById(copy, 0);
reinterpret_cast<PlayAnimation_t>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(copy, -1, 0, 0, 0,
1.0f, 0, 1);
M2::PlayAnimation(copy, -1, 0, 0, 0, 1.0f, 0, 1);
float projection[16];
float view[16];
@@ -188,19 +180,12 @@ bool Bake(Portrait &p) {
toNdc(&x1, &y1, x1, y1);
// First collection: evaluate the portrait camera.
auto isLoaded = reinterpret_cast<ModelState_t>(Offsets::FUN_MODEL_INSTANCE_LOADED);
auto isReady = reinterpret_cast<ModelState_t>(Offsets::FUN_MODEL_INSTANCE_READY);
auto setAnimated = reinterpret_cast<SetFlag_t>(Offsets::FUN_M2MODEL_SET_ANIMATED);
auto animate = reinterpret_cast<AnimateScene_t>(Offsets::FUN_M2SCENE_ANIMATE);
auto getVector = reinterpret_cast<GetVector_t>(Offsets::FUN_CAMERA_GET_VECTOR);
isLoaded(copy, 1, 1);
setAnimated(copy, 1);
M2::IsLoaded(copy, 1, 1);
M2::SetAnimated(copy, true);
const float origin[3] = {0.0f, 0.0f, 0.0f};
animate(scene, origin);
float eye[3] = {};
float target[3] = {};
getVector(camera, Offsets::CAMERA_PROP_EYE, eye);
getVector(camera, Offsets::CAMERA_PROP_TARGET, target);
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);
@@ -209,12 +194,12 @@ bool Bake(Portrait &p) {
clear(3);
// Second collection from the portrait eye, then both passes.
isReady(copy, 1, 1);
setAnimated(copy, 1);
reinterpret_cast<SetFlag_t>(Offsets::FUN_M2MODEL_SET_VISIBLE)(copy, 1);
reinterpret_cast<SetRenderCb_t>(Offsets::FUN_MODEL_INSTANCE_SET_RENDER_CB)(
copy, reinterpret_cast<const void *>(Offsets::FUN_PORTRAIT_LIGHTING), nullptr);
animate(scene, eye);
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);
@@ -239,14 +224,14 @@ bool Bake(Portrait &p) {
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);
reinterpret_cast<ReleaseModel_t>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(copy);
M2::Release(copy);
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(scene);
return true;
}
// One frame for one pending display; true once it is baked (or cannot be).
bool Step(Portrait &p) {
if (!reinterpret_cast<ModelState_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(p.model, 0, 1))
if (!M2::IsLoaded(p.model, 0, 1))
return false;
if (p.dressRecord != nullptr) {
// A dress the engine rejects bakes undressed, as a unit with no
@@ -298,8 +283,8 @@ Portrait *Acquire(uint32_t displayID) {
void *scene = reinterpret_cast<CreateScene_t>(Offsets::FUN_M2_CREATE_SCENE)();
if (scene == nullptr)
return nullptr;
void *model = reinterpret_cast<CreateModel_t>(Offsets::FUN_M2SCENE_CREATE_MODEL)(
scene, Game::Read<const char *>(modelData, Offsets::OFF_CREATUREMODELDATA_MODEL_PATH), 0);
void *model = M2::CreateModel(
scene, Game::Read<const char *>(modelData, Offsets::OFF_CREATUREMODELDATA_MODEL_PATH));
if (model == nullptr) {
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(scene);
return nullptr;
+7 -9
View File
@@ -28,17 +28,15 @@
namespace Model::Rotation {
namespace {
constexpr int kTypeNone = -1; // lua_type of an absent argument
using LuaMethod_t = int(__fastcall *)(void *L);
int __fastcall Script_SetRotation(void *L) {
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
const bool animate = Game::Lua::Type(L, 3) == kTypeNone || Game::Lua::ToBoolean(L, 3) != 0;
const bool animate =
Game::Lua::Type(L, 3) == Game::Lua::TYPE_NONE || Game::Lua::ToBoolean(L, 3) != 0;
if (animate && !Model::Animation::HasRestingAnimation(pane))
return reinterpret_cast<LuaMethod_t>(Offsets::FUN_SCRIPT_PLAYERMODEL_SET_ROTATION)(L);
return reinterpret_cast<Game::Lua::CFunction>(
Offsets::FUN_SCRIPT_PLAYERMODEL_SET_ROTATION)(L);
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: SetRotation(rotation (in radians))");
return 0;
@@ -63,8 +61,8 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods, 1,
g_methodDocs, 1);
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods,
g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+5 -12
View File
@@ -33,20 +33,14 @@
namespace Model::Subject {
namespace {
using LuaMethod_t = int(__fastcall *)(void *L);
void *ResolvePane(void *L) {
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
}
void *PaneUnit(void *pane) {
return Object::ByGuid(Offsets::TYPEMASK_UNIT,
Game::Read<uint64_t>(pane, Offsets::OFF_PLAYERMODEL_UNIT_GUID));
}
int __fastcall Script_SetUnit(void *L) {
reinterpret_cast<LuaMethod_t>(Offsets::FUN_SCRIPT_PLAYERMODEL_SET_UNIT)(L);
void *pane = ResolvePane(L);
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_PLAYERMODEL_SET_UNIT)(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
Game::Lua::PushBool(L, PaneUnit(pane) != nullptr);
@@ -54,7 +48,7 @@ int __fastcall Script_SetUnit(void *L) {
}
int __fastcall Script_CanSetUnit(void *L) {
if (ResolvePane(L) == nullptr)
if (Game::Lua::ResolvePlayerModel(L) == nullptr)
return 0;
if (!Game::Lua::IsString(L, 2)) {
Game::Lua::Error(L, "Usage: CanSetUnit(unit)");
@@ -65,7 +59,7 @@ int __fastcall Script_CanSetUnit(void *L) {
}
int __fastcall Script_GetDisplayInfo(void *L) {
void *pane = ResolvePane(L);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
uint32_t displayID = 0;
@@ -112,8 +106,7 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+43
View File
@@ -0,0 +1,43 @@
// 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 "model/Teardown.h"
#include "Game.h"
#include "Offsets.h"
namespace Model::Teardown {
namespace {
AutoSubscribe *g_head = nullptr;
using Destroy_t = void(__fastcall *)(void *frame);
Destroy_t g_origDestroy = nullptr;
void __fastcall Destroy_h(void *frame) {
for (auto *node = g_head; node != nullptr; node = node->next)
node->cb(frame);
g_origDestroy(frame);
}
const Game::HookAutoRegister _destroyHook{Offsets::FUN_SIMPLEMODEL_DESTROY,
reinterpret_cast<void *>(&Destroy_h),
reinterpret_cast<void **>(&g_origDestroy)};
} // namespace
AutoSubscribe::AutoSubscribe(Callback cb) : cb(cb), next(g_head) {
g_head = this;
}
} // namespace Model::Teardown
+38
View File
@@ -0,0 +1,38 @@
// 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
// Model frame teardown. A Model frame's destructor (FUN_SIMPLEMODEL_DESTROY)
// releases its M2 scene, and the scene frees every model in it, whatever
// references are still out. Anything that holds an engine object tied to a
// frame's model (a compositor, an effect model) releases it here, the way the
// engine's own frames free theirs in their destructors. The UI teardown
// (CGGameUI::Shutdown, on /reload and logout) destroys every frame long
// before a ReloadAutoRegister cleanup runs, so a reload cleanup is too late
// for this.
//
// Declare a file-scope `const Model::Teardown::AutoSubscribe` in the module's
// TU; the callback runs with the frame and its models still intact.
namespace Model::Teardown {
using Callback = void (*)(void *frame);
struct AutoSubscribe {
explicit AutoSubscribe(Callback cb);
Callback cb;
AutoSubscribe *next;
};
} // namespace Model::Teardown
+55 -54
View File
@@ -42,12 +42,16 @@
//
// The effects are children of the pane's model, so they go when the model
// does: SetUnit, SetItem, a hide and show, and so on. A model destroyed with
// stopped effects still on it detaches them first (0x0070E3F5).
// stopped effects still on it detaches them first (0x0070E3F5). A stopped
// effect is in the pane's scene, which frees it with the frame, so the pane's
// teardown (Model::Teardown) drops our reference first.
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include "model/DisplayInfo.h"
#include "model/M2.h"
#include "model/Teardown.h"
#include "tick/FrameTick.h"
#include <cstdint>
@@ -56,19 +60,12 @@
namespace Model::VisualKit {
namespace {
using GetScene_t = void *(__thiscall *)(void *frame);
using CreateModel_t = void *(__thiscall *)(void *scene, const char *path, uint32_t flags);
using ModelFn_t = void(__fastcall *)(void *model);
using HasAttachment_t = int(__thiscall *)(void *model, uint32_t id);
using Attach_t = void(__thiscall *)(void *child, void *parent, uint32_t id);
using SetVisible_t = void(__thiscall *)(void *model, int visible);
using SetAlpha_t = void(__thiscall *)(void *model, float alpha);
using SetDiffuse_t = void(__thiscall *)(void *model, const float *rgb);
using HasLiveParticles_t = int(__thiscall *)(void *model);
using SetAnimDone_t = void(__thiscall *)(void *model, const void *callback, uint32_t userLow,
uint32_t userHigh);
using PlayAnimation_t = void(__thiscall *)(void *model, int keyBone, uint32_t id, int variation,
int offset, float speed, int blend, int primary);
// The attachment of each effect column, in the ghost kit's order.
constexpr uint32_t kAttachments[] = {20, 34, 19, 21, 22, 17, 23, 24, 25};
@@ -78,28 +75,25 @@ constexpr int32_t kProcAlpha = 14;
// The ghost kit's byte scale (0x008026C8).
constexpr float kByteScale = 1.0f / 255.0f;
// Stopped one-shot effects, each with one reference of ours.
std::vector<void *> g_stopped;
// A stopped one-shot effect, with one reference of ours, and the pane whose
// scene holds it.
struct Stopped {
void *pane;
void *model;
};
std::vector<Stopped> g_stopped;
void Release(void *model) {
reinterpret_cast<ModelFn_t>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(model);
}
void SetDoneCallback(void *model, const void *callback) {
reinterpret_cast<SetAnimDone_t>(Offsets::FUN_M2MODEL_SET_ANIM_DONE_CALLBACK)(model, callback,
0, 0);
}
// EffectDoneCallback's test, then CEffect::Stop's model steps.
// EffectDoneCallback's test, then CEffect::Stop's model steps. The callback's
// user data is the pane.
void __fastcall OnEffectDone(void *model, uint32_t /*keyBone*/, uint32_t /*animID*/,
uint32_t reason, uint32_t /*overshoot*/, uint32_t /*userLow*/,
uint32_t reason, uint32_t /*overshoot*/, uint32_t userLow,
uint32_t /*userHigh*/) {
if (reason != 0)
return;
SetDoneCallback(model, nullptr);
reinterpret_cast<SetVisible_t>(Offsets::FUN_M2MODEL_SET_VISIBLE)(model, 0);
reinterpret_cast<ModelFn_t>(Offsets::FUN_MODEL_INSTANCE_ADDREF)(model);
g_stopped.push_back(model);
M2::SetAnimDoneCallback(model, nullptr, 0, 0);
M2::SetVisible(model, false);
M2::AddRef(model);
g_stopped.push_back({reinterpret_cast<void *>(static_cast<uintptr_t>(userLow)), model});
}
// KitEffectLoopCallback: a sequence that ended cleanly plays again.
@@ -108,8 +102,7 @@ void __fastcall OnEffectLoop(void *model, uint32_t /*keyBone*/, uint32_t animID,
uint32_t /*userHigh*/) {
if (reason != 0)
return;
reinterpret_cast<PlayAnimation_t>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(model, -1, animID, -1,
0, 1.0f, 1, 1);
M2::PlayAnimation(model, -1, animID, -1, 0, 1.0f, 1, 1);
}
// Detaches a stopped effect that is still on its model, then drops our
@@ -117,27 +110,42 @@ void __fastcall OnEffectLoop(void *model, uint32_t /*keyBone*/, uint32_t animID,
void Discard(void *model) {
if (Game::Read<void *>(model, Offsets::OFF_M2MODEL_PARENT) != nullptr)
reinterpret_cast<ModelFn_t>(Offsets::FUN_M2MODEL_DETACH)(model);
Release(model);
M2::Release(model);
}
// The dying-effect drain: a stopped effect goes once its particles are gone,
// or at once when its model has already dropped it.
void Drain() {
// Discards every stopped effect `done` picks.
template <typename Pred>
void DiscardWhere(Pred done) {
for (size_t i = 0; i < g_stopped.size();) {
void *model = g_stopped[i];
if (Game::Read<void *>(model, Offsets::OFF_M2MODEL_PARENT) != nullptr &&
reinterpret_cast<HasLiveParticles_t>(Offsets::FUN_M2MODEL_HAS_LIVE_PARTICLES)(model)) {
if (!done(g_stopped[i])) {
++i;
continue;
}
Discard(model);
Discard(g_stopped[i].model);
g_stopped[i] = g_stopped.back();
g_stopped.pop_back();
}
}
// The dying-effect drain: a stopped effect goes once its particles are gone,
// or at once when its model has already dropped it.
void Drain() {
DiscardWhere([](const Stopped &e) {
return Game::Read<void *>(e.model, Offsets::OFF_M2MODEL_PARENT) == nullptr ||
!reinterpret_cast<HasLiveParticles_t>(Offsets::FUN_M2MODEL_HAS_LIVE_PARTICLES)(
e.model);
});
}
const Tick::FrameTick::AutoSubscribe g_drain{&Drain};
// The pane's scene is about to free its stopped effects.
void OnPaneDestroyed(void *pane) {
DiscardWhere([pane](const Stopped &e) { return e.pane == pane; });
}
const Model::Teardown::AutoSubscribe _teardown{&OnPaneDestroyed};
// The ghost kit's procedure step: kind 1 tints, kind 14 sets the alpha.
void ApplyProcedures(void *model, const uint8_t *kit) {
for (int i = 0; i < kProcedures; ++i) {
@@ -157,10 +165,10 @@ void ApplyProcedures(void *model, const uint8_t *kit) {
}
void Apply(void *pane, const uint8_t *kit, bool oneShot) {
void *model = Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
void *model = Pane::Model(pane);
if (model == nullptr)
return;
void *scene = reinterpret_cast<GetScene_t>(Offsets::FUN_SIMPLEMODEL_GET_SCENE)(pane);
void *scene = Pane::Scene(pane);
for (int i = 0; i < static_cast<int>(sizeof(kAttachments) / sizeof(kAttachments[0])); ++i) {
const uint8_t *effect = DBC::Record(
Offsets::VAR_SPELLVISUALEFFECTNAME_RECORDS, Offsets::VAR_SPELLVISUALEFFECTNAME_COUNT,
@@ -169,21 +177,24 @@ void Apply(void *pane, const uint8_t *kit, bool oneShot) {
!reinterpret_cast<HasAttachment_t>(Offsets::FUN_M2MODEL_HAS_ATTACHMENT)(
model, kAttachments[i]))
continue;
void *child = reinterpret_cast<CreateModel_t>(Offsets::FUN_M2SCENE_CREATE_MODEL)(
scene, Game::Read<const char *>(effect, Offsets::OFF_SPELLVISUALEFFECTNAME_FILE), 0);
void *child = M2::CreateModel(
scene, Game::Read<const char *>(effect, Offsets::OFF_SPELLVISUALEFFECTNAME_FILE));
if (child == nullptr)
continue;
SetDoneCallback(child, oneShot ? reinterpret_cast<const void *>(&OnEffectDone)
: reinterpret_cast<const void *>(&OnEffectLoop));
if (oneShot)
M2::SetAnimDoneCallback(child, reinterpret_cast<const void *>(&OnEffectDone),
static_cast<uint32_t>(reinterpret_cast<uintptr_t>(pane)), 0);
else
M2::SetAnimDoneCallback(child, reinterpret_cast<const void *>(&OnEffectLoop), 0, 0);
reinterpret_cast<Attach_t>(Offsets::FUN_M2MODEL_ATTACH)(child, model, kAttachments[i]);
Release(child);
M2::Release(child);
}
Model::DisplayInfo::LightChildren(pane, model);
ApplyProcedures(model, kit);
}
int __fastcall Script_ApplySpellVisualKit(void *L) {
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
void *pane = Game::Lua::ResolvePlayerModel(L);
if (pane == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
@@ -216,20 +227,10 @@ const Game::Doc::Method g_methodDocs[] = {
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
// Drop every stopped effect before the reload tears the panes down.
void PrepareForReload() {
for (void *model : g_stopped)
Discard(model);
g_stopped.clear();
}
const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
} // namespace
} // namespace Model::VisualKit

Some files were not shown because too many files have changed in this diff Show More