mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
master
1473
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 #135v1.20.6 |
||
|
|
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 |
||
|
|
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).v1.20.5 |
||
|
|
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. |
||
|
|
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> |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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#11v1.20.4 |
||
|
|
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.v1.20.3 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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>
|
||
|
|
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 |
||
|
|
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
v1.20.2
|
||
|
|
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.
|
||
|
|
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.v1.20.1 |
||
|
|
c0b403ba05 |
feat(model): add PlayerModel:ApplySpellVisualKit
PlayerModel:ApplySpellVisualKit(spellVisualKitID [, oneShot]) shows a SpellVisualKit's effects on the pane's model. In the world, 1.12.1 plays a kit on a unit (0x0060EDF0), through effect objects, spell durations and the unit's own animation, and a pane has none of these. The engine's own kit on a bare model is the char-select ghost (0x004727F0), and this is that routine for any kit on the pane's model: - Each of the nine effect columns (+0x0C) is created as a model from its SpellVisualEffectName file, in the pane's scene. It is attached at the column's attachment (20, 34, 19, 21, 22, 17, 23, 24, 25) when the model has that attachment (0x00712CB0, 0x00712F70), as in 0x00472780. The effects get the frame's light through LightChildren. - Procedure kind 1 sets the tint (an 0xRRGGBB colour stored as a float, scaled by 1/255) and kind 14 the alpha, clamped to 0..1. Both are plain stores (0x00710CF0, 0x00710CB0), as the ghost kit makes them. The kit's animation and sound do not play, which matches retail. An effect model's sequence plays one time. Without oneShot, each effect gets the world's loop callback (KitEffectLoopCallback, 0x0060ED00): a clean end (reason 0) plays the animation again. With oneShot, each effect gets EffectDoneCallback's test (0x005FBF50) and CEffect::Stop's model steps (0x006203E0): clear the callback and hide the model, so its emitters stop and the particles already out finish. A FrameTick drain, which runs after the strata walk, then mirrors DrainDyingEffects (0x0061F680). It detaches a stopped effect (0x00713020) once HasLiveParticles (0x007135C0) is false, or at once when its model has already dropped it. A model's destructor detaches its children first (0x0070E3F5), so the drain never reaches a freed parent. The effects are children of the pane's model, so SetUnit, SetItem or a hide and show clears them.v1.20.0 |
||
|
|
f6b98bc3ec |
fix(dbc): reject negative IDs in DBC::Record
DBC::Record checked the ID against the store's count as a signed int. A DBC column uses -1 for "none", and -1 passed that check, so the helper returned records[0xFFFFFFFF]. On 32-bit that wraps to records[-1], the heap word in front of the index array. A caller that passed an unchecked column read a garbage "record": an empty SpellVisualKit effect column gave the pointer 0xF and crashed. The engine rejects these IDs before it compares with the count: the char-select ghost kit's effect step tests the ID with a signed jl at 0x00472797, then a jg against the count. Record now compares unsigned, so it rejects 0, every negative ID, an ID above the count, and an unloaded store, whose count is -1. PvP::Arena had its own copy of the lookup with the same signed compare. It now calls DBC::Record. |
||
|
|
8f4f574174 |
feat(model): add PlayerModel:SetItem
PlayerModel:SetItem(itemID [, appearanceModID, itemVisualID]) shows one item's own model in the pane: a weapon, shield, helm, shoulder, quiver or ammo, with its skin texture and its glow. A 1.12.1 pane shows whatever its display record names (+0x3F0). SetDisplay (0x00505CB0) stores the record and builds it when the pane is visible, the show path (0x00505D49) builds it again, and the SetUnit helper (0x00505B50) clears it. SetItem gives the pane a record of its own and passes it to SetDisplay. The AttachDisplayModel (0x00505A70) co-hook in Model::DisplayInfo then builds the item instead of a creature. So hide, show, SetUnit, SetDisplayInfo and SetKeepModelOnHide handle an item with no new hooks. The model is built from 1.12.1's own steps: - the item folder by inventory type, from the engine's own Item\ObjectComponents\ path strings (0x00838CD8 on); - model name 0, else name 1, with the texture of the same index; - for a head item, the helm builder's (0x004799A0) path, with the player's race prefix (ChrRaces +0x18) and sex letter (0x00838AB4); - AttachItemModelFile's (0x004798C0) steps: create the model in the pane's scene, load the skin with the item-model status (0x00B424F0) and filter 3, bind it as texture type 2 (0x00710EC0), and attach the item visuals (0x00479700); - AttachDisplayModel's install: SetModelInstance (vtable +0x90), the pane's done callback (0x005062A0 through 0x00711BB0), Stand, and the release of the creation reference. The display's own ItemVisuals row wins over itemVisualID, the rule GetItemDescVisual (0x0062EC70) applies to enchants. The visual's child models get the frame's light and fog callback, now a shared LightChildren helper, so they are not drawn black. An uncached item is queued through Item::Data::WarmCache, and each visible pane that still shows it is built again when the item loads. |
||
|
|
43a989dd56 |
feat(model): add PlayerModel:FreezeAnimation
FreezeAnimation(animation, variation, frame) holds the model in one pose of an animation until the pane loads a new model, as SetAnimation does. frame is the time into the animation in milliseconds, the unit of M2 sequence times. The engine's sequence time is start + (ftol(elapsed * speed) + offset) mod length. So the pane's arm plays the animation through PlayAnimation (0x007121A0) at speed 0 and writes frame to bone 0's slot offset (+0xB8, bone states 0x118 bytes each at [model+0x90]). Speed 0 also puts the end stamp on the start stamp. Update (0x00719370) would then queue the done callback on the next frame, and the pane's done callback (0x005062A0) would arm Stand. The arm therefore sets the slot's done latch (+0xC0), the flag Update itself sets once it has reported done. A held animation never ends. SetAnimation and FreezeAnimation now also mark the pane for one re-arm. A call with the animation that already plays then still starts or ends a freeze, where the per-paint compare alone would skip it. |
||
|
|
0caddb8fe6 |
feat(model): add PlayerModel animation, keep-on-hide and unit methods
Adds SetAnimation, SetDoBlend/GetDoBlend, HasAnimation, SetKeepModelOnHide/GetKeepModelOnHide, CanSetUnit and GetDisplayInfo to PlayerModel (and so to DressUpModel and TabardModel). SetUnit now returns whether the unit exists, and SetCreature takes an optional display ID. Animation. A 1.12.1 pane rests on Stand: its per-paint UpdateModel (0x00505C50) arms Stand (PlayAnimation 0x007121A0 with id 0) whenever key bone -1 plays anything else and no turn is pending. A deadline of 0 counts as passed, so any other animation lasted one frame. SetAnimation gives the pane a resting animation and variation of its own. With one set, UpdateModel is hooked to arm that animation in Stand's place, using the pane's blend setting. Every other pane runs the stock function. The pane's done callback (0x005062A0) arms Stand when the animation ends, so the next paint starts it again and it loops. CSimpleModelFFX's SetModelInstance (0x0076CD30), which every pane model build and hide goes through, clears it, so a new model rests on Stand again. SetRotation turns without the shuffle while a resting animation other than Stand is set. HasAnimation is PlayAnimation's own test on the raw id: the lookup at shared data +0x24 must give an index below the sequence count at +0x1C. It is false until the model has loaded. Keep on hide. The pane's Hide (0x00505CE0) drops its model through slot 36, and its Show (0x00505D00) builds a new one from the unit or display record. With keep on, Hide runs only the base hide (0x0076AD50) and Show runs only the base show (0x0076AE10) while the pane still holds its model. The model then comes back with its try-ons and animation. The SetUnit helper (0x00505B50, behind SetUnit, RefreshUnit and Dress) and SetDisplay (0x00505CB0) only store the new subject while the pane is hidden, so they drop a kept model first. The next Show then builds the new one. Off is the default and matches the stock client. Units and displays. SetUnit runs the engine's method (0x00505D70, which Turtle patches) unchanged. It then reports whether the GUID it stored resolves to a unit (type mask 8), the same test the helper uses before it builds. CanSetUnit is that resolve without the set. GetDisplayInfo reads the display record's ID, or the unit's UNIT_FIELD_DISPLAYID (+0x1F4, field 0x7D). SetCreature with a display ID shows that display through the SetDisplayInfo record. |
||
|
|
8e603a7a0b |
fix(player): make IsLoggedIn false until PLAYER_LOGIN fires
IsLoggedIn returned whether the player GUID was set. That GUID falls
back to the char-select GUID before the world loads, and on a /reload
the live GUID is already set while the addons load. So IsLoggedIn was
true while every addon's files ran, and EventUtil.ContinueOnPlayerLogin
ran its callback at once instead of waiting for PLAYER_LOGIN. An addon
that hooked another addon this way hooked it before that addon had set
itself up.
1.12.1 fires PLAYER_LOGIN from one place: the enter cascade
(0x004908C0) calls FUN_FIRE_EVENT_NO_ARGS with 0x10E at 0x00490959,
once per interface initializer run (login and reload). Its arm byte
0x00B4E260 cannot answer the question directly: the initializer sets
it after the addons load (0x0049011D) and the cascade clears it after
the fire (0x0049095E). That call is now redirected to set a flag just
before the event goes out, and the FrameScript_Initialize cleanup,
which runs before every /reload and /logout, clears it.
IsLoggedIn is now false on glue and during addon load, and true from
the PLAYER_LOGIN handlers on, through world transfers.
UnitGUID("player") keeps its char-select fallback.
|
||
|
|
4726fd7c85 |
feat(model): add DressUpModel try-on hand, enchant glow, sheathing and auto-dress
TryOn now takes retail's (item [, handSlotName, spellEnchantID]) and returns an Enum.ItemTryOnReason. The pane gains IsGeoReady, SetSheathed / GetSheathed and SetAutoDress / GetAutoDress. Hand: TryOnItem (0x00504540) picks a lane at one site, 0x0050492D (mov edx,[edi+0x2C]; cmp edx,0x0D). A 6-byte call there jumps a named hand to the worker's own main-hand (0x0050495F) or off-hand (0x005049B1) path, so its CanHoldBoth (0x00504BC0) eviction, lane store and attach all still run; with no hand it replays the two instructions and the jnz reads their flags. The name is matched like GetInventorySlotInfo matches it: SStrCmpI over the PaperDollItemFrame rows, slot 16 or 17. Enchant glow: the player's AttachEnchantVisual step (0x005EED50) on the attachment AttachItemModel (0x0047A0C0) returns: FindChild (0x00712F00), then AttachItemVisuals (0x00479700) with SpellItemEnchantment +0x58, skipped when the display's own ItemVisuals row exists (GetItemDescVisual's rule, 0x0062EC70). The enchant is the argument, else the link's enchant field, which Item::Arg now parses. Sheathing re-runs the unit's AttachHandItem step on the pane's lanes with the sheathed flag. The lanes keep no display row, so the pane records one: the unit's vtable +0x9C row when SetModelInstance (slot 0x00808438) clones it, then whatever the worker's attach (0x00504AE6) installs; its two evictions (0x00504A0C, 0x00504A55) drop theirs. Each lane keeps its glow too, a player's read through GetVisibleItem + GetItemDescVisual, so a re-hung weapon keeps it. Auto-dress off runs ClearModel (0x00504490) after SetModelInstance unless Dress() is the one rebuilding. TryOn's result reads the gates the worker passes: an uncached record is DataPending, a held type installs, armor needs the component and the ready byte (+0x40C). DressUpItemLink now passes the whole link to DressUpItem, so a Control-click shows the link's enchant, and skips a cached item that IsDressableItem rejects. DressUpItem stays, since Blizzard_AuctionUI and Turtle both replace it. |
||
|
|
3e0c40184b |
feat(cursor): add expanded cursors for NPCs and the fishing bobber
Adds the expandedCursors cvar (default 0; AddOn Options > ClassicAPI > Interface > Cursor). At 1 the hover cursor shows what an NPC offers: an anvil for a repairer, a hearthstone for an innkeeper, a horseshoe for a stable master, and a quest giver's ! or ? (a blue ? for a repeatable reward). The player's fishing bobber shows a fish hook in place of the Interact gear. Each has a gray form beyond reach or when not usable. NPCs: ClassifyHoveredUnit (0x00482200) reaches its NPC flag chain at 0x00482336 for a unit the player can interact with, the unit in ESI and the beyond-reach flag in EBX. The chain's `mov ecx, [esi+0x110]` becomes a call to a thunk. At 0 it replays the instruction and the stock chain runs. At 1 it picks from the quest giver status first (CGUnit_C +0xCB8, read by the meaning of the client's status-to-marker table at 0x0080C454), then the NPC flags in service order: repair, innkeeper, flight master, trainer, spirit healer, banker, registrar/tabard/ battlemaster, auctioneer, stable master, vendor, then gossip or a quest giver IsQuestGiverToTalk (0x005DF490) accepts. It leaves through the chain's own epilogue (0x00482356). Bobber: the fishing node handler's cursor slot (vtable 0x0080BC80, slot 4; GetCursor 0x005F8760 is its one reader) points at a wrapper that runs BaseHandlerGetCursor (0x005F3070) and maps Interact/UnableInteract to the hook. The engine still decides whether the bobber is interactable and usable. Showing a mode past 41: SetCursorMode indexes a 42-slot image table, so its existing hook now sends the new modes to Cursor::Extended::Show. It calls SetCustomCursor (0x00523C40) with the mode's path, so a real file wins, and otherwise does what that function does after its load: copy the 32x32 image into slot 41 (0x00C0BD80), store mode 41, send it (SendCursorImage 0x00523790) and keep the name. The hook also records the new modes while Hover::Mode computes, so the soft-interact pick, its icon and SetUnitCursorTexture follow; a quest cursor has no icon image. Images: the 14 BLPs ship inside the DLL (src/embed/Textures, a new TEXTURES_ROOT pack). Texture::Builtin decodes each once with the engine's BLP reader from memory: BlpParse (0x005A83B0) is BlpOpen without the file read, and BlpReset never frees the file buffer. The reader moves to Texture::BlpReader, shared with the map overlays. Texture loads of the same paths, such as the option label's inline icons, get the same images. The reader refuses a DXT5 file. BlpDecodeLevel (0x005A7F00) converts a DXT level through the matrix InitConvertMatrix (0x005A4FC0) fills, and that matrix has DXT1 and DXT3 converters to BGRA8888 but none for DXT5: BlpDecodeLevel would return 1 with its SMemAlloc buffer unwritten. No shipped cursor is DXT5; the check keeps a later embedded DXT5 file, or a DXT5 map overlay, from being read as heap memory. Game paths now compare through one rule, Common::SamePath (any case, either slash), and the embedded manifests through Embed::FindFile. The embedded addons, the bindings splice and sound muting each had their own copy of the rule; they and Texture::Builtin now share it. The option's header, label and tooltip are translated for every client locale, in Blizzard's own terms for each (NSC, PNJ, 物体, 物件, ...). Release Win32 build is clean. In game, only the soft-interact quest icon was checked so far. |
||
|
|
b3e8d92b03 |
feat(localization): translate the addon options panel into zhCN (#126)
* feat(localization): translate the addon options panel into zhCN A Chinese client kept the English strings for ClassicAPI's own options: the AddOn Options window title and its game-menu button, every page name, header, control label and tooltip, and the AoE Loot binding name, which zhCN only had as a commented-out line. The generic strings enUS gained with those pages were missing from zhCN too: FOCUS_TOKEN_NOT_FOUND, LUA_ERROR, RELOADUI, PLAYER_LIST_DELIMITER, SYMBOLIC_NEGATIVE_NUMBER, SYMBOLIC_POSITIVE_NUMBER and the money texture formats. The locale files load enUS first and the file that matches GetLocale() overrides it, so English clients are unchanged and only zhCN is filled in. Terminology follows the client's own: 插件, 界面, 姓名板, 动作条 and 动作瞄准 for action targeting. The Release Win32 build embeds the new zhCN.lua in the !!!ClassicAPI pack (the packed strings were checked). Not tested in-game. * fix(localization): use Blizzard's zhCN terms in the options panel The backpack options said 背包, which zhCN uses for bags in general, so "ignore backpack" read as "ignore bags" and its tooltip contradicted itself. Use 行囊 for bag 0, as BACKPACK_TOOLTIP and BINDING_NAME_TOGGLEBACKPACK do. Junk is 垃圾 (BAG_FILTER_JUNK), and helpful spells are 有益法术, as in the self-cast option tooltips. Drop the coin-texture and signed-number format strings: they were byte-for-byte copies of enUS.lua, which always loads first. --------- Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com> |
||
|
|
0a3f8324ea |
fix(frame): stop a re-anchor during layout from freeing the point being read
Crash #132 at 0x00768323 (FUN_REGION_GET_RECT reading [5+0x3C]): Show lays a frame out before its children leave their layout deferral, so its OnSizeChanged ran; the ScrollBox handler read a region's size, and laying that region out reached an anchor point on a still-deferred child. The point getter (0x007A2F90 / 0x007A3070) ends the relative's deferral with FUN_REGION_LAYOUT_REALIZE(0), which ran the child's OnSizeChanged; that script re-anchored the region, freeing the point, and the getter then read the point again (0x007A2FC5) from freed heap. The getters now end the deferral themselves with the point marked busy. The point's deleting destructor (0x00767D80, the only free path) flags a busy point; a flagged getter returns the layout's undefined value without touching the point, and the point walk (0x007671A0 / 0x007671F0) is run again with the current anchors, up to ten times. Otherwise the engine's own getter reads the value, with the deferred bit hidden so a script that hid the relative again cannot start a second resolve. No hooks: the three getters/destructor are patched in the relative-point vtable (0x0081C44C, their only reference), and the six edge resolvers' calls to the walks are redirected. Adds Common::RedirectSlot for the vtable entries. |
||
|
|
6f3a8ffc1d |
feat(texture): add static creature portraits by display ID (#123)
* feat(texture): add static creature portraits by display ID * docs(api): document static creature portrait functions * Update API.md * refactor(model): bake display-ID portraits on the engine's own path SetPortraitTextureFromCreatureDisplayID now replays 1.12.1's unit portrait bake, BakePortrait (0x00524F60), call for call on a display model of its own, instead of driving a hidden PlayerModel through Lua. The display model is built the way AttachDisplayModel (0x00505A70) builds one, without a frame: M2CreateScene (0x00706E10), then CreateModelFromPath (0x00707350) on the CreatureModelData path, then ApplyCreatureDisplay (0x004797B0). A character display is dressed through the engine compositor with the same code that dresses PlayerModel:SetDisplayInfo, now shared as Model::DisplayInfo:: CreateDresser / PumpDresser / DestroyDresser. The bake's own gate, IsReady(model, 0, 1) (0x00710450), only passes for a model that something has drawn: a BLP texture gets its device texture only when GetTextureDeviceObject (0x0044ACF0) is asked with wait set. The shared M2 still loads on its own, so the gate here is IsLoaded(model, 0, 1) (0x007103D0) plus the dress, and the bake's IsReady(copy, 1, 1) makes the device textures, as it does for the copy of a unit's model. The bake itself is the engine's sequence: copy into a fresh scene, DetachAllItemModels, camera 0, PlayAnimation, save projection, view, viewport and fog (GxMasterEnable 0x0058A210 / 0x0058A1E0), the 64x64 DDC box in the bake's own float order, CameraSetDeviceTransforms, clear colour 0, ClearDevice(3), PortraitLighting, both scene passes, read back through 0x00589CF0, clear, cut the alpha mask in (0x005258A0), restore. It runs at the head of the UI render callback, the same point as an OnUpdate script that calls SetPortraitTexture, behind the bake's device gate (0x0058A260 window active, 0x0058A280 device alive). Each display ID owns one callback texture (TextureCreateWithCallback, which makes its device texture at creation). It is bound with SetImage (0x00770300) at call time and filled in place with GxTexMarkRectDirty (0x0058ACD0), the bake's own path for re-baking a cached portrait. A region the caller re-points before the bake finishes keeps what it was given, so no region is tracked: the request map, the region-destruction subscription and ClassicAPI_CancelPortraitTextureRequest are gone. The function returns nothing, as retail's does. Portraits are kept until logout, the lifetime of the engine's display-ID portrait cache (cleared in ShutdownGame). A display with no portrait camera is done once and stays transparent. Offsets.h: the portrait block is renamed to engine names with calling conventions and sources; the duplicates of FUN_M2SCENE_ANIMATE, FUN_M2MODEL_SET_ANIMATED and GetCameraById are removed, as are the made-up draw-flag and scene-count fields and the raw device-vtable slots. Model::Camera::CameraById is exposed instead of being copied. --------- Co-authored-by: z1xel <ramirezthiago@71gmail.com> Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com> |
||
|
|
3862408e1a |
docs(readme): list the new base-library functions
The Lua row gains debuglocals, debugstack's coroutine argument, difftime, gmatch, math.cosh/sinh/tanh, newproxy, scrub, string.byte and strbyte ranges, string.format's two-digit %n$, %0 in string.gsub and table.removemulti. The Lua errors section says the window shows each error's local variables. |
||
|
|
12a81f7f89 |
feat(baselib): debugstack takes a coroutine
debugstack([thread,] [start [, count1 [, count2]]]) returns the stack of a coroutine when one is passed first, with start defaulting to 0 (the coroutine's innermost level), as 4.3.4's Script_debugstack does. 1.12.1's debugstack (0x703760) has no thread argument. A thread fails its lua_isnumber and lua_isstring checks, so it returns the thread itself, and msg .. debugstack(co) raises "attempt to concatenate a thread value" in the coroutine error paths of WeakAuras, Questie's ThreadLib, MRT and Zygor. The engine function still builds the text, so its line format and its 12/10-level head and tail windows are unchanged, which keeps addons that parse debugstack() for their folder working. It reads levels through six calls on the state it runs on: lua_getstack (0x6FBAA0) at 0x70383F, 0x70386E, 0x703896, 0x7038AD and 0x703998, and lua_getinfo (0x6FBC70) at 0x7038CD. Those calls are redirected to read from the state the new global names for the call: the thread, or the calling state. The engine function has no callers of its own, so the global is its only way in and sets that state for every call; an error part way cannot leave a thread behind for the next call. The global removes the thread argument, inserts 0 as start when none is given, and calls the engine function directly in its own frame, so levels without a thread count exactly as before. |
||
|
|
05fa5845b7 |
feat(baselib): debuglocals, and the error window shows locals
debuglocals([level [, skipFunctionsAndUserdata]]) returns the locals and
then the upvalues of a call level as text, one name=value line each.
It runs only while an error handler runs, as in 4.3.4. 1.12.1's error
function sets no flag for that, but while a handler runs that function
(FUN_FRAMESCRIPT_CALL_ERROR_HANDLER) is a C closure level on the stack;
ErrorCallstack's search for it moves into a header so both modules use
it. level counts as lua_getstack and debugstack do.
Each local comes from lua_getlocal (0x6FBB20, fastcall L/ar + n), then
each upvalue of the level's function (lua_getinfo "f") from
lua_getupvalue (0x6F4660, fastcall L/funcindex + n). Values are
formatted as 4.3.4's FrameScript_PackVariable does, with retail's
spacing (probed: n=7, s="hi", t=<table>{ / k=1 / }): a frame table shows
its name, read from t[0] as FrameScript_GetObject does; a table's fields
show one space in at the top level only; a function shows where it is
defined (lua_getinfo ">nS"). The text stops at 4095 bytes. The second
argument, which retail's error handler passes as true, leaves out
function and userdata values.
Where 4.3.4 would fail inside the handler, this does not: a number key
outside int range is formatted from a copy, not converted in place
(which breaks lua_next), and a key that is not a number or string prints
as its type.
Retail also lists (*temporary) lines, 5.1's names for a level's stack
slots past its locals, including a C level's arguments. 1.12.1's
lua_getlocal is 5.0's and names declared locals only; those slots are
compiler registers that 5.0 and 5.1 allocate differently, so they are
not imitated.
The error window takes the locals as retail's GetErrorData does,
debuglocals(level, true) at the stack's level, and shows them under
Locals ("<none>" when there are none).
LuaCAPI.md had lua_pushupvalues, lua_getupvalue and aux_upvalue one row
off; corrected.
|
||
|
|
6ed0edc947 |
feat(baselib): gmatch, table.removemulti, math hyperbolics, difftime, newproxy, scrub
The base-library names that 1.12.1's Lua 5.0 and its compat.lua do not have: - gmatch: the global alias of string.gmatch (5.0's str_gfind, 0x7FCFA0), set by value like strmatch and strrev. - table.removemulti(t [, pos [, count]]): removes count elements from pos, shifts the rest down with lua_rawgeti/lua_rawseti and returns the removed elements. 5.0 keeps the length apart from the keys, so like the engine's table.remove (0x7FB770) it records the new length with luaL_setn (0x6F4EA0). - math.cosh, math.sinh, math.tanh: luaL_checknumber (0x6F4C80) and the C function. - difftime(t2 [, t1]): C difftime over the two times, next to the engine's date and time globals. - newproxy([false | true | proxy]): an empty userdata from lua_newuserdata. The closure's upvalue is a weak table (its own metatable, __mode "kv") that marks the metatables newproxy(true) makes, so only those can be shared. - scrub(...): replaces each argument that is not a boolean, number or string with nil, in place with lua_replace (0x6F32B0). All are registered on both Lua states. Game::Lua gains RawSetI (lua_rawseti, 0x6F3F60), Replace, CheckLString (luaL_checklstring, 0x6F4BE0), CheckNumber and OptNumber (luaL_optnumber, 0x6F4CD0); string.byte now uses them instead of local typedefs. issecure, issecurevariable and forceinsecure are left out. 1.12.1 has no taint, so they could only return constants, and a constant "secure" from issecurevariable makes AceHook-3.0 refuse every plain hook.v1.19.3 |
||
|
|
1645e6b43c |
feat(baselib): string.byte returns a range of byte codes
1.12.1's str_byte (0x7FC210) is Lua 5.0's: it reads only the start position and pushes one code, so string.byte(s, 1, -1) silently returns the first byte alone. string.byte and the strbyte global now take (s [, i [, j]]) and push the codes of bytes i..j, with j defaulting to i. Negative positions count from the end and the range is clamped to the string. The arguments are read with the engine's luaL_checklstring (0x6F4BE0) and luaL_optnumber (0x6F4CD0), and positions truncate toward zero as the engine's str_byte and str_sub do, so a call without j returns exactly what it did before. Each code takes a stack slot, reserved with lua_checkstack (0x6F2F30). The engine refuses more than 2048 slots, so a range longer than about 2000 bytes raises "stack overflow (string slice too long)". compat.lua set strbyte from the old string.byte when the state opened, so strbyte is re-aliased to the new table entry. |
||
|
|
efcdc54a50 |
feat(baselib): string.gsub takes 5.1 string and table replacements
1.12.1's str_gsub (0x7FD0E0) is Lua 5.0's. Its replacement argument
differs from 5.1 in four ways: a table is rejected ("string or function
expected"); in a replacement string, %0, and %1 when the pattern has no
captures, raise "invalid capture index" (add_s checks every %n with
check_capture, 0x7FCE20); a function that returns nil or false deletes
the match instead of keeping it; and one that returns a table deletes
it too instead of raising an error.
The first two only ever raised errors, so the 5.1 behavior cannot break
an existing addon. The function rules can: SuperAPI's
CombatText_AddMessage returns nil to delete the player's own " [0x...]"
GUID from heal text. So function replacements keep 1.12.1's behavior,
and only string and table replacements change. A scan of every gsub
call in the installed addons and the stock FrameXML found no other code
that depends on the old string or function results.
str_gsub's match loop, anchor, count and buffer stay the engine's. Two
parts are replaced:
- The 60-byte replacement-type gate at 0x7FD15E (lua_gettop / isstring /
lua_type == function, then luaL_argerror) becomes a call to a
__fastcall check with ECX = L that accepts a number, string, function
or table and raises "string/function/table expected" otherwise.
- The one call to add_s (0x7FD203 -> 0x7FD2A0, __fastcall ms/b + s/e on
the stack, ret 8) goes to a function with the same signature. A
function replacement calls the engine's add_s unchanged. A string or
number gets 5.1's add_s, and a table gets 5.1's lookup: the first
capture or whole match as the key, a nil or false value keeps the
match, and any other non-string value raises "invalid replacement
value". Both use the engine's push_onecapture (0x7FCF40, plus 5.1's
whole-match case for index 0 with no captures), luaL_addlstring
(0x6F51F0) and luaL_addvalue (0x6F5270).
Both patches check the original bytes or call target and are skipped
when they differ.
The table-replacement shim in StringLib is removed. It wrapped the
pattern in an outer capture so a closure could return the whole match,
and deleted the match for a missing key when the pattern had a
back-reference. string.gsub and gsub are the engine's function again.
|
||
|
|
6a24271cd1 |
feat(baselib): two-digit %n$ argument numbers in string.format
1.12.1's str_format (0x7FD3D0) only reads a one-digit argument number: isdigit(p[0]) && p[1] == '$'. In "%10$s" the "10" is read as a width and the '$' as the conversion, which raises "invalid option in `format'". Combat log strings such as ACTION_SPELL_PERIODIC_LEECH_RESULT, "(%10$s %8$s Gained)", cannot be formatted. Replace the engine's 18-byte one-digit branch at 0x7FD4AC with a call to a naked thunk that sets the argument number ([ebp-8]) and the flags scan start (EBX) for both "n$" and "nn$". EAX/ECX/EDX are dead at the site. The engine still tests the first digit with its own isdigit, increments the argument after the site, and does every conversion, so string.format, format and the C callers of str_format all get the change. The patch checks the original bytes and is skipped when they differ. |
||
|
|
a7ce7555f0 |
fix(softinteract): anyinteract yields when the target is out of reach (#121)
* fix(softinteract): anyinteract yields when the target is out of reach
GetAny treated Reach::Out — the cursor's Unable form — as a usable
target, so with a target too far away the Interact Key pressed the inert
target instead of the nearest object it could interact with.
Prefer the target only while the cursor says it can be used now
(Reach::Now), and fall back to softinteract otherwise. A target behind
the camera still wins while it is in reach: the cursor verdict does not
look at facing.
* docs(api): anyinteract needs the target to be in reach now
The token section said a target that is too far away still counts, which
the implementation no longer does.
* fix(softinteract): anyinteract keeps an out-of-reach target when nothing is picked
Retail 12.1.5, targeting an NPC that is out of reach: facing away from
everything (softinteract nil), anyinteract is the target; facing a nearer
NPC, it is that NPC. The previous change returned 0 in the first case,
so the Interact Key lost the engine's own out-of-reach handling on the
target (the "need to be closer" error, or the click-to-move walk).
GetAny now reads the target's cursor verdict once: in reach wins, then
softinteract, then the target while it is interactable but out of reach.
* feat(interact): honor looseTargeting in C_PlayerInteractionManager.InteractUnit
On retail the Interact Key on a nearer NPC opens its dialog while a
farther unit stays targeted; with no target it targets the NPC. The key
calls InteractUnit("anyinteract") with looseTargeting at its default,
true, which ClassicAPI ignored, so the right-click's select replaced the
target.
FUN_ON_SPRITE_RIGHT_CLICK (0x00492820) is the active-player gate, the
select (FUN_SET_SELECTION_BY_GUID, 0x00493540), then the object's
right-click virtual (vtable +0x60, call at 0x00492881) with
FUN_GAMEUI_IS_KEY_DOWN(0). The select stores only units, so only a unit
other than the current target would move it. With looseTargeting and a
target, that one case runs the gate and the virtual without the select;
every other case still calls the engine function, which nampower also
detours for its right-click guards. The global InteractUnit keeps
targeting.
---------
Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
|
||
|
|
04790cb6c1 |
feat(options): name the page scroll bars for skins
Each options page's MinimalScrollBar is now ClassicAPIOptionsScrollBarN, numbered like the ClassicAPIOptionsControlN controls, so a skin can find the scroll bars the same way it finds the controls. |
||
|
|
e7ecae56da |
feat(options): key binding rows for ClassicAPI's bindings
The Interact Key page gets rows for Interact With Target and Interact With Mouseover, and Bags and Items gets AoE Loot under a Loot header. A row is retail's settings-page binding row: the binding's name, then a button for each of its two keys. Left click a button and press a key, mouse button 3-5 or the wheel to bind it; Escape cancels; right click unbinds that key. The tooltip on a bound key and the page's output line use retail's strings, added to every locale with Blizzard's translations. The binding is 1.12's Key Bindings window's (Blizzard_BindingUI): the same ALT-CTRL-SHIFT chord order, the same skipped modifier keys, the same KEY_UNBOUND_ERROR check for an action that loses its last key, and the same put-back when SetBinding refuses a key. A change saves at once with SaveBindings(GetCurrentBindingSet()), so it lands in the account or character set in use, as every other control on these pages writes at once. While a button listens, a full-screen button on FULLSCREEN_DIALOG takes keys, clicks and the wheel, as retail's KeybindListener and its input blocker do. A left or right click on it cancels, and so does hiding the page. |
||
|
|
65c6b0b271 |
feat(action): spell search, slot item and #showtooltip action queries
C_ActionBar.FindSpellActionButtons, HasSpellActionButtons and IsOnBarOrSpecialBar find a spell on the bars. A slot matches on the spell ID stored in VAR_ACTION_TABLE under the spell tag, the same test 4.3.4's PutActionInSlot makes, so a macro or item that casts the spell does not count and a rank matches only itself. A name resolves through the engine's spellbook resolver. IsOnBarOrSpecialBar also reads the shapeshift bar's spell array (pointer at 0x00B71108, count at 0x00B71104, as Script_GetShapeshiftFormInfo 0x004B45C0 reads them) and the pet bar's spell entries (types 1-5, spell ID in the low 16 bits, as Script_GetPetActionInfo 0x004BDC50 reads them, gated on the pet GUID). Enum.ActionBarSet is registered; all 120 slots are its Mkb set. IsStackableAction (global, 1/nil) and C_ActionBar.IsStackableAction (bool) answer 4.3.4's IsStackableAucItem: the slot's item has a max stack above 1. C_ActionBar.GetItemActionOnEquipSpellID returns the item's first spell with trigger 1 (on equip). The item is the item-by-ID action's, or the one a macro's #showtooltip / #show resolved to, as 4.3.4's CGActionBar::GetItem returns it. Both peek the item cache and never query it. C_ActionBar.IsMacroActionWithShowTooltip parses the macro's body with the parse Macro::ShowTooltip hands the tooltip over on, so it answers while that module yields or a publisher owns the macro. |
||
|
|
5f94fb98bd |
feat(action): C_ActionBar.GetSpell
C_ActionBar.GetSpell(actionID) returns the spell an action-bar slot casts, or 0. It is the engine's own slot resolver (0x004E5A50), which the cooldown, usable and range readers already go through: a spell action is its spell, a macro is its cached primary spell (+0x564, the one #showtooltip sets), and an item-by-ID action is the first on-use spell in its item cache record. An empty slot, a slot outside 1..120, an equipment set or a macro whose macro is gone gives 0. The spellbook flag the resolver also hands back is not part of the answer. |
||
|
|
426f6b6ec9 |
fix(container): free slots count the bank and use the engine's counter
GetContainerNumFreeSlots and GetContainerFreeSlots now take the main bank (-1) and the bank bags (5..10) while a banker is open. The main bank is gated on the banker GUID (0x00BDD038); bank bags get the gate from the engine's container-GUID getter (0x004F93E0), which answers 0 for them while the bank is closed. A bag's empty slots are counted by the engine's own CountFreeSlots (0x00622910, __thiscall on the bag's inventory list), and its slot count comes from that live list instead of the item record. The backpack and main bank are ranges of the player's list, walked slot by slot. Nothing walks PackBagSlot per slot any more, so the Lua stack is left alone. A container that is not there (keyring, empty bag slot, closed bank) now returns only 0, leaving bagFamily nil. The bag view SortBags kept for itself moves to Container::Bag so both modules share it, and Item::Location::EquippedBagInventory becomes BagInventory (1..10) beside a new BagContainer. C_Item.GetItemData's bagFamily now goes through BitmaskForRecord, so a bag whose family field is empty reports the same family there as it does from GetItemFamily (a Soul Pouch was 0 there, 0x4 here). |
||
|
|
3a4d291748 |
fix(link): split links with string.split captured at load
LinkUtil.SplitLinkData and SplitLinkOptions looked up string.split on every call. BigWigs (pepopo978 fork) defines a table-returning string:split in Plugins/SpellRequests.lua, which replaces the global, so with it loaded every custom link type resolved to a table, no handler matched, and SetItemRef rejected the link as unknown. string.split is registered in C by the DLL before any addon loads, so a file-local captured when LinkUtil.lua loads always holds the real one. Fixes brues-code/pfUI#73v1.19.2 |
||
|
|
1cdd46958f |
feat(combatlog): 4.3.4 combat log lines in retail-named log files
While combat logging is on, each combat log entry is also written as one CSV line in 4.3.4's PackEvent layout: the subevent, source and dest (GUID, quoted name or nil, flags as hex), the spell prefix, then the suffix blocks the entry carries. Fields are written as 1 or nil, damage values as numbers, and there is no hideCaster, isOffHand or alternate power type. The line is written from Queue before the events fire, so it is written even when no frame listens. The switch is LoggingCombat's own flag (0x00B4FDB8), so /combatlog and LoggingCombat() control both files. Each time the flag turns on, a new file starts, named as retail names it: Logs\WoWCombatLog-MMDDYY_HHMMSS.txt, the local time logging started. The file is a Storm log opened as LoggingCombat opens its own: SLogCreate (0x0065A0C0) with flag 4 (append). Lines go through SLogWrite (0x0065ACE0), which puts the "M/D HH:MM:SS.mmm " stamp in front. The flag is read once a frame on FrameTick and before each line. When it turns off, SLogClose (0x00659EC0) writes the buffered lines and closes the file. This also covers logout: the chat teardown in CGGameUI::ShutdownGame (0x00498700, its only caller) clears the flag, and FrameTick runs on the glue screen too. Storm closes what is still open at exit (0x0065A4B0). LoggingCombat itself is not hooked: it is a jmp into the enable code the chat log shares, and other DLLs add their own logs to it. 1.12's Logs\WoWCombatLog.txt keeps its chat-text lines, written by 0x0049B1E8, for the log readers that parse them.v1.19.1 |
||
|
|
72f0e5d98f | docs(action): range and usable events move to the Events section | ||
|
|
ceb6aaf46b |
feat(action): action range and usable events
ACTION_RANGE_CHECK_UPDATE(slot, isInRange, checksRange) and
C_ActionBar.EnableActionRangeCheck(slot, enable). Enabled slots are
checked on each world tick while a frame is registered for the event,
with IsActionInRange's own answer (Action::Range::Answer, the macro
unit redirect included), and fire on change. Enabling is counted per
slot, and each enable makes the slot's next check fire, because retail
buttons take their first state from the event. A new Lua state starts
with no slot enabled.
ACTION_USABLE_CHANGED(changes) carries { slot, usable, noMana } for
each slot that changed. The engine's bulk refresh 0x004E5C00 already
compares each slot's pair against the caches IsUsableAction reads
(0x00BC6B60 / 0x00BC67A0). Its per-slot call at 0x004E5C15 is
redirected to record the slots whose pair differs, and its
ACTIONBAR_UPDATE_USABLE signal at 0x004E5C53 to send that event
unchanged and then the new one. No hook.
|
||
|
|
84c936faa4 |
fix(compat): hooks wrap the engine's body when wow_turbo got there first
wow_turbo detours about 380 engine functions to its own reimplementations from its DllMain. It yields any function whose prologue another mod has already patched, but ClassicAPI installs from the Load export, which VanillaFixes calls after every DllMain. So wow_turbo always patches first, and MinHook copies its jmp into our trampoline: the hook then calls the reimplementation, not the engine's body. Everything that lives in that body was lost. The script runners (0x00704D50, 0x00704F10) never reached the pcall redirects at 0x00704E79 / 0x00705199, so every handler saw self == nil. The text builder (0x005CDC20) calls its own emitter, so the inline texture emitter hook (0x005CCBE0) and the word-wrap redirects at 0x005CDDD3 / 0x005CDE23 never ran. Before each HookAutoRegister hook is created, a prologue that is a jmp rel32 into wow_turbo.dll gets its five stock bytes back. The bytes come from WoW.exe on disk, found through the loaded image's section table. Then the hook wraps the engine's body. wow_turbo's own rule produces the same state when it loads after a mod that hooked first. It keeps its original in place, logs the overwrite and does not re-patch these entries. Release builds reclaim six: the two runners, the text builder and emitter, GxSetViewport (0x0058AF60) and the selection-circle update (0x00614A90). Nothing changes when wow_turbo is not loaded. |
||
|
|
957d66af49 |
fix(addons): memory usage walks the live objects, as luaC_sizeall does
UpdateAddOnMemoryUsage summed Gc::Tag::SizeOf over the keys of the tag map. That is only safe while every free reaches luaC_freeobj (0x006F7260), whose co-hook erases the key. wow_turbo replaces luaC_collectgarbage (0x006F7340) with a parallel sweep that frees strings, tables, closures, userdata and upvalues itself and sends only threads and protos through freeobj. Dead keys stayed in the map, and the recount read freed blocks: crash 132. The recount now walks the lists the engine sweeps: rootgc (G+0x10), rootudata (G+0x14) and every string-table bucket (G+0x04, size G+0x0C), and looks up the tag of each live object. 3.3.5's luaC_sizeall walks the same lists. A create with no context word now erases its address's entry, so a reused address never carries a dead object's tag, whoever freed it. Dead entries a foreign sweep leaves are never read, and the pool's address reuse bounds them. _classicapi_LuaHeapAudit uses the same walk. Fixes #120 |
||
|
|
3849b7513b |
feat(statusbar): the standard fill moves the bar texture's rect
1.12's CSimpleStatusBar::OnFrameUpdate (0x007833C0) draws the fill by cropping. InstallBarTexture (0x007831F0) sets the texture's SetTexCoordModifiesRect flag (+0x124), and the update writes the filled part into the coordinates. The crop shrinks the drawn quad but leaves the texture's rect at the full bar, so a region anchored to GetStatusBarTexture() stays on the bar's far edge. Reverse, center, rotated and tiled bars already moved the texture's points. The OnFrameUpdate hook now replaces the engine's update for every bar. It runs the base frame update (0x0076B2C0), clears the crop flag, and moves the texture's four points in from the bar's edges with CLayoutFrame SetPoint and doResize, as 4.3.4's OnLayerUpdate does. The fraction comes from the bar's fill-fraction virtual (+0x98), so SetValue, the ease and SetTimerDuration all move the rect. doResize queues the texture for the layout pass before the draw, so anchored regions follow in the same frame. An empty fill keeps a zero-width rect on the start edge (CalculateRect 0x767A20 has no size test), so its anchors still resolve. The crop path and the code that handed a bar back to it are removed. A standard vertical bar now shows the bottom part of its image. |