Compare commits

...
Author SHA1 Message Date
Brues d021ae3e3d docs(faction): describe the reputation functions
Descriptors for GetFactionIDByIndex, GetFactionInfoByID,
GetFactionParentID, the 17 undocumented C_Reputation functions, the
FactionData structure, and the FACTION_STANDING_CHANGED event.
C_Reputation.IsFactionInactive and SetFactionInactive mirror stock
globals, but no Blizzard client has them under C_Reputation, so they
are marked as ClassicAPI extensions.
2026-10-30 02:34:17 -05:00
Brues f4862dc817 docs(equipmentset): describe the C_EquipmentSet functions
Descriptors for the 18 undocumented C_EquipmentSet functions,
GameTooltip:SetEquipmentSet, and the EQUIPMENT_SETS_CHANGED,
EQUIPMENT_SWAP_PENDING and EQUIPMENT_SWAP_FINISHED events. They
describe what the functions return here, which differs from retail in
places: an icon path, not a file ID; numMissing, not numLost;
GetIgnoredSlots lists slot numbers.
2026-10-30 02:34:17 -05:00
Brues 3a6f53f919 docs(aura): describe the C_UnitAuras functions
Descriptors for the 16 undocumented C_UnitAuras functions, plus the
AuraData structure that GetAuraDataByAuraInstanceID already named but
nothing defined. The four positional getters share the Classic Era
UnitAura return list.
2026-10-30 02:34:17 -05:00
Brues 19b299d3df docs(container): describe the C_Container functions
Descriptors for the 17 undocumented C_Container functions and the
ItemQuestInfo structure. SwapItems, MoveItem, AutoStoreItem,
GetContainerItemCharges, GetContainerItemRepairCost and
IsContainerItemOpenable are not in retail's API and are marked as
ClassicAPI extensions.
2026-10-30 02:34:17 -05:00
Brues 77b304ae68 feat(smf): RemoveMessagesByPredicate
ScrollingMessageFrame:RemoveMessagesByPredicate(predicate), retail's: the
predicate is called with each message's GetMessageInfo values, oldest
first, and the messages it returns true for are removed; the rest keep
their order. It covers 4.3.4's RemoveMessagesByAccessID / ByExtraData,
whose int fields the retail-shaped records here do not have.

1.12's ring has no compaction of its own; the removal is 4.3.4's
RemoveLines loop over it: every kept record moves forward in ring order to
the first free slot (freeing the text it lands on, nulling the one it
left), the position moves back by the removed count, and the
extra-argument tables move with their records. Then the engine's own
layout (0x00788750) from the record at the kept scroll offset, clamped to
the oldest, and its re-arm (0x00788B80) - a layout at the bottom keeps the
fade state, so 4.3.4 re-arms after - or its Clear (0x007882B0) when
nothing is kept.

The predicate runs for every message before anything moves, through the
engine's error handler; a predicate that errors keeps that message, as
retail's safesecurecall does. GetMessageInfo shares its record lookup and
value push with it.
2026-10-30 02:14:08 -05:00
Brues c965e49117 feat(smf): BackFillMessage
ScrollingMessageFrame:BackFillMessage(text, r, g, b, ...), retail's: the
message becomes the oldest, for rebuilding a frame from history newest to
oldest (Blizzard_CombatLog's refilter adds each entry with it).

The ring's writer 0x00788150 has one slot rule: the one after the newest.
BackFillMessage writes the slot 4.3.4's AddMessage(..., addToTop) picks —
the free one before the oldest, (pos - count + 1) mod max once the count
has grown — and fills the record as the writer does: a Storm copy of the
text, AddMessage's colour (clamped, x255 + 0.5 truncated, alpha 0xFF; the
font's without r, g, b), shown, the frame's two times, id 0, and the extra
arguments kept for GetMessageInfo. Nothing already in the ring moves, so a
scrolled-up frame keeps its view; at the bottom the lines are laid out
again from the newest, as the writer does.

A frame at its line limit drops the message, as retail's CircularBuffer
PushBack does ("Won't overwrite front"); 4.3.4 overwrites the oldest slot
there, which Blizzard_CombatLog never reaches.
2026-10-30 01:45:47 -05:00
Brues ead52e79ce fix(editbox): caret, selection and click rows follow tall inline icons
A multi-line edit box places its caret, selection pieces and a click's
cursor by rows of one height (font height + spacing, pixel-aligned), while
the render moves a line holding an icon taller than the font, and every
line after it, down by the overflow, and the box grows to match. Below the
first tall-icon line the caret and highlight sat above their text, and
OnCursorChanged reported that y, so ScrollingEditBoxMixin's scroll-into-
view stopped short (pfUI chat copy with faction emblems).

4.3.4's CSimpleEditBox has the same fault: UpdateVisibleCursor,
UpdateHighlightArea and ConvertCoordinateToIndex are icon-blind too. So
this goes past 4.3.4, as editbox/CharClass does. EditBox::IconRows co-hooks
the three placers and gives each the render's move, from the new
Text::InlineTexture::LineIconShift over the box's own line table: the
click's y is mapped onto the uniform row before the engine reads it; the
selection piece and caret are re-anchored after, with the engine's own x;
the caret's OnCursorChanged is muted for the original call and fired once
with the corrected y through the engine's forwarder, with the same packing
PlaceCaret uses (verified in the disassembly and the binary's constants).

Also fixes the "caret positioner" misnomer for 0x77DE70 (the selection-
piece placer) in Offsets.h and the docs.
2026-10-30 01:15:01 -05:00
Brues 7cb6cda101 feat(smf): TOP insert mode
SetInsertMode(SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP) now shows the
newest message at the top, older ones below it, as retail's mixin does.

1.12's line layout anchors each line once, when it makes it, through one
SetPoint call (0x00788975). That call is redirected: lines of a TOP frame
take the TOP anchors, every other line keeps the engine's own arguments.
The layout remakes the lines when the frame resizes, so the redirect
covers that too. A mode change re-anchors the existing lines with the
layout's own steps and refreshes the display.

Insert mode moves out of Scroll.cpp into InsertMode.cpp.
2026-10-30 00:20:19 -05:00
Brues 5ab497f2c7 feat(smf): scroll offset, insert mode, display-refreshed callbacks
Retail's ScrollingMessageFrame SetScrollOffset, GetScrollOffset,
GetMaxScrollRange, ScrollByAmount, SetInsertMode / GetInsertMode with the
SCROLLING_MESSAGE_FRAME_INSERT_MODE_* globals, and
AddOnDisplayRefreshedCallback, which ScrollUtil's
InitScrollingMessageFrameWithScrollBar calls.

The offset is the engine's own: SetScrollFromBottom's body (0x00788700)
is 4.3.4's SetScrollOffset step for step, and GetCurrentScroll is
GetScrollOffset under retail's name. The setters clamp to
0..GetNumMessages()-1 first, as retail does; the engine wraps. 1.12's
layout stacks lines bottom-up only, so the insert mode is BOTTOM and
SetInsertMode raises an error for anything else.

The callbacks run after the frame's own update (0x00788460, OnUpdate then
the fader), where retail's OnPostUpdate runs them, for a frame the line
layout, AddMessage or ScrollByAmount marked. The layout hook moves out of
Text::InlineTexture into ScrollingMessageFrame::Layout, which owns it and
fans out to both.
2026-10-29 23:56:24 -05:00
Brues 205d1fd43b feat(smf): ScrollingMessageFrame GetMessageInfo
Retail's smf:GetMessageInfo(index): text, r, g, b, then every argument
AddMessage took after the colour. Index 1 is the oldest message, and an
index outside 1..GetNumMessages() returns nothing.

The text and colour come from the engine's record in the frame's message
ring, so UpdateColorByID's changes read back. The record keeps only an
integer id after the colour, so AddMessage is registered again over the
engine's: Storm's hash links a new method node at the head of its bucket
and the lookup takes the first match. It runs the engine's AddMessage,
then keeps the extra arguments for the record just written, in a
registry table reused per record.
2026-10-29 23:43:45 -05:00
Brues 950c9deb76 feat(death): C_DeathInfo self-resurrect options, SELF_RES_SPELL_CHANGED
C_DeathInfo.GetSelfResurrectOptions, C_DeathInfo.UseSelfResurrectOption
and Enum.SelfResurrectOptionType over what 1.12 already serves through
HasSoulstone and UseSoulstone. Both pick one option the same way:
PLAYER_SELF_RES_SPELL when the server set it, else the first carried
item whose on-use spell is a self-resurrect. The list holds that option
under HasSoulstone's alive gate, and using it calls the engine's
UseSoulstone. Nothing in 1.12 limits a self-resurrection, so canUse is
true, isLimited false and priority 0.

SELF_RES_SPELL_CHANGED is 4.3.4's watcher on PLAYER_SELF_RES_SPELL from
SetActiveMirrorHandlers. 1.12 watches no such field, so the watcher joins
CGPlayer_C::AddActiveWatchers and leaves before its removal, the lifetime
of the engine's own player watchers.
2026-10-29 20:37:40 -05:00
Brues e7ccc11309 feat(unit): UnitHasIncomingResurrection, INCOMING_RESURRECT_CHANGED
4.3.4's CGIncomingResurrection. SMSG_SPELL_START adds a resurrect cast
under its object target, a unit or a corpse. SMSG_SPELL_GO, the failure
packets and ClearCastingSpell (through Spell::Cast, Turtle's only
interrupt signal) end it. A cast whose caster leaves view never reports
its end, so it ends on the next world tick. Each change fires the event
for every token naming the target, or the corpse's owner. 1.12's class
resurrections carry effect 113, not 4.3.4's 18, so both count.

Object::Corpse::ByOwner, lifted from spell/Range.cpp, now skips bones as
4.3.4's ClntObjMgrFindCorpseByOwner does. Turtle's bones keep their
owner, so old bones hid a ghost's live corpse, from this API and from
the range check's ghost redirect.

HasEffect moves from CombatLog to Spell::Lookup, and the SpellCastTargets
object mask 0x8A02 becomes one Offsets.h constant.
2026-10-29 19:21:47 -05:00
Brues bab31b5f7b docs(unit): bound the GetShapeshiftForm example to the bar
GetShapeshiftForm returns the bar count + 1 for a form that is not on
the stance bar, as 3.3.5 and 4.3.4 do. The example passed that index to
GetShapeshiftFormInfo, which returns nils for it.
2026-10-28 23:31:49 -05:00
Brues e0e24bab34 feat(macro): SetMacroDisplay range unit
C_Macro.SetMacroDisplay(macroSlot, value [, unit]) takes the unit the
button's range check uses, so a publisher's [@unit] reaches the range
check as SetMacroSpell's target does. The token stays a token: each
lookup resolves it again, so the range follows a new mouseover, focus or
target with no republish. A token that names no unit leaves the target
in charge.

The engine's resolver vets the unit at publish and raises its "Unknown
unit name" error for anything else, as UnitExists does. It tells a
token's family from its spelling alone, so a token that passes once
never raises at a later lookup. The unit is ignored for false and for an
empty value, and a publish that changes only the unit updates it.
2026-10-28 23:19:36 -05:00
53 changed files with 3476 additions and 163 deletions
+6 -2
View File
@@ -71,6 +71,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
| [DeathInfo](docs/API.md#deathinfo) | `C_DeathInfo.GetSelfResurrectOptions`, `C_DeathInfo.UseSelfResurrectOption` |
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.PickupEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid`, `GetFramesRegisteredForEvent` |
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetAccountExpansionLevel`, `GetClassicExpansionLevel`, `GetExpansionLevel`, `GetMaxPlayerLevel` |
@@ -85,7 +86,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation` |
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--sliderisenabled) | `:Enable` / `:Disable` / `:IsEnabled` |
| [Frame: Cooldown](docs/API.md#createframecooldown--name-parent-template) | `CreateFrame("Cooldown")`, `:SetCooldown`, `:SetCooldownDuration`, `:SetCooldownFromExpirationTime`, `:Clear`, `:GetCooldownTimes`, `:GetCooldownDuration`, `:GetCooldownDisplayDuration`, `:Pause` / `:Resume`, `:SetPaused` / `:IsPaused`, `:SetReverse` / `:GetReverse`, `:SetDrawEdge` / `:GetDrawEdge`, `:SetDrawSwipe` / `:GetDrawSwipe`, `:SetDrawBling` / `:GetDrawBling`, `:SetSwipeColor`, `:SetEdgeColor`, `:SetSwipeTexture`, `:SetEdgeTexture`, `:SetBlingTexture`, `:SetRotation` / `:GetRotation`, `:SetHideCountdownNumbers` / `:GetHideCountdownNumbers`, `:GetCountdownFontString`, `:SetCountdownAbbrevThreshold` / `:GetCountdownAbbrevThreshold`, `:SetCountdownMillisecondsThreshold` / `:GetCountdownMillisecondsThreshold`, `:SetMinimumCountdownDuration` / `:GetMinimumCountdownDuration`, `OnCooldownDone`, `CooldownFrame_Set`, `CooldownFrame_Clear`, `CooldownFrame_SetDisplayAsPercentage` |
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled` |
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled`, `:GetMessageInfo` (with the extra `:AddMessage` values), `:BackFillMessage` (adds a message as the oldest), `:RemoveMessagesByPredicate`, `:SetScrollOffset` / `:GetScrollOffset`, `:GetMaxScrollRange`, `:ScrollByAmount`, `:SetInsertMode` / `:GetInsertMode`, `:AddOnDisplayRefreshedCallback` |
| [Frame: scripts](docs/API.md#onenable-and-ondisable-scripts) | `OnEnable`, `OnDisable`, `OnMinMaxChanged`, `OnAttributeChanged`, `OnCharComposition`, `OnTextChanged` (`userInput`), `PreClick`, `PostClick`, `SetModernScriptArgs` / `GetModernScriptArgs` |
| [FriendList](docs/API.md#friendlist) | `AddOrRemoveFriend`, `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
@@ -133,7 +134,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked` |
| [Trainer](docs/API.md#trainer) | `GetTrainerServiceItemLink` |
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHasIncomingResurrection`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByAuraInstanceID`, `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySlot`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetAuraSlots`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration`, `C_UnitAuras.UnitAura`, `C_UnitAuras.UnitAuraBySlot`, `C_UnitAuras.UnitBuff`, `C_UnitAuras.UnitDebuff` |
| [VoiceChat](docs/API.md#voicechat) | `C_VoiceChat.GetTtsVoices`, `C_VoiceChat.GetRemoteTtsVoices`, `C_VoiceChat.SpeakText`, `C_VoiceChat.StopSpeakingText`, `C_TTSSettings.GetSpeechRate`, `C_TTSSettings.GetSpeechVolume`, `C_TTSSettings.GetSpeechVoiceID`, `C_TTSSettings.GetVoiceOptionName`, `C_TTSSettings.SetSpeechRate`, `C_TTSSettings.SetSpeechVolume`, `C_TTSSettings.SetVoiceOption`, `C_TTSSettings.SetVoiceOptionByName`, `C_TTSSettings.SetDefaultSettings`, `C_TTSSettings.RefreshVoices` |
| [XMLUtil](docs/API.md#xmlutil) | `C_XMLUtil.DoesTemplateExist`, `C_XMLUtil.GetTemplateInfo`, `C_XMLUtil.GetTemplates` |
@@ -279,6 +280,7 @@ when launching with `-console`), not as Lua functions. See the
| `GLOBAL_MOUSE_DOWN` | `button` |
| `GLOBAL_MOUSE_UP` | `button` |
| `HEARTHSTONE_BOUND` | *(none)* |
| `INCOMING_RESURRECT_CHANGED` | `unitTarget` |
| `INVENTORY_SEARCH_UPDATE` | *(none)* |
| `ITEM_DATA_LOAD_RESULT` | `itemID, success` |
| `LEARNED_SPELL_IN_SKILL_LINE` | `spellID, skillLineIndex, isGuildPerkSpell` |
@@ -312,6 +314,7 @@ when launching with `-console`), not as Lua functions. See the
| `QUEST_QUERY_COMPLETE` | *(none)* (Turtle WoW only) |
| `QUEST_REMOVED` | `questID` |
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
| `SELF_RES_SPELL_CHANGED` | *(none)* |
| `SOUNDKIT_FINISHED` | `soundHandle` (only for sounds played with `runFinishCallback`) |
| `UNIT_AURA_DELTA` | `unitTarget, updateInfo` (the instance IDs of the auras added, updated, and removed) |
| `UNIT_SPELLCAST_SENT` | `"player", target, castGUID, spellID, spellName, rank` |
@@ -359,6 +362,7 @@ when launching with `-console`), not as Lua functions. See the
| Spellbook bank | `Enum.SpellBookSpellBank.{Player,Pet}` (0–1) |
| Spellbook item type | `Enum.SpellBookItemType.{None,Spell,FutureSpell,PetAction,Flyout}` (0–4; 1.12 only yields `Spell`/`PetAction`) |
| Player swing type | `Enum.PlayerSwingType.{MainHand,OffHand,Ranged}` (0–2) |
| Self-resurrect option type | `Enum.SelfResurrectOptionType.{Spell,Item}` (0–1) |
</details>
+16 -3
View File
@@ -4081,9 +4081,22 @@ Each entry still needs the usual check of how the engine does it before building
`GetRotatesTexture` (`CSimpleStatusBar.cpp:135`, the vertical texcoord path
at `:285`). Retail adds `SetFillStyle` (STANDARD / CENTER / REVERSE),
`SetColorFill`, `SetStatusBarDesaturated` / `Desaturation`.
- **ScrollingMessageFrame** `SetInsertMode` / `GetInsertMode`,
`GetMessageInfo`, `RemoveMessagesByAccessID`, `RemoveMessagesByExtraData`,
`SetScrollOffset` — in C in 4.3.4 (retail moved the type to Lua).
- **DONE** (`src/scrollingmessageframe/`, retail's shapes).
**ScrollingMessageFrame** `GetMessageInfo`, `SetScrollOffset` (1.12's
`SetScrollFromBottom`, 0x00793070, under its later name, clamped as
retail), `GetScrollOffset`, `GetMaxScrollRange`, `ScrollByAmount`,
`AddOnDisplayRefreshedCallback`, `SetInsertMode` / `GetInsertMode` (TOP
through a redirect of the layout's one line-anchor call, 0x00788975),
`BackFillMessage` (writes 4.3.4's addToTop slot of the ring, the free one
before the oldest; a full frame drops it, as retail's CircularBuffer
`PushBack` does — 4.3.4 overwrites the oldest there).
- **DONE** (`src/scrollingmessageframe/MessageInfo.cpp`).
**ScrollingMessageFrame** `RemoveMessagesByPredicate` (retail's shape; it
covers 4.3.4's `RemoveMessagesByAccessID` / `RemoveMessagesByExtraData`,
whose int fields our retail-shaped records do not have). The compaction
is 4.3.4's `RemoveLines` loop over 1.12's ring, then the engine's own
layout (0x00788750) at the kept scroll offset and re-arm (0x00788B80),
or Clear (0x007882B0) when nothing is kept.
- **DONE** (`src/editbox/Focus.cpp`, `src/animation/Framerate.cpp`).
**Globals** (`SimpleFrameScript_Scripts.cpp`): `GetCurrentKeyBoardFocus()`
(the focused edit box's wrapper, or nil); `SetMaxAnimFramerate(fps)` /
+297 -9
View File
@@ -182,6 +182,10 @@ build instructions.
- [`GetCursorInfo()`](#getcursorinfo)
- [`CursorHasMacro()`](#cursorhasmacro)
- [DeathInfo](#deathinfo)
- [`C_DeathInfo.GetSelfResurrectOptions()`](#c_deathinfogetselfresurrectoptions)
- [`C_DeathInfo.UseSelfResurrectOption(optionType, id)`](#c_deathinfouseselfresurrectoptionoptiontype-id)
- [EquipmentSet](#equipmentset)
- [Overview & file format](#overview--file-format)
- [`C_EquipmentSet.CanUseEquipmentSets()`](#c_equipmentsetcanuseequipmentsets)
@@ -223,6 +227,7 @@ build instructions.
- [`EQUIPMENT_SWAP_FINISHED` event](#equipment_swap_finished-event)
- [`WEAR_EQUIPMENT_SET` event](#wear_equipment_set-event)
- [`FACTION_STANDING_CHANGED` event](#faction_standing_changed-event)
- [`INCOMING_RESURRECT_CHANGED` event](#incoming_resurrect_changed-event)
- [`INVENTORY_SEARCH_UPDATE` event](#inventory_search_update-event)
- [`LOOT_HISTORY_ROLL_CHANGED` / `LOOT_HISTORY_ROLL_COMPLETE` / `LOOT_HISTORY_FULL_UPDATE` events](#loot_history_roll_changed--loot_history_roll_complete--loot_history_full_update-events)
- [`LEARNED_SPELL_IN_SKILL_LINE` event](#learned_spell_in_skill_line-event)
@@ -240,6 +245,7 @@ build instructions.
- [`QUEST_QUERY_COMPLETE` event](#quest_query_complete-event)
- [`QUEST_REMOVED` event](#quest_removed-event)
- [`QUEST_TURNED_IN` event](#quest_turned_in-event)
- [`SELF_RES_SPELL_CHANGED` event](#self_res_spell_changed-event)
- [`SOUNDKIT_FINISHED` event](#soundkit_finished-event)
- [`UNIT_AURA_DELTA` event](#unit_aura_delta-event)
- [`UNIT_FACTION` event (fire-coverage fix)](#unit_faction-event-fire-coverage-fix)
@@ -379,6 +385,12 @@ build instructions.
- [`frame:HookScript(scriptType, handler)`](#framehookscriptscripttype-handler)
- [Script binding types](#script-binding-types)
- [`smf:SetHyperlinksEnabled(enabled)` / `smf:GetHyperlinksEnabled()`](#smfsethyperlinksenabledenabled--smfgethyperlinksenabled)
- [`smf:GetMessageInfo(index)`](#smfgetmessageinfoindex)
- [`smf:BackFillMessage(text, r, g, b, ...)`](#smfbackfillmessagetext-r-g-b-)
- [`smf:RemoveMessagesByPredicate(predicate)`](#smfremovemessagesbypredicatepredicate)
- [`smf:SetScrollOffset(offset)` / `smf:GetScrollOffset()` / `smf:GetMaxScrollRange()` / `smf:ScrollByAmount(amount)`](#smfsetscrolloffsetoffset--smfgetscrolloffset--smfgetmaxscrollrange--smfscrollbyamountamount)
- [`smf:SetInsertMode(mode)` / `smf:GetInsertMode()`](#smfsetinsertmodemode--smfgetinsertmode)
- [`smf:AddOnDisplayRefreshedCallback(callback)`](#smfaddondisplayrefreshedcallbackcallback)
- [`frame:IsEventRegistered(event)`](#frameiseventregisteredevent)
- [`frame:RegisterUnitEvent(event, ...units)`](#frameregisteruniteventevent-units)
- [`frame:GetEffectiveAlpha()`](#framegeteffectivealpha)
@@ -644,7 +656,7 @@ build instructions.
- [`GetMacroIcons` / `GetMacroItemIcons` / `GetLooseMacroIcons` / `GetLooseMacroItemIcons`](#getmacroicons--getmacroitemicons--getloosemacroicons--getloosemacroitemicons)
- [`C_Macro.CreateMacro` / `C_Macro.EditMacro`](#c_macrocreatemacro--c_macroeditmacro)
- [`C_Macro.GetMacroIcon(macroSlot)`](#c_macrogetmacroiconmacroslot)
- [`C_Macro.SetMacroDisplay(macroSlot, value)`](#c_macrosetmacrodisplaymacroslot-value)
- [`C_Macro.SetMacroDisplay(macroSlot, value [, unit])`](#c_macrosetmacrodisplaymacroslot-value--unit)
- [Mail](#mail)
- [`GetSendMailItemLink([attachmentIndex])`](#getsendmailitemlinkattachmentindex)
@@ -1011,6 +1023,7 @@ build instructions.
- [`UnitPowerType(unit)`](#unitpowertypeunit)
- [`UnitSpellHaste(unit)`](#unitspellhasteunit)
- [`UnitSpellTargetName(unit)`](#unitspelltargetnameunit)
- [`UnitHasIncomingResurrection(unit)`](#unithasincomingresurrectionunit)
- [`CanBeRaidTarget(unit)`](#canberaidtargetunit)
- [UnitAuras](#unitauras)
@@ -4357,6 +4370,57 @@ Changes to what the cursor holds are reported by the
Returns `true` if the cursor holds a macro, and `false` if it does not.
## DeathInfo
### `C_DeathInfo.GetSelfResurrectOptions()`
```
options = C_DeathInfo.GetSelfResurrectOptions()
```
Returns an array of the ways that the dead player can resurrect themselves.
Each entry is a `SelfResurrectOption` table:
| Field | Type | Description |
|---|---|---|
| `name` | string | The name of the spell or the item. |
| `optionType` | number | `Enum.SelfResurrectOptionType.Spell` (0) or `Enum.SelfResurrectOptionType.Item` (1). |
| `id` | number | The spell ID or the item ID. |
| `canUse` | boolean | Always `true`. |
| `isLimited` | boolean | Always `false`. |
| `priority` | number | Always `0`. |
The array has one entry at most. A self-resurrection spell, such as
Soulstone Resurrection or Reincarnation, comes before an item. The array is
empty when the player is alive or a ghost, or has no option. The function
returns nothing before the player enters the world.
```lua
local options = C_DeathInfo.GetSelfResurrectOptions()
if options and options[1] then
print("You can resurrect with", options[1].name)
end
```
[`SELF_RES_SPELL_CHANGED`](#self_res_spell_changed-event) fires when the
self-resurrection spell changes.
### `C_DeathInfo.UseSelfResurrectOption(optionType, id)`
Resurrects the dead player with an option from
[`C_DeathInfo.GetSelfResurrectOptions`](#c_deathinfogetselfresurrectoptions).
`optionType` and `id` must match an entry that it returns now. If they do
not match, the call does nothing. An item option uses the item, and this
stops a spell cast in progress.
```lua
local options = C_DeathInfo.GetSelfResurrectOptions()
local option = options and options[1]
if option then
C_DeathInfo.UseSelfResurrectOption(option.optionType, option.id)
end
```
## EquipmentSet
`C_EquipmentSet.*` — named sets of gear you can save and equip again
@@ -5297,6 +5361,25 @@ forwarding, which [`C_Reputation.GetLastStandingChange`](#c_reputationgetlaststa
exposes — so addons can read the structured payload from inside the
chat event without re-parsing the localized string.
### `INCOMING_RESURRECT_CHANGED` event
Fires when a resurrection cast at a unit starts or stops. `arg1` is the unit
token. The event gives no value.
[`UnitHasIncomingResurrection`](#unithasincomingresurrectionunit) gives the
new value.
The event fires one time for each token that names the unit, for example
`"party1"` and `"target"`. If the cast is at the corpse of a released player,
`arg1` names that player.
```lua
local f = CreateFrame("Frame")
f:RegisterEvent("INCOMING_RESURRECT_CHANGED")
f:SetScript("OnEvent", function()
print(arg1, UnitHasIncomingResurrection(arg1))
end)
```
### `INVENTORY_SEARCH_UPDATE` event
Fires with no payload when
@@ -5762,6 +5845,14 @@ quest invalidated, etc.). Only fires on a real successful turn-in.
> done, quest removed from log). Hooking the packet handler gives
> us a clean turn-in signal.
### `SELF_RES_SPELL_CHANGED` event
Fires with no payload when the self-resurrection spell of the player
changes. For example, it fires when the player dies with a Soulstone, and
again when the player comes back to life. When it fires, read
[`C_DeathInfo.GetSelfResurrectOptions`](#c_deathinfogetselfresurrectoptions)
again. A change to an item option does not fire this event.
### `SOUNDKIT_FINISHED` event
Fires when a sound finishes playing.
@@ -8578,6 +8669,151 @@ ChatFrame1:SetHyperlinksEnabled(false) -- links in this chat window stop react
ChatFrame1:GetHyperlinksEnabled() -- false
```
### `smf:GetMessageInfo(index)`
On ScrollingMessageFrame. Returns one message of the frame: its text, its
color, and the extra values that `AddMessage` got for the message. Index 1 is
the oldest message, and `smf:GetNumMessages()` is the index of the newest
message. If no message has the index, the function returns nothing. If
`index` is not a number, the function gives an error.
- `text` is the text that the frame keeps, with its color codes.
- `r`, `g` and `b` are the color that the frame shows for the message. After
`UpdateColorByID` changes the color, these values also change. The frame
keeps each color value in steps of 1/255, so `0.5` returns as `0.50196…`.
- `smf:AddMessage(text, r, g, b, ...)` keeps all the values after `b`.
`GetMessageInfo` returns these values in the same order, nil values
included.
An addon that replaces `AddMessage` and gives only `text, r, g, b` to the
original function removes the extra values.
```lua
ChatFrame1:AddMessage("hello", 1, 0.5, 0, 7, "extra")
ChatFrame1:GetMessageInfo(ChatFrame1:GetNumMessages())
-- "hello", 1, 0.50196078431373, 0, 7, "extra"
for i = 1, ChatFrame1:GetNumMessages() do
local text, r, g, b = ChatFrame1:GetMessageInfo(i) -- oldest message first
end
```
### `smf:BackFillMessage(text, r, g, b, ...)`
On ScrollingMessageFrame. Adds a message as the oldest message of the frame.
`AddMessage` adds the newest message. `BackFillMessage` adds history behind
the messages that the frame already has. An addon that rebuilds a frame from
a history walks the history from the newest entry to the oldest and adds each
entry with this function. The messages that the frame already has do not
move.
- `text`, `r`, `g`, `b` and the extra values work as in `AddMessage`.
Without `r`, `g` and `b`, the message takes the color of the frame's font.
`GetMessageInfo` returns the extra values after the color.
- If the frame is at its line limit (`SetMaxLines`), the function drops the
message, because the message is older than each message that the frame
keeps.
- If the frame is scrolled to the bottom, the frame shows the message when
there is room above its other lines. If the frame is scrolled up, the frame
keeps its view.
```lua
ChatFrame1:BackFillMessage("older", 1, 1, 1)
ChatFrame1:GetMessageInfo(1) -- "older", 1, 1, 1
```
### `smf:RemoveMessagesByPredicate(predicate)`
On ScrollingMessageFrame. Removes the messages that `predicate` selects. The
function calls `predicate(text, r, g, b, ...)` for each message, from the
oldest to the newest, with the values that `GetMessageInfo` returns for the
message. If `predicate` returns true, the function removes the message. The
other messages keep their order. If `predicate` is not a function, the
function gives an error.
- The function calls `predicate` for all messages before it removes any
message. If `predicate` gives an error for a message, the frame keeps that
message.
- The frame keeps its scroll offset (`GetScrollOffset`). If fewer messages
remain than the offset, the offset becomes the oldest message.
- The frame shows the lines that remain at once. If no message remains, the
result is the same as `smf:Clear()`.
```lua
-- remove the messages that were added with the extra value 7
ChatFrame1:RemoveMessagesByPredicate(function(text, r, g, b, id)
return id == 7
end)
```
### `smf:SetScrollOffset(offset)` / `smf:GetScrollOffset()` / `smf:GetMaxScrollRange()` / `smf:ScrollByAmount(amount)`
On ScrollingMessageFrame. The scroll offset is the number of lines that the
frame is scrolled away from the newest message. When the frame is not
scrolled, the offset is 0. The largest offset is `GetMaxScrollRange()`, that
is `GetNumMessages() - 1`, or 0 for an empty frame. At that offset, the oldest
message is the bottom line, or the top line in TOP insert mode.
- `SetScrollOffset(offset)` scrolls to `offset`. An offset less than 0 or
more than `GetMaxScrollRange()` changes to the nearest limit.
- `ScrollByAmount(amount)` adds `amount` to the offset. A positive amount
scrolls to older messages. The function also shows the faded lines
again, also when the offset does not change.
- If `offset` or `amount` is not a number, the function gives an error.
```lua
ChatFrame1:ScrollByAmount(3) -- 3 lines up
ChatFrame1:GetScrollOffset() -- 3
ChatFrame1:SetScrollOffset(ChatFrame1:GetMaxScrollRange()) -- the oldest message
ChatFrame1:SetScrollOffset(0) -- the newest message again
```
### `smf:SetInsertMode(mode)` / `smf:GetInsertMode()`
On ScrollingMessageFrame. The insert mode tells where the newest message
shows.
- `SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM` (2) shows the newest message
at the bottom, with older messages above it. This is the default.
- `SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP` (1) shows the newest message at
the top, with older messages below it.
In both modes, the scroll offset counts lines from the newest message. A
change of mode moves the lines that show and runs the display-refreshed
callbacks. If `mode` is the current mode, `SetInsertMode` does nothing. If
`mode` is not one of the two values, the function gives an error.
```lua
local f = CreateFrame("ScrollingMessageFrame", nil, UIParent)
f:SetInsertMode(SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP)
f:AddMessage("first")
f:AddMessage("second") -- shows above "first"
f:GetInsertMode() -- 1
```
### `smf:AddOnDisplayRefreshedCallback(callback)`
On ScrollingMessageFrame. Adds a function that the frame calls as
`callback(frame)` after it refreshes the messages that it shows. The frame
refreshes after a new message, a scroll, a resize, a font change, or
`Clear()`.
- The callbacks run one time, in the next update of the frame, in the order
that you added them.
- A hidden frame runs its callbacks when it shows again.
- An error in one callback goes to the error handler, and the other callbacks
still run.
- A change that a callback makes to the frame does not run the callbacks
again.
- If `callback` is not a function, the method gives an error. You cannot
remove a callback.
```lua
ChatFrame1:AddOnDisplayRefreshedCallback(function(frame)
MyScrollLabel:SetText(frame:GetScrollOffset() .. " / " .. frame:GetMaxScrollRange())
end)
```
### `frame:IsEventRegistered(event)`
Returns `true` if the frame is registered for `event`, else `false` (also
@@ -14968,7 +15204,7 @@ to.
If SuperCleveRoidMacros is loaded, it controls macro display, and ClassicAPI
does not evaluate `#showtooltip`. A build that hands its own results over
through
[`C_Macro.SetMacroDisplay`](#c_macrosetmacrodisplaymacroslot-value) keeps
[`C_Macro.SetMacroDisplay`](#c_macrosetmacrodisplaymacroslot-value--unit) keeps
`#showtooltip` working for the macros it does not claim.
A macro that names a condition
@@ -15342,7 +15578,7 @@ mark, and the macro's own icon otherwise.
selector edits. Use this function when you want the icon a macro shows, and
`GetMacroInfo` when you want the one the player picked.
### `C_Macro.SetMacroDisplay(macroSlot, value)`
### `C_Macro.SetMacroDisplay(macroSlot, value [, unit])`
Tells the client what a macro is about, so the action button shows it. For
an addon that parses macros itself and wants the button to follow, instead
@@ -15350,9 +15586,10 @@ of replacing `GetActionTexture`, `GetActionCooldown`, `IsUsableAction` and
the rest in Lua.
```lua
C_Macro.SetMacroDisplay(1, "Frostbolt") -- show this
C_Macro.SetMacroDisplay(1, false) -- mine, nothing matched, show ?
C_Macro.SetMacroDisplay(1, nil) -- released, `#showtooltip` resumes
C_Macro.SetMacroDisplay(1, "Frostbolt") -- show this
C_Macro.SetMacroDisplay(1, "Flash Heal", "mouseover") -- show this, range to the mouseover
C_Macro.SetMacroDisplay(1, false) -- mine, nothing matched, show ?
C_Macro.SetMacroDisplay(1, nil) -- released, `#showtooltip` resumes
```
`macroSlot` is the macro index, the same one
@@ -15366,6 +15603,31 @@ it, the same as a directive that names its own value.
Returns `true` when the value named a spell or an item.
**The range unit.** `unit` is optional. It names the unit that the button's
range check uses in place of your target. Usually it is the `[@unit]` of the
clause that your parser matched. It takes any unit token or GUID string that
`UnitExists` takes, for example `"mouseover"`, `"focus"` or
`"party2target"`. If you give no `unit`, or `nil`, the range check uses your
target.
The client finds the unit again each time it checks the button's range. So
if `"mouseover"` moves to a different unit, the range follows it with no new
call. The same is true for `"focus"`, `"target"` and every other token. This
is different from the `target` of
[`SetMacroSpell`](#setmacrospellmacro-spell--target--setmacroitemmacro-item--target),
which is found once, when you call it.
If the token points at no unit at that time, for example `"mouseover"` with
nothing under the cursor, the range check uses your target. If the unit is
too far away for the client to see it, for example a party member in another
zone, the button has no range state.
The function ignores `unit` when `value` is `false`, `nil` or an empty
string, because the macro then aims at nothing. A call with the same `value`
and a different `unit` changes the unit. If `unit` is not a unit token, the
function raises the error `Unknown unit name: <unit>`, the same error that
`UnitExists` gives. Then it publishes nothing.
**What the button picks up.** The spell is written where the client keeps
each macro's spell, which is the field its own buttons read. So cooldown,
range, usable, out-of-mana greying, the current-cast highlight and
@@ -15379,8 +15641,8 @@ button, on the cursor while you drag the macro, and in the macro window.
The icon replaces the macro's own only when that is the question mark, which
is the same rule `#showtooltip` follows. A macro with a chosen icon keeps it.
**Call it whenever your answer changes.** Nothing is re-evaluated for you.
The value stands until you publish another, and it survives the client
**Call it whenever your answer changes.** Nothing is re-evaluated for you,
except the unit that a `unit` token points at. The value stands until you publish another, and it survives the client
re-reading the macro, so an edit elsewhere cannot quietly replace it.
Publishing takes the macro over: `#showtooltip` is not parsed for it while
@@ -20560,7 +20822,7 @@ is the number that `[stance:N]` and `[form:N]` test.
```lua
local index = GetShapeshiftForm()
if index > 0 then
if index > 0 and index <= GetNumShapeshiftForms() then
local icon, name = GetShapeshiftFormInfo(index)
print("active:", name)
end
@@ -23142,6 +23404,32 @@ The player's target lands with the confirming packet, about one round-trip
after the cast starts. So `UnitSpellTargetName("player")` can read `nil` for
the first part of your own cast, then return the name.
### `UnitHasIncomingResurrection(unit)`
```
hasIncomingResurrection = UnitHasIncomingResurrection(unit)
```
Returns true while somebody casts a resurrection spell at `unit`. Otherwise
it returns false. If `unit` is a released player, the function also includes
casts at the corpse of that player.
```lua
if UnitHasIncomingResurrection("party1") then
print(UnitName("party1"), "has an incoming resurrection")
end
```
The value changes to true when the cast starts. It changes to false when the
cast stops, for any reason. This includes a completed cast that the target did
not accept yet.
You see a cast only when its caster is near you. If the caster goes out of
range during the cast, the value changes to false.
[`INCOMING_RESURRECT_CHANGED`](#incoming_resurrect_changed-event) fires at
each change.
### `CanBeRaidTarget(unit)`
Returns true if you can put a raid target icon on the unit. Otherwise it
+22 -4
View File
@@ -86,9 +86,11 @@ The DLL hooks the engine's own text pipeline — no companion addon:
(`49934b0`, `b9d0705`).
- **Chat fade** — icons fade in lockstep with their chat line (`3ca3b7e`).
- **Edit boxes** — render the icons in their text, as 4.3.4 does; the caret,
the mouse and the arrow keys treat an icon as one character, and Ctrl+C
drops icons (4.3.4's 0x1500 strip mask). Typed markup stays literal by the
engine's own input escape (below).
the mouse and the arrow keys treat an icon as one character, Ctrl+C
drops icons (4.3.4's 0x1500 strip mask), and a multi-line box's caret,
selection and click rows follow the lines a tall icon moves (below;
4.3.4's own rows do not). Typed markup stays literal by the engine's own
input escape (below).
- **Payload** — the full
`|Tpath:height:width:offsetX:offsetY:texW:texH:left:right:top:bottom:r:g:b|t`:
size, pen offset, sprite-sheet crop, and vertex tint. BLP and uncompressed
@@ -377,7 +379,7 @@ funnel for exactly 4 callers:
- `Script_GetStringWidth` (0x0079E510)
- the GameTooltip auto-size (0x00530640)
- the editbox caret positioner (0x0077DE70)
- the editbox selection-piece placer (0x0077DE70, a whole last line's width)
- the layout effective-width vmethod (`FUN_00772930` = fs+0x24 vtbl +0x1C,
which feeds auto-width fontstring layout)
@@ -523,6 +525,22 @@ the same input escape, so ClassicAPI's edit boxes need no special case:
icon with the following character into one step; ours is its own step.
- **Caret x** — `FUN_0077DA80` measures the line through
`FUN_00772AE0`, whose co-hook adds the icons.
- **Rows (multi-line)** — `PlaceCaret` (`0x77da80`), `PlaceSelectionPiece`
(`0x77de70`) and `GetIndexAtPoint` (`0x77d0d0`) step rows of one height
(font height + spacing, pixel-aligned), as 4.3.4's `UpdateVisibleCursor`,
`UpdateHighlightArea` and `ConvertCoordinateToIndex` do — while the render
moves a tall-icon line and every line after it down by the overflow
(`InitializeText`; the emitter co-hook) and the box grows to match (the
string-height co-hook). 4.3.4 has that fault too; `EditBox::IconRows`
(`src/editbox/IconRows.cpp`) gives each placer the render's move, from
`Text::InlineTexture::LineIconShift` over the box's own line table: the
click's y is moved onto the uniform row of the icon-aware row under it
before the engine maps it; a selection piece is re-anchored after the
engine placed it, with the engine's own x; the caret likewise, then
`OnCursorChanged` fired once with the moved y through
`FUN_FRAME_RUN_SCRIPT_VARIADIC` exactly as `PlaceCaret` fires it (its own
fire muted for the call by emptying the slot), so
`ScrollingEditBoxMixin:ScrollCursorIntoView` scrolls to the real caret.
- **Click → cursor, single-line window** — `GetIndexAtPoint` (`0x77d0d0`),
`RebuildLabel` and `MeasureWindow` count characters with
`FUN_FONTSTRING_FIT_TEXT` (`0x772bc0` → `0x44d860` → `0x5c6c50`), whose
+169 -2
View File
@@ -988,7 +988,8 @@ enum Offsets {
// message. Every new message shifts the newest index, so EVERY visible slot
// is re-SetText'd -> its inline icons must be recomputed; the engine builds
// the line lazily at paint, so icons land one frame after the glyphs (the
// one-frame lag). Text::InlineTexture co-hooks this and, post-refresh,
// one-frame lag). ScrollingMessageFrame::Layout owns the hook and fans
// out after it; Text::InlineTexture listens and, post-refresh,
// forces each icon-bearing line to resolve+build and applies its icon
// placement synchronously — pre-render (the refresh runs from the SMF's
// Lua/event-driven methods, never mid-render), so icons draw with their
@@ -2425,6 +2426,16 @@ enum Offsets {
// right after that same unlock — Player::Equipment co-hooks it to do so.
FUN_INV_SLOT_CHANGED_OBSERVER = 0x005DDCF0,
// CGPlayer_C::AddActiveWatchers / its removal — `void __thiscall
// (CGPlayer_C *)`, plain `ret`. Register and remove the active player's
// bank-PLAYER observers (inventory, quests, coinage, skills, the
// 0x005DE4B0 event fields); 4.3.4's SetActiveMirrorHandlers (0x0056E430)
// / UnsetActiveMirrorHandlers. Add runs on arrival (0x005DEA58) and
// reactivation (0x005E9C00); remove only at 0x005E9B50, for the active
// player.
FUN_PLAYER_ADD_ACTIVE_WATCHERS = 0x005DD8A0,
FUN_PLAYER_REMOVE_ACTIVE_WATCHERS = 0x005DE5B0,
// PLAYER_FIELD_INV_SLOT_HEAD — first of the 19 equipment-slot u64 GUID
// fields in the player descriptor (0x4A8..0x538, stride 8; 0x540 is
// the first equipped-bag slot). 0-based slot = (offset - 0x4A8) >> 3;
@@ -3396,6 +3407,13 @@ enum Offsets {
SMSG_SPELL_CHANNEL_UPDATE = 0x13A,
SMSG_SPELL_DELAYED = 0x1E2,
SMSG_SPELL_FAILED_OTHER = 0x2A6,
// SpellCastTargets mask bits that carry a packed object GUID right after
// the u16 mask in SMSG_SPELL_START / SMSG_SPELL_GO: unit 0x2, PvP corpse
// 0x200, game object 0x800, corpse 0x8000 — one GUID for whichever is
// set (the client's target reader 0x007E4ED0; tortoise-wow
// SpellCastTargets::write). A resurrect aimed at a released player
// carries the corpse.
SPELL_TARGET_FLAGS_OBJECT = 0x8A02,
// Melee swing result broadcast. Body (verified against the server's own
// writer, tortoise-wow Unit::SendAttackStateUpdate): u32 hitInfo,
// packedGuid attacker, packedGuid victim, u32 totalDamage, u8 subCount,
@@ -4575,6 +4593,12 @@ enum Offsets {
// (see OFF_UNIT_FIELD_CHANNEL_SPELL below); PLAYER_FLAGS only
// exists on the +0xE68 sub-struct here.
OFF_CGPLAYER_INFO = 0xE68,
// PLAYER_SELF_RES_SPELL in that block (UpdateField table: player index
// 0x40C, owner-only): the spell the server offers a dead player to raise
// themselves (Soulstone Resurrection, Reincarnation), 0 for none. Also
// its offset in observer bank DESC_OBSERVER_BANK_PLAYER. Read by
// HasSoulstone (0x0048ACC6) and UseSoulstone (0x0048ADB6).
OFF_CGPLAYER_INFO_SELF_RES_SPELL = 0x1030,
// The engine's explored-area bit test (benilla `IsAreaExplored`):
// __thiscall(this = player + OFF_CGPLAYER_INFO, int areaBit) → nonzero
// when explored, ret 4. Reads `[this] + 0xE6C` (the 0x100-byte
@@ -4986,6 +5010,16 @@ enum Offsets {
// target is passed, so passing zero (no target) is fine for most
// items. `flag` is 0 in every Script_UseContainerItem call site.
FUN_ITEM_USE = 0x005D8D00,
// The player's self-resurrect item — `CGItem_C *__thiscall(CGPlayer_C *)`,
// plain `ret`: the first carried item (equipment, bags, backpack,
// keyring) with an on-use spell of effect 94 (SELF_RESURRECT), or null.
// HasSoulstone and UseSoulstone ask it when PLAYER_SELF_RES_SPELL is 0.
FUN_PLAYER_SELF_RES_ITEM = 0x005ED630,
// Script_UseSoulstone — the Lua C function itself, `int __fastcall(L)`;
// reads no argument and pushes nothing. Sends CMSG_SELF_RES (0x2B3, no
// body) when PLAYER_SELF_RES_SPELL is set, else cancels the cast
// (0x006E4940(1, 1, 0x1C)) and uses FUN_PLAYER_SELF_RES_ITEM's item.
FUN_SCRIPT_USE_SOULSTONE = 0x0048AD70,
// `CGPlayer::AutoEquipCursorItem` — `__thiscall(CGPlayer *this, int flag)`.
// The engine-internal helper that `Script_AutoEquipCursorItem`
// (`0x0048A040`) is a thin wrapper around: that wrapper just
@@ -12804,6 +12838,93 @@ enum Offsets {
OFF_SIMPLEHTML_FONTS = 0x350,
VAR_SIMPLEHTML_LUA_TYPE_ID = 0x00CF4F64,
FUN_SIMPLEHTML_READ_TEXT_TYPE = 0x00795D80,
// The ScrollingMessageFrame's message ring. GetNumMessages (0x007934D0)
// pushes the count. The ring's one writer, 0x00788150 (`__thiscall(smf,
// text, const CImVector *color, int id)`, `ret 0xC`), called only by the
// AddMessage method (FUN_SCRIPT_..._ADD_MESSAGE, `int __fastcall(L)`),
// raises the count up to the limit, advances the position (wrapping at
// the limit) and fills the record there. So message i, 1 the oldest, is
// record (position - count + i), plus the limit when negative. Records
// are 0x18 bytes: the text (an SStrDupA copy), the colour (a CImVector,
// the dword 0xAARRGGBB: AddMessage builds it R, G, B with alpha 0xFF at
// 0x00792ADD..0x00792B11), shown +0x8, the visible and fade times +0xC and
// +0x10, and the integer id +0x14 (benilla-re ui/messageframe).
FUN_SCRIPT_SCROLLINGMESSAGEFRAME_ADD_MESSAGE = 0x00792900,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT = 0x33C,
OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES = 0x340,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS = 0x344, // the newest record; -1 when empty
OFF_SCROLLINGMESSAGEFRAME_MESSAGES = 0x394, // the record array
SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE = 0x18,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT = 0x0,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COLOR = 0x4,
// The rest of the record as the ring writer 0x00788150 fills it: shown
// (1), the frame's visible and fade times, and the integer id.
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_SHOWN = 0x8,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_VISIBLE_TIME = 0xC,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_FADE_TIME = 0x10,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_ID = 0x14,
// The bottom flag (4.3.4's m_atBottom): the ring writer lays the lines
// out from the newest record when it is set (0x00788150 tail), and
// ScrollToBottom re-arms the shown lines instead of laying out
// (0x007886D0; benilla-re ui/messageframe "Scrolling brings lines back").
OFF_SCROLLINGMESSAGEFRAME_AT_BOTTOM = 0x354,
// The visible and fade times the writer copies into each record
// (0x00788150: record+0xC = +0x364, record+0x10 = +0x368; the setters
// 0x00788090 / 0x007880F0 store them).
OFF_SCROLLINGMESSAGEFRAME_VISIBLE_TIME = 0x364,
OFF_SCROLLINGMESSAGEFRAME_FADE_TIME = 0x368,
// The colour (CImVector) of the object at OFF_SCROLLINGMESSAGEFRAME_FONT,
// the message colour AddMessage takes without r, g, b
// (0x00792B3C..0x00792B45: `[[smf+0x35C]+0x58]`).
OFF_SCROLLINGMESSAGEFRAME_FONT_COLOR = 0x58,
// The frame's scroll in retail's units, the lines it is scrolled up from
// the newest message. ScrollToOffset (`void __thiscall(smf, int offset)`,
// `ret 4`) is the body of SetScrollFromBottom (0x00793070), 4.3.4's
// SetScrollOffset (0x0051E6F0) step for step: the record (position -
// offset + count) % count, then it re-arms the shown lines (0x00788B80)
// when that is the current scroll and lays the lines out from it
// (FUN_SMF_DISPLAY_REFRESH) otherwise. ScrollOffset (`int
// __thiscall(smf)`) is the inverse, (position - scroll + count) % count,
// 0 when empty; the GetCurrentScroll method pushes it.
FUN_SCROLLINGMESSAGEFRAME_SCROLL_TO_OFFSET = 0x00788700,
FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET = 0x00788AC0,
// Clear (`void __thiscall(smf)`, 4.3.4's Clear): count 0, position 0, a
// layout from 0, +0x350 = 0, position -1. The Clear method's body.
FUN_SCROLLINGMESSAGEFRAME_CLEAR = 0x007882B0,
// RearmLines (`void __thiscall(smf)`, 4.3.4's RefreshMessages): every
// shown line back to alpha 0xFF, shown, the frame's two times, and its
// node shown (benilla-re ui/messageframe "Scrolling brings lines back").
// FUN_SMF_DISPLAY_REFRESH (LayoutLines) owns +0x348 (the scroll record)
// and +0x354 (at bottom = record == position) itself, and re-arms only
// when it moves off the bottom; a layout at the bottom keeps the fade
// state, so 4.3.4 re-arms explicitly after a removal's layout.
FUN_SCROLLINGMESSAGEFRAME_REARM_LINES = 0x00788B80,
FUN_SCRIPT_SCROLLINGMESSAGEFRAME_GET_CURRENT_SCROLL = 0x00793650,
// The frame's OnFrameUpdate override (vtable+0x38, see
// FUN_FRAME_ON_UPDATE), `void __thiscall(smf, float elapsed)`, `ret 4`:
// FUN_FRAME_ON_UPDATE (OnUpdate), then the fader.
FUN_SCROLLINGMESSAGEFRAME_ON_UPDATE = 0x00788460,
// The line layout (FUN_SMF_DISPLAY_REFRESH) anchors a line once, when it
// creates the line's font string, through the one SetPoint call at
// CALL_SMF_LAYOUT_ANCHOR_LINE (`E8 rel32` to FUN_REGION_SET_POINT; ESI the
// frame, EDI the font string, its parent at OFF_REGION_PARENT already set
// by SetRegionParent 0x0077FD10): the first line (BOTTOMLEFT, the frame,
// BOTTOMLEFT, left inset, bottom inset), each next one (BOTTOMLEFT, the
// line before, TOPLEFT, 0, FUN_PIXEL_ALIGN_Y(spacing)), then
// FUN_LAYOUT_RESIZE(1). The layout is the only maker of lines (the array
// grow 0x00789060 and the node constructor 0x00788D90 have no other
// caller). OnRectChanged (0x00788310) destroys every line when the message
// area changes size and lays them out again; Clear (0x007882B0) only hides
// them. The insets are the four floats SetInsets (0x00787FB0) stores and
// OnRectChanged subtracts from the rect: bottom, left, top, right.
CALL_SMF_LAYOUT_ANCHOR_LINE = 0x00788975,
OFF_SCROLLINGMESSAGEFRAME_INSET_BOTTOM = 0x37C,
OFF_SCROLLINGMESSAGEFRAME_INSET_LEFT = 0x380,
OFF_SCROLLINGMESSAGEFRAME_INSET_TOP = 0x384,
OFF_FONTSTRING_SPACING = 0xF4, // float, the line spacing
// PixelAlignY - `float __stdcall(float)`, `ret 4`: rounds a vertical
// length up to whole device pixels (benilla-re ui/regions).
FUN_PIXEL_ALIGN_Y = 0x00766750,
// Focused-editbox global — holds the CSimpleEditBox that currently has
// keyboard focus (the one showing a cursor), or 0 when no input field is
@@ -12972,6 +13093,51 @@ enum Offsets {
// ending at 0x0077D49D.
PATCH_EDITBOX_TEXT_CHANGED_FIRE = 0x0077D485,
OFF_EDITBOX_ONTEXTCHANGED_SLOT = 0x418,
// --- Edit box rows (EditBox::IconRows) ---
// The mode flags byte (benilla-re ui/editbox): bit 0 autoFocus, 1
// multiLine, 2 numeric, 3 password, 4 ignoreArrows. Every row placer
// below tests it (`TEST byte ptr [ESI+0x318], 0x2 / 0x8`). Not the dirty
// word OFF_EDITBOX_FLAGS (+0x31c).
OFF_EDITBOX_MODE_FLAGS = 0x318,
EDITBOX_MODE_MULTILINE = 0x02,
EDITBOX_MODE_PASSWORD = 0x08,
// The password copy of the text (every byte '*'), the buffer the label
// and the rows read with EDITBOX_MODE_PASSWORD set (4.3.4's
// m_textHidden); OFF_EDITBOX_TEXT otherwise. RebuildLabel 0x0077D4D0
// refreshes it.
OFF_EDITBOX_TEXT_MASKED = 0x334,
// The visible window in bytes: its first byte and its length (4.3.4's
// m_visiblePosition / m_visibleLength). PlaceCaret hides the caret and
// fires nothing for a cursor outside it (0x0077DA89..0x0077DAA7).
OFF_EDITBOX_VISIBLE_START = 0x348,
OFF_EDITBOX_VISIBLE_LENGTH = 0x34C,
// The caret texture (4.3.4's m_cursorTexture), anchored by PlaceCaret.
OFF_EDITBOX_CARET = 0x368,
// OnCursorChanged's script slot; PlaceCaret fires it when non-zero
// (0x0077DD8C).
OFF_EDITBOX_ONCURSORCHANGED_SLOT = 0x428,
// The three row placers, all `__thiscall`: PlaceCaret (no args; 4.3.4's
// UpdateVisibleCursor), PlaceSelectionPiece `(editbox, texture, from, to)`
// `ret 0xC` (UpdateHighlightArea) and GetIndexAtPoint `(editbox, float x,
// float y, int *index)` `ret 0xC` (ConvertCoordinateToIndex). Each steps
// rows of FUN_PIXEL_ALIGN_Y(fontH × scale) + FUN_PIXEL_ALIGN_Y(spacing ×
// scale), the first two divided by the box's OFF_LAYOUT_SCALE (benilla-re
// ui/editbox "The caret" / "Selection"; Ghidra). Co-hooked by
// EditBox::IconRows: tall inline icons move the rows.
FUN_EDITBOX_PLACE_CARET = 0x0077DA80,
FUN_EDITBOX_PLACE_SELECTION_PIECE = 0x0077DE70,
FUN_EDITBOX_INDEX_AT_POINT = 0x0077D0D0,
// "%f%f%f%f" (.data; verified in the file at 0x479CB8) — the format
// PlaceCaret fires OnCursorChanged with through
// FUN_FRAME_RUN_SCRIPT_VARIADIC: x, y, width, height, each a double of
// FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X(FUN_COORD_GET_SCALED_SCREEN_ASPECT()
// × [FLOAT_SCRIPT_COORD_SCALE] × value) — 4.3.4's
// RunOnCursorChangedScript (0x0077DDA0..0x0077DE20).
VAR_SCRIPT_FMT_FFFF = 0x00879CB8,
// 1024.0f (.rdata; verified in the file at 0x3FFD68): the anchor→script
// push factor every script coordinate push multiplies by (the `1024` of
// the GetFont note at FUN_COORD_GET_SCALED_SCREEN_ASPECT).
FLOAT_SCRIPT_COORD_SCALE = 0x007FFD68,
// `void __thiscall(editbox, int maxLines)` — the SetHistoryLines internal
// (FUN_0077cd90). Reallocates the history ring to maxLines; maxLines == 0
// frees the whole buffer and zeroes the container. ClearHistory calls it
@@ -12989,7 +13155,8 @@ enum Offsets {
// core FUN_0044D670(fontHandle fs+0xE0, text, len, fontH*[fs+0x7C], &out,
// fs+0x108, 0, flags fs+0x120), then `fs+0xFC = out / [fs+0x7C]`. Exactly 4
// callers: Script_GetStringWidth (0x0079E510), GameTooltip auto-size
// (0x00530640), the editbox caret positioner (0x0077DE70), and the layout
// (0x00530640), the editbox selection-piece placer (0x0077DE70, a whole
// last line's width), and the layout
// effective-width vmethod (FUN_00772930 = fs+0x24 vtbl slot +0x1C: explicit
// rect width else string width). Prologue PUSH EBX; PUSH ESI; MOV ESI,ECX;
// FLD [ESI+0xFC] — MinHook-safe. Co-hooked by Text::InlineTexture to add
+190 -14
View File
@@ -702,40 +702,216 @@ const Game::Doc::Function kCancelUnitBuff{
"Cancels one of your own auras by index.", kCancelUnitBuffArgs, {},
"SpellGlobals"};
const Game::Doc::Field kAuraDataFields[] = {
Game::Doc::Req("name", "string"),
Game::Doc::Req("icon", "string", "The icon's texture path."),
Game::Doc::Req("applications", "number", "The stack count."),
Game::Doc::Req("spellId", "number"),
Game::Doc::Req("dispelName", "string",
"Magic, Curse, Disease, or Poison; empty when the aura has no dispel type."),
Game::Doc::Req("isHelpful", "bool"),
Game::Doc::Req("isHarmful", "bool"),
Game::Doc::Req("duration", "number", "In seconds; 0 when the duration is not known."),
Game::Doc::Req("expirationTime", "number",
"When the aura ends, in GetTime() seconds; 0 when not known."),
Game::Doc::Opt("sourceUnit", "UnitToken", nullptr,
"Nil when the cast was not seen or the caster has no unit token."),
Game::Doc::Opt("sourceGUID", "WOWGUID", nullptr,
"The caster's GUID, a ClassicAPI extension; nil when the cast was not seen."),
Game::Doc::Opt("auraInstanceID", "number", nullptr,
"Nil for an aura that is not in the unit's aura list."),
Game::Doc::Req("charges", "number", "Always 0."),
Game::Doc::Req("maxCharges", "number", "Always 0."),
Game::Doc::Req("timeMod", "number", "Always 1."),
Game::Doc::Req("isFromPlayerOrPlayerPet", "bool",
"True when any player or a player's pet applied the aura."),
Game::Doc::Req("canApplyAura", "bool",
"True when the player or the pet knows the spell that applies the aura."),
Game::Doc::Req("isRaid", "bool", "True when the aura passes the RAID filter."),
Game::Doc::Req("isStealable", "bool",
"True when the player can remove this buff from an enemy."),
Game::Doc::Req("isBossAura", "bool", "Always false."),
Game::Doc::Req("isNameplateOnly", "bool", "Always false."),
Game::Doc::Req("nameplateShowAll", "bool", "Always false."),
Game::Doc::Req("nameplateShowPersonal", "bool", "Always false."),
Game::Doc::Req("shouldConsolidate", "bool", "Always false."),
};
const Game::Doc::Structure kAuraData{"AuraData", "UnitAuras", kAuraDataFields,
"One aura on a unit."};
// The Classic Era `UnitAura` tuple, shared by the four positional getters.
// `Data::PushPositional` is the source of the order.
const Game::Doc::Field kPositionalRets[] = {
Game::Doc::Opt("name", "string", nullptr,
"Nil, as the only return, when there is no such aura."),
Game::Doc::Opt("icon", "string"),
Game::Doc::Opt("count", "number"),
Game::Doc::Opt("dispelType", "string", nullptr,
"Empty when the aura has no dispel type."),
Game::Doc::Opt("duration", "number"),
Game::Doc::Opt("expirationTime", "number"),
Game::Doc::Opt("source", "UnitToken", nullptr, "Nil when the caster is not known."),
Game::Doc::Opt("isStealable", "bool"),
Game::Doc::Opt("nameplateShowPersonal", "bool", nullptr, "Always false."),
Game::Doc::Opt("spellId", "number"),
Game::Doc::Opt("canApplyAura", "bool"),
Game::Doc::Opt("isBossDebuff", "bool", nullptr, "Always false."),
Game::Doc::Opt("castByPlayer", "bool", nullptr,
"True when a player applied the aura; false for a pet."),
Game::Doc::Opt("nameplateShowAll", "bool", nullptr, "Always false."),
Game::Doc::Opt("timeMod", "number", nullptr, "Always 1."),
};
const Game::Doc::Field kAuraRets[] = {
Game::Doc::Opt("aura", "AuraData", nullptr, "Nil when there is no such aura."),
};
const Game::Doc::Field kByIndexArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("index", "luaIndex"),
Game::Doc::Opt("filter", "AuraFilters", nullptr,
"Tokens such as HELPFUL, HARMFUL, PLAYER, RAID; HELPFUL when no range is given."),
};
const Game::Doc::Function kGetAuraDataByIndex{
"Returns the aura at an index among a unit's auras that match a filter.", kByIndexArgs,
kAuraRets};
const Game::Doc::Function kUnitAura{
"Returns the aura at an index as positional values, without making a table.",
kByIndexArgs, kPositionalRets};
const Game::Doc::Field kLockedIndexArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("index", "luaIndex"),
};
const Game::Doc::Function kGetBuffDataByIndex{
"Returns the buff at an index on a unit.", kLockedIndexArgs, kAuraRets};
const Game::Doc::Function kGetDebuffDataByIndex{
"Returns the debuff at an index on a unit.", kLockedIndexArgs, kAuraRets};
const Game::Doc::Field kLockedFilterArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("index", "luaIndex"),
Game::Doc::Opt("filter", "AuraFilters", nullptr,
"Further tokens such as PLAYER or RAID; the range is fixed."),
};
const Game::Doc::Function kUnitBuff{
"Returns the buff at an index as positional values, without making a table.",
kLockedFilterArgs, kPositionalRets};
const Game::Doc::Function kUnitDebuff{
"Returns the debuff at an index as positional values, without making a table.",
kLockedFilterArgs, kPositionalRets};
const Game::Doc::Field kSlotsArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Opt("filter", "AuraFilters"),
Game::Doc::Opt("maxSlots", "number", nullptr, "Nil or 0 returns all of them."),
Game::Doc::Opt("continuationToken", "number", nullptr,
"The first return of the previous call, to get the next batch."),
Game::Doc::Opt("buf", "table", nullptr,
"A table to fill with the slot IDs instead; the call then returns the "
"token and the count. A ClassicAPI extension."),
};
const Game::Doc::Field kSlotsRets[] = {
Game::Doc::Opt("outContinuationToken", "number", nullptr,
"Nil when this batch reaches the last aura."),
Game::Doc::Vararg("number", "The slot IDs, valid only in the frame they are returned."),
};
const Game::Doc::Function kGetAuraSlots{
"Returns the slot IDs of a unit's auras that match a filter, in batches.", kSlotsArgs,
kSlotsRets};
const Game::Doc::Field kBySlotArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("slot", "number", "A slot ID from C_UnitAuras.GetAuraSlots."),
};
const Game::Doc::Function kGetAuraDataBySlot{
"Returns the aura that a slot ID names.", kBySlotArgs, kAuraRets};
const Game::Doc::Function kUnitAuraBySlot{
"Returns the aura that a slot ID names as positional values, without making a table.",
kBySlotArgs, kPositionalRets};
const Game::Doc::Field kBySpellIDArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("spellID", "number"),
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
};
const Game::Doc::Function kGetUnitAuraBySpellID{
"Returns the first aura on a unit with a spell ID; the player's own aura comes first.",
kBySpellIDArgs, kAuraRets};
const Game::Doc::Field kPlayerBySpellIDArgs[] = {
Game::Doc::Req("spellID", "number"),
};
const Game::Doc::Function kGetPlayerAuraBySpellID{
"Returns the first aura on the player with a spell ID.", kPlayerBySpellIDArgs, kAuraRets};
const Game::Doc::Field kBySpellNameArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("spellName", "cstring", "The localized name; case does not matter."),
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
};
const Game::Doc::Function kGetAuraDataBySpellName{
"Returns the first aura on a unit with a spell name; the player's own aura comes first.",
kBySpellNameArgs, kAuraRets};
const Game::Doc::Field kUnitAurasArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Opt("filter", "AuraFilters", nullptr,
"With no HELPFUL or HARMFUL token, both are returned."),
};
const Game::Doc::Field kUnitAurasRets[] = {
Game::Doc::Req("auras", "table", "An array of AuraData; empty when the unit has none."),
};
const Game::Doc::Function kGetUnitAuras{
"Returns all of a unit's auras that match a filter, buffs first.", kUnitAurasArgs,
kUnitAurasRets};
const Game::Doc::Field kDispelColorArgs[] = {
Game::Doc::Opt("dispelName", "string", nullptr,
"Magic, Curse, Disease, Poison, or Enrage; any other value gives the "
"no-type color."),
};
const Game::Doc::Field kDispelColorRets[] = {
Game::Doc::Req("color", "colorRGBA"),
};
const Game::Doc::Function kGetAuraDispelTypeColor{
"Returns the border color for an aura's dispel type.", kDispelColorArgs,
kDispelColorRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByIndex",
&Script_GetAuraDataByIndex);
&Script_GetAuraDataByIndex, &kGetAuraDataByIndex);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetBuffDataByIndex",
&Script_GetBuffDataByIndex);
&Script_GetBuffDataByIndex, &kGetBuffDataByIndex);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetDebuffDataByIndex",
&Script_GetDebuffDataByIndex);
&Script_GetDebuffDataByIndex, &kGetDebuffDataByIndex);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAura",
&Script_UnitAura);
&Script_UnitAura, &kUnitAura);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitBuff",
&Script_UnitBuff);
&Script_UnitBuff, &kUnitBuff);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitDebuff",
&Script_UnitDebuff);
&Script_UnitDebuff, &kUnitDebuff);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraSlots",
&Script_GetAuraSlots);
&Script_GetAuraSlots, &kGetAuraSlots);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySlot",
&Script_GetAuraDataBySlot);
&Script_GetAuraDataBySlot, &kGetAuraDataBySlot);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByAuraInstanceID",
&Script_GetAuraDataByAuraInstanceID,
&kGetAuraDataByAuraInstanceID);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAuraBySlot",
&Script_UnitAuraBySlot);
&Script_UnitAuraBySlot, &kUnitAuraBySlot);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuraBySpellID",
&Script_GetUnitAuraBySpellID);
&Script_GetUnitAuraBySpellID, &kGetUnitAuraBySpellID);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetPlayerAuraBySpellID",
&Script_GetPlayerAuraBySpellID);
&Script_GetPlayerAuraBySpellID, &kGetPlayerAuraBySpellID);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySpellName",
&Script_GetAuraDataBySpellName);
&Script_GetAuraDataBySpellName, &kGetAuraDataBySpellName);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuras",
&Script_GetUnitAuras);
&Script_GetUnitAuras, &kGetUnitAuras);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDispelTypeColor",
&Script_GetAuraDispelTypeColor);
&Script_GetAuraDispelTypeColor, &kGetAuraDispelTypeColor);
Game::Lua::RegisterGlobalFunction("CancelUnitBuff", &Script_CancelUnitBuff,
&kCancelUnitBuff);
}
+15 -1
View File
@@ -193,9 +193,23 @@ int __fastcall Script_RegisterComboDuration(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("spellID", "number"),
Game::Doc::Req("baseSeconds", "number",
"The duration at zero combo points; each point adds a fifth of the "
"difference to maxSeconds."),
Game::Doc::Req("maxSeconds", "number", "The duration at five combo points."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("success", "bool"),
};
const Game::Doc::Function kRegisterComboDuration{
"Sets the combo point durations of a finisher whose client data is missing or wrong.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_UnitAuras", "RegisterComboDuration",
&Script_RegisterComboDuration);
&Script_RegisterComboDuration, &kRegisterComboDuration);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+22 -1
View File
@@ -946,10 +946,31 @@ int __fastcall Script_RegisterAuraDurationModifierByTrigger(void *L) {
return 1;
}
const Game::Doc::Field kDurationModArgs[] = {
Game::Doc::Req("triggerFamily", "number", "The SpellFamilyName of the triggering cast."),
Game::Doc::Req("triggerSchool", "number",
"The school index the trigger must have, or below 0 for any school."),
Game::Doc::Req("affectedFamily", "number", "The SpellFamilyName of the aura to change."),
Game::Doc::Req("affectedFamilyFlags", "number",
"A SpellFamilyFlags mask the aura must overlap, or 0 to match on the icon."),
Game::Doc::Req("affectedIcon", "number",
"The SpellIconID the aura must have, or 0 for any icon."),
Game::Doc::Req("op", "string", "refresh, reduce, set, or remove."),
Game::Doc::Opt("valueSeconds", "number", nullptr, "The amount for reduce and set."),
};
const Game::Doc::Field kDurationModRets[] = {
Game::Doc::Req("success", "bool"),
};
const Game::Doc::Function kRegisterAuraDurationModifierByTrigger{
"Changes the shown duration of the caster's aura on a unit when a matching cast by "
"that caster lands.",
kDurationModArgs, kDurationModRets, nullptr, true};
void RegisterDurationModLua() {
Game::Lua::RegisterTableFunction("C_UnitAuras",
"RegisterAuraDurationModifierByTrigger",
&Script_RegisterAuraDurationModifierByTrigger);
&Script_RegisterAuraDurationModifierByTrigger,
&kRegisterAuraDurationModifierByTrigger);
}
const Game::ModuleAutoRegister _autoregDurationMod{&RegisterDurationModLua};
+8 -10
View File
@@ -42,6 +42,7 @@
#include "combatlog/RecentCasts.h"
#include "net/PacketDispatch.h"
#include "object/Resolve.h"
#include "spell/Lookup.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -54,13 +55,9 @@ using Net::CDataStore;
using Net::Read;
using Net::ReadPackedGuid;
constexpr uint32_t SMSG_SPELL_START = 0x131;
constexpr uint32_t SMSG_SPELL_GO = 0x132;
// The cast flag that keeps a cast out of the combat log (both servers'
// CAST_FLAG_HIDDEN_COMBATLOG); 4.3.4 tests the same bit.
constexpr uint16_t CAST_FLAG_HIDDEN_COMBATLOG = 0x1;
constexpr uint16_t TARGET_FLAG_OBJECT = 0x8A02;
constexpr uint8_t SPELL_MISS_REFLECT = 11;
// 4.3.4's SpellHasOpenLockEffect tests 33 alone; 1.12's success line also
@@ -85,7 +82,7 @@ int CastingTime(void *unit, int spellId) {
// The rules both cast builders share.
bool Logged(const uint8_t *spell) {
return !Hidden(spell) && !HasEffect(spell, SPELL_EFFECT_LEARN_SPELL) &&
return !Hidden(spell) && !Spell::Lookup::HasEffect(spell, SPELL_EFFECT_LEARN_SPELL) &&
(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX4) &
ATTR_EX4_NO_CAST_LOG) == 0;
}
@@ -127,7 +124,8 @@ void SpellGo(CDataStore *p) {
Read<uint8_t>(p);
}
const uint16_t targetMask = Read<uint16_t>(p);
const uint64_t target = (targetMask & TARGET_FLAG_OBJECT) ? ReadPackedGuid(p) : 0;
const uint64_t target =
(targetMask & Offsets::SPELL_TARGET_FLAGS_OBJECT) ? ReadPackedGuid(p) : 0;
if (Overrun(p))
return;
RecentCasts::Record(caster, static_cast<uint32_t>(spellId), hitList, hits);
@@ -135,17 +133,17 @@ void SpellGo(CDataStore *p) {
return;
void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, caster);
const uint8_t *spell = SpellRecord(spellId);
if (unit == nullptr || !Logged(spell) || HasEffect(spell, SPELL_EFFECT_OPEN_LOCK) ||
CastingTime(unit, spellId) != 0)
if (unit == nullptr || !Logged(spell) ||
Spell::Lookup::HasEffect(spell, SPELL_EFFECT_OPEN_LOCK) || CastingTime(unit, spellId) != 0)
return;
Entry e = Begin(caster, target, SPELL_CAST_SUCCESS, spell);
Queue(e);
}
void OnPacket(uint32_t opcode, CDataStore *p) {
if (opcode == SMSG_SPELL_START)
if (opcode == Offsets::SMSG_SPELL_START)
SpellStart(p);
else if (opcode == SMSG_SPELL_GO)
else if (opcode == Offsets::SMSG_SPELL_GO)
SpellGo(p);
}
-10
View File
@@ -609,16 +609,6 @@ bool Ranged(const uint8_t *spell) {
uint32_t SchoolMask(uint32_t school) { return school < kSchoolCount ? 1u << school : 0; }
bool HasEffect(const uint8_t *spell, int effect) {
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 + i * 4) == effect)
return true;
}
return false;
}
bool HasAuraEffect(const uint8_t *spell, int auraName) {
if (spell == nullptr)
return false;
-3
View File
@@ -158,9 +158,6 @@ bool Ranged(const uint8_t *spell);
// A school index (Spell.dbc School, the packets' school) as a school mask.
uint32_t SchoolMask(uint32_t school);
// Whether one of the spell's three effects is `effect`.
bool HasEffect(const uint8_t *spell, int effect);
// Whether one of the spell's three effects applies the aura `auraName`
// (EffectApplyAuraName).
bool HasAuraEffect(const uint8_t *spell, int auraName);
+2 -1
View File
@@ -14,6 +14,7 @@
#include "combatlog/RecentCasts.h"
#include "combatlog/Entry.h"
#include "spell/Lookup.h"
#include "time/Clock.h"
#include <cstdint>
@@ -80,7 +81,7 @@ const uint8_t *SpellByEffect(uint64_t caster, uint64_t target, int effectA, int
if (c.caster != caster || !Hit(c, target))
return false;
const uint8_t *spell = SpellRecord(static_cast<int>(c.spellId));
if (!HasEffect(spell, effectA) && !HasEffect(spell, effectB))
if (!Spell::Lookup::HasEffect(spell, effectA) && !Spell::Lookup::HasEffect(spell, effectB))
return false;
found = spell;
return true;
+15 -1
View File
@@ -62,9 +62,23 @@ int __fastcall Script_C_Container_AutoStoreItem(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("srcBag", "BagIndex"),
Game::Doc::Req("srcSlot", "luaIndex"),
Game::Doc::Opt("dstBag", "BagIndex", "0",
"0 for anywhere in the bags, 1 to 4 for one bag, -1 for the bank."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("sent", "bool", "False when the server cannot be asked for this move."),
};
const Game::Doc::Function kAutoStoreItem{
"Moves an item and lets the server choose where it goes, merging it into "
"existing stacks first.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "AutoStoreItem",
&Script_C_Container_AutoStoreItem);
&Script_C_Container_AutoStoreItem, &kAutoStoreItem);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+13 -1
View File
@@ -43,9 +43,21 @@ int __fastcall Script_C_Container_GetContainerItemCharges(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("uses", "number", nullptr, "Nil when the slot is empty."),
};
const Game::Doc::Function kGetContainerItemCharges{
"Returns the uses left on the item in a bag slot: its charges times its stack size.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemCharges",
&Script_C_Container_GetContainerItemCharges);
&Script_C_Container_GetContainerItemCharges,
&kGetContainerItemCharges);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+14 -1
View File
@@ -33,9 +33,22 @@ int __fastcall Script_C_Container_GetItemCooldown(void *L) {
return 3;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("itemID", "number"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("startTime", "number",
"When the cooldown started, in GetTime() seconds; 0 when none."),
Game::Doc::Req("duration", "number", "Length in seconds; 0 when there is no cooldown."),
Game::Doc::Req("enable", "number",
"0 while the cooldown waits to start, as for a potion used in combat; else 1."),
};
const Game::Doc::Function kGetItemCooldown{
"Returns the cooldown of an item's use effect.", kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetItemCooldown",
&Script_C_Container_GetItemCooldown);
&Script_C_Container_GetItemCooldown, &kGetItemCooldown);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+14 -1
View File
@@ -40,9 +40,22 @@ int __fastcall Script_C_Container_GetContainerItemDurability(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("durability", "number", nullptr,
"Nil when the slot is empty or the item has no durability."),
Game::Doc::Opt("maxDurability", "number"),
};
const Game::Doc::Function kGetContainerItemDurability{
"Returns the current and maximum durability of the item in a bag slot.", kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemDurability",
&Script_C_Container_GetContainerItemDurability);
&Script_C_Container_GetContainerItemDurability,
&kGetContainerItemDurability);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+16 -1
View File
@@ -80,10 +80,25 @@ int __fastcall Script_C_Container_GetContainerItemEquipmentSetInfo(void *L) {
return 2;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("inSet", "bool"),
Game::Doc::Opt("setList", "string", nullptr,
"The set names joined by PLAYER_LIST_DELIMITER; nil when inSet is false."),
};
const Game::Doc::Function kGetContainerItemEquipmentSetInfo{
"Returns whether the item in a bag slot is saved in an equipment set, and the "
"names of those sets.",
kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction(
"C_Container", "GetContainerItemEquipmentSetInfo",
&Script_C_Container_GetContainerItemEquipmentSetInfo);
&Script_C_Container_GetContainerItemEquipmentSetInfo,
&kGetContainerItemEquipmentSetInfo);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+33 -3
View File
@@ -182,15 +182,45 @@ int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
return 1;
}
const Game::Doc::Field kBagArgs[] = {
Game::Doc::Req("bagIndex", "BagIndex", "0 for the backpack, 1 to 4 for the equipped bags."),
};
const Game::Doc::Field kNumFreeSlotsRets[] = {
Game::Doc::Req("numFreeSlots", "number", "0 when the bag does not exist."),
Game::Doc::Req("bagFamily", "number",
"The bag's family bitmask; 0 for a bag that takes any item."),
};
const Game::Doc::Function kGetContainerNumFreeSlots{
"Returns the number of empty slots in a bag and the kind of items the bag takes.",
kBagArgs, kNumFreeSlotsRets};
const Game::Doc::Field kFreeSlotsRets[] = {
Game::Doc::Opt("freeSlots", "table", nullptr,
"The empty slot numbers in ascending order; nil when the bag does not exist."),
};
const Game::Doc::Function kGetContainerFreeSlots{
"Returns the numbers of the empty slots in a bag.", kBagArgs, kFreeSlotsRets};
const Game::Doc::Field kTotalRets[] = {
Game::Doc::Req("totalFreeSlots", "number"),
};
const Game::Doc::Function kCalculateTotalNumberOfFreeBagSlots{
"Returns the number of empty slots in the backpack and the equipped bags that take "
"any item.",
{}, kTotalRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerNumFreeSlots",
&Script_C_Container_GetContainerNumFreeSlots);
&Script_C_Container_GetContainerNumFreeSlots,
&kGetContainerNumFreeSlots);
Game::Lua::RegisterTableFunction("C_Container", "GetContainerFreeSlots",
&Script_C_Container_GetContainerFreeSlots);
&Script_C_Container_GetContainerFreeSlots,
&kGetContainerFreeSlots);
Game::Lua::RegisterTableFunction("C_Container", "CalculateTotalNumberOfFreeBagSlots",
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots);
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots,
&kCalculateTotalNumberOfFreeBagSlots);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+12 -1
View File
@@ -40,9 +40,20 @@ int __fastcall Script_C_Container_HasContainerItem(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("hasItem", "bool"),
};
const Game::Doc::Function kHasContainerItem{
"Returns whether a bag slot holds an item, even one whose data is not cached yet.",
kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "HasContainerItem",
&Script_C_Container_HasContainerItem);
&Script_C_Container_HasContainerItem, &kHasContainerItem);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+16 -2
View File
@@ -117,13 +117,27 @@ int __fastcall Script_C_Container_UseHearthstone(void *L) {
return 1;
}
const Game::Doc::Field kHasRets[] = {
Game::Doc::Opt("itemID", "number", nullptr, "Nil when no hearthstone is in the bags."),
};
const Game::Doc::Function kPlayerHasHearthstone{
"Returns the ID of the hearthstone in the player's bags.", {}, kHasRets};
const Game::Doc::Field kUseRets[] = {
Game::Doc::Req("used", "bool",
"True when a hearthstone was found; the cast itself can still fail."),
};
const Game::Doc::Function kUseHearthstone{"Uses the hearthstone in the player's bags.", {},
kUseRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "PlayerHasHearthstone",
&Script_C_Container_PlayerHasHearthstone);
&Script_C_Container_PlayerHasHearthstone,
&kPlayerHasHearthstone);
Game::Lua::RegisterTableFunction("C_Container", "UseHearthstone",
&Script_C_Container_UseHearthstone);
&Script_C_Container_UseHearthstone, &kUseHearthstone);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+11 -1
View File
@@ -41,9 +41,19 @@ int __fastcall Script_C_Container_GetContainerItemID(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("itemID", "number", nullptr, "Nil when the slot is empty."),
};
const Game::Doc::Function kGetContainerItemID{"Returns the ID of the item in a bag slot.",
kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemID",
&Script_C_Container_GetContainerItemID);
&Script_C_Container_GetContainerItemID, &kGetContainerItemID);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+16 -2
View File
@@ -57,9 +57,23 @@ int __fastcall Script_C_Container_MoveItem(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("srcBag", "BagIndex"),
Game::Doc::Req("srcSlot", "luaIndex"),
Game::Doc::Req("dstBag", "BagIndex"),
Game::Doc::Req("dstSlot", "luaIndex"),
Game::Doc::Req("count", "number", "From 1 to 255."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("sent", "bool", "False when an argument is not valid."),
};
const Game::Doc::Function kMoveItem{
"Moves part of a stack to another bag slot without the cursor.", kArgs, kRets, nullptr,
true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "MoveItem",
&Script_C_Container_MoveItem);
Game::Lua::RegisterTableFunction("C_Container", "MoveItem", &Script_C_Container_MoveItem,
&kMoveItem);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+16 -1
View File
@@ -37,9 +37,24 @@ int __fastcall Script_C_Container_IsContainerItemOpenable(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("isOpenable", "bool", nullptr,
"Nil when the slot is empty or the item's data is not cached yet."),
Game::Doc::Opt("canOpen", "bool", nullptr, "False while the item is still locked."),
};
const Game::Doc::Function kIsContainerItemOpenable{
"Returns whether the item in a bag slot can be opened, and whether the player can "
"open it now.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "IsContainerItemOpenable",
&Script_C_Container_IsContainerItemOpenable);
&Script_C_Container_IsContainerItemOpenable,
&kIsContainerItemOpenable);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+21 -1
View File
@@ -97,9 +97,29 @@ int __fastcall Script_C_Container_GetContainerItemQuestInfo(void *L) {
return 1;
}
const Game::Doc::Field kItemQuestInfoFields[] = {
Game::Doc::Req("isQuestItem", "bool"),
Game::Doc::Opt("questID", "number", nullptr,
"The quest the item begins; nil when it begins none."),
Game::Doc::Req("isActive", "bool", "True when the quest in questID is in the quest log."),
};
const Game::Doc::Structure kItemQuestInfo{"ItemQuestInfo", "Container", kItemQuestInfoFields};
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("questInfo", "ItemQuestInfo",
"Always a table; an empty slot reports no quest."),
};
const Game::Doc::Function kGetContainerItemQuestInfo{
"Returns how the item in a bag slot relates to quests.", kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemQuestInfo",
&Script_C_Container_GetContainerItemQuestInfo);
&Script_C_Container_GetContainerItemQuestInfo,
&kGetContainerItemQuestInfo);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+13 -1
View File
@@ -37,9 +37,21 @@ int __fastcall Script_C_Container_GetContainerItemRepairCost(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("repairCost", "number", "In copper; 0 when the item needs no repair."),
};
const Game::Doc::Function kGetContainerItemRepairCost{
"Returns what a merchant charges to repair the item in a bag slot.", kArgs, kRets,
nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemRepairCost",
&Script_C_Container_GetContainerItemRepairCost);
&Script_C_Container_GetContainerItemRepairCost,
&kGetContainerItemRepairCost);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+14 -2
View File
@@ -49,9 +49,21 @@ int __fastcall Script_C_Container_SwapItems(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("srcBag", "BagIndex"),
Game::Doc::Req("srcSlot", "luaIndex"),
Game::Doc::Req("dstBag", "BagIndex"),
Game::Doc::Req("dstSlot", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("sent", "bool", "False when an argument is not valid."),
};
const Game::Doc::Function kSwapItems{
"Swaps the contents of two bag slots without the cursor.", kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "SwapItems",
&Script_C_Container_SwapItems);
Game::Lua::RegisterTableFunction("C_Container", "SwapItems", &Script_C_Container_SwapItems,
&kSwapItems);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+218
View File
@@ -0,0 +1,218 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_DeathInfo.GetSelfResurrectOptions()`, `C_DeathInfo.UseSelfResurrectOption
// (optionType, id)` and `SELF_RES_SPELL_CHANGED` — the self-resurrection that
// 1.12 serves through HasSoulstone (0x0048AC80) and UseSoulstone (0x0048AD70).
//
// 1.12 offers at most one way for a dead player to raise themselves, and both
// functions pick it the same way: PLAYER_SELF_RES_SPELL when the server set it
// (Soulstone Resurrection, Reincarnation), else the first carried item whose
// on-use spell is a self-resurrect. So the list holds that one option, under
// HasSoulstone's gate (a living player has none), and using it calls
// UseSoulstone itself. Nothing in 1.12 limits a self-resurrection, so every
// option can be used and none is limited.
//
// SELF_RES_SPELL_CHANGED is 4.3.4's: CGPlayer_C::SetActiveMirrorHandlers
// watches PLAYER_SELF_RES_SPELL with OnUpdatePlayerScriptEvent. 1.12 watches
// no such field, so the same watcher joins 1.12's AddActiveWatchers and
// leaves with its removal, the lifetime of the engine's own player watchers.
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "item/Link.h"
#include "spell/Lookup.h"
#include "unit/Identity.h"
#include <cstdint>
namespace Death::SelfResurrect {
namespace {
constexpr int OPTION_TYPE_SPELL = 0;
constexpr int OPTION_TYPE_ITEM = 1;
const Game::Doc::Event kChangedDoc{
"DeathInfo",
"The player's self-resurrection spell changed. Read C_DeathInfo.GetSelfResurrectOptions.",
{}};
const Event::Custom::AutoReserve kChanged{"SELF_RES_SPELL_CHANGED", &kChangedDoc};
struct Option {
const char *name;
int optionType;
int id;
};
// HasSoulstone's answer: the option, or false while the player lives or has
// none. `itemName` holds an item option's name.
bool CurrentOption(Option &out, char *itemName, size_t itemNameSize) {
const uint8_t *player = Unit::Identity::PlayerObject();
const uint8_t *desc = Unit::Identity::PlayerDescriptor();
if (player == nullptr || desc == nullptr ||
Game::Read<int32_t>(desc, Offsets::OFF_UNIT_FIELD_HEALTH) > 0)
return false;
const auto *info = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO);
const int spellID =
info != nullptr ? Game::Read<int32_t>(info, Offsets::OFF_CGPLAYER_INFO_SELF_RES_SPELL) : 0;
if (spellID != 0) {
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
out = {rec != nullptr
? Game::Read<const char *>(rec, Offsets::OFF_SPELL_NAMES +
Game::Read<int>(Offsets::VAR_LOCALE_INDEX) *
4)
: "UNKNOWN",
OPTION_TYPE_SPELL, spellID};
return true;
}
using SelfResItem_t = const uint8_t *(__fastcall *)(const void *player);
const uint8_t *item =
reinterpret_cast<SelfResItem_t>(Offsets::FUN_PLAYER_SELF_RES_ITEM)(player);
if (item == nullptr)
return false;
Item::Link::NameFromCGItem(item, itemName, itemNameSize);
const auto *fields = Game::Read<const uint8_t *>(item, Offsets::OFF_UNIT_GUID_PTR);
out = {itemName, OPTION_TYPE_ITEM,
fields != nullptr ? Game::Read<int32_t>(fields, Offsets::OFF_OBJECT_FIELD_ENTRY) : 0};
return true;
}
int __fastcall Script_GetSelfResurrectOptions(void *L) {
if (Unit::Identity::PlayerObject() == nullptr)
return 0;
Game::Lua::NewTable(L);
char itemName[0x400];
Option option;
if (CurrentOption(option, itemName, sizeof(itemName))) {
Game::Lua::PushNumber(L, 1);
Game::Lua::NewTable(L);
Game::Lua::SetFieldString(L, "name", option.name);
Game::Lua::SetFieldNumber(L, "optionType", option.optionType);
Game::Lua::SetFieldNumber(L, "id", option.id);
Game::Lua::SetFieldBool(L, "canUse", true);
Game::Lua::SetFieldBool(L, "isLimited", false);
Game::Lua::SetFieldNumber(L, "priority", 0);
Game::Lua::SetTable(L, -3);
}
return 1;
}
int __fastcall Script_UseSelfResurrectOption(void *L) {
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: C_DeathInfo.UseSelfResurrectOption(optionType, id)");
return 0;
}
char itemName[0x400];
Option option;
if (CurrentOption(option, itemName, sizeof(itemName)) &&
option.optionType == static_cast<int>(Game::Lua::ToNumber(L, 1)) &&
option.id == static_cast<int>(Game::Lua::ToNumber(L, 2)))
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_USE_SOULSTONE)(L);
return 0;
}
// Observer callback ABI per the FUN_DESC_OBSERVER_REGISTER block in
// Offsets.h: __fastcall(fieldOffset /*ecx*/, size /*edx*/, guidLo, guidHi,
// oldValues*, userArg), returns 1, callee cleans 0x10. It runs only when the
// field's bytes changed.
int __fastcall SelfResSpellChanged(uint32_t /*fieldOffset*/, uint32_t /*size*/,
uint32_t /*guidLo*/, uint32_t /*guidHi*/,
const void * /*oldValues*/, void * /*userArg*/) {
Event::Custom::Fire(kChanged.Slot(), "");
return 1;
}
using Register_t = void(__fastcall *)(int bank, uint32_t fieldOffset, uint32_t guidLo,
uint32_t guidHi, int size, const void *callback,
void *userArg1, void *userArg2);
using Unregister_t = void(__fastcall *)(int bank, uint32_t fieldOffset, uint32_t guidLo,
uint32_t guidHi, const void *callback, void *userArg1);
using Watchers_t = void(__fastcall *)(void *player, void *edx);
Watchers_t g_origAddWatchers = nullptr;
Watchers_t g_origRemoveWatchers = nullptr;
void __fastcall AddActiveWatchers_h(void *player, void *edx) {
g_origAddWatchers(player, edx);
const uint64_t guid = Unit::Identity::GuidForObject(player);
reinterpret_cast<Register_t>(Offsets::FUN_DESC_OBSERVER_REGISTER)(
Offsets::DESC_OBSERVER_BANK_PLAYER, Offsets::OFF_CGPLAYER_INFO_SELF_RES_SPELL,
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32), 4,
reinterpret_cast<const void *>(&SelfResSpellChanged), nullptr, nullptr);
}
// Removed before the original, while the registry is as the engine's own
// removals find it.
void __fastcall RemoveActiveWatchers_h(void *player, void *edx) {
const uint64_t guid = Unit::Identity::GuidForObject(player);
reinterpret_cast<Unregister_t>(Offsets::FUN_DESC_OBSERVER_UNREGISTER)(
Offsets::DESC_OBSERVER_BANK_PLAYER, Offsets::OFF_CGPLAYER_INFO_SELF_RES_SPELL,
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32),
reinterpret_cast<const void *>(&SelfResSpellChanged), nullptr);
g_origRemoveWatchers(player, edx);
}
const Game::HookAutoRegister _addHook{Offsets::FUN_PLAYER_ADD_ACTIVE_WATCHERS,
reinterpret_cast<void *>(&AddActiveWatchers_h),
reinterpret_cast<void **>(&g_origAddWatchers)};
const Game::HookAutoRegister _removeHook{Offsets::FUN_PLAYER_REMOVE_ACTIVE_WATCHERS,
reinterpret_cast<void *>(&RemoveActiveWatchers_h),
reinterpret_cast<void **>(&g_origRemoveWatchers)};
const Game::Lua::EnumIntegerEntry kOptionTypeEntries[] = {
{"Spell", OPTION_TYPE_SPELL},
{"Item", OPTION_TYPE_ITEM},
};
const Game::Doc::Field kOptionFields[] = {
Game::Doc::Req("name", "string"),
Game::Doc::Req("optionType", "SelfResurrectOptionType"),
Game::Doc::Req("id", "number", "The spell ID or item ID."),
Game::Doc::Req("canUse", "bool"),
Game::Doc::Req("isLimited", "bool"),
Game::Doc::Req("priority", "number", "Lower comes first."),
};
const Game::Doc::Structure kOption{"SelfResurrectOption", "DeathInfo", kOptionFields,
"A way for the dead player to resurrect themselves."};
const Game::Doc::Field kGetRets[] = {
Game::Doc::Req("options", "table", "Array of SelfResurrectOption; empty while alive."),
};
const Game::Doc::Function kGetSelfResurrectOptions{
"The ways the dead player can resurrect themselves.", {}, kGetRets};
const Game::Doc::Field kUseArgs[] = {
Game::Doc::Req("optionType", "SelfResurrectOptionType"),
Game::Doc::Req("id", "number"),
};
const Game::Doc::Function kUseSelfResurrectOption{
"Resurrects the dead player with one of the options from GetSelfResurrectOptions.",
kUseArgs, {}};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_DeathInfo", "GetSelfResurrectOptions",
&Script_GetSelfResurrectOptions, &kGetSelfResurrectOptions);
Game::Lua::RegisterTableFunction("C_DeathInfo", "UseSelfResurrectOption",
&Script_UseSelfResurrectOption, &kUseSelfResurrectOption);
Game::Lua::RegisterIntegerEnum("Enum", "SelfResurrectOptionType", kOptionTypeEntries,
sizeof(kOptionTypeEntries) / sizeof(kOptionTypeEntries[0]),
"DeathInfo");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Death::SelfResurrect
+308
View File
@@ -0,0 +1,308 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Tall inline icons move the edit box's rows.
//
// A multi-line edit box places its caret, its selection pieces and a click's
// cursor by rows of one height — the label's font height plus its spacing,
// pixel-aligned (FUN_EDITBOX_PLACE_CARET, FUN_EDITBOX_PLACE_SELECTION_PIECE,
// FUN_EDITBOX_INDEX_AT_POINT; 4.3.4's UpdateVisibleCursor, UpdateHighlightArea
// and ConvertCoordinateToIndex step the same rows). The label's text leaves
// those rows once a line holds an icon taller than the font: the render moves
// that line and every line after it down by the overflow (4.3.4's
// InitializeText; the emitter co-hook in Text::InlineTexture), and the box
// grows by the same amount through the string-height co-hook. So below the
// first tall-icon line the caret and the highlight sat above their text by
// the summed overflow, and OnCursorChanged reported that y, which
// ScrollingEditBoxMixin:ScrollCursorIntoView scrolls by — pfUI's chat copy
// box with faction-emblem icons. 4.3.4 has the same fault; nothing it ships
// puts a tall icon in an edit box.
//
// Each placer keeps its engine code and gets the move the render makes, from
// Text::InlineTexture::LineIconShift over the box's own line table (the
// breaks the render lays out), split by the label's vertical justify as the
// emitter splits it:
// - the click: its y is moved onto the uniform row that the icon-aware row
// under it has, before the engine maps it;
// - a selection piece: re-anchored after the engine placed it, with the
// engine's own x, lower by the move;
// - the caret: the same re-anchor, then OnCursorChanged fired once with the
// moved y through the engine's own forwarder. The engine's fire is muted
// for the call by emptying the script slot: it reads the row y before
// anything can correct it.
// A single-line box has one row and needs nothing: its label is justified
// MIDDLE in a rect of fixed height, so its text stays centred on the caret.
#include "Game.h"
#include "Offsets.h"
#include "text/InlineTexture.h"
#include <cstdint>
namespace EditBox::IconRows {
namespace {
using PlaceCaret_t = void(__fastcall *)(void *editBox);
using PlaceSelectionPiece_t = void(__fastcall *)(void *editBox, void *edx, void *piece, int from, int to);
using IndexAtPoint_t = void(__fastcall *)(void *editBox, void *edx, float x, float y, int *index);
using FontHeight_t = float(__fastcall *)(void *fs, void *edx, int mode);
using MeasureRun_t = float(__fastcall *)(void *fs, void *edx, const uint8_t *text, int len);
using PixelAlignY_t = float(__stdcall *)(float length);
using GetRect_t = int(__fastcall *)(void *anchor, void *edx, float *out);
using ClearAllPoints_t = void(__fastcall *)(void *anchor);
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint, float x, float y,
int flag);
using Resize_t = void(__fastcall *)(void *anchor, void *edx, int force);
using Extent_t = float(__fastcall *)(void *anchor, void *edx);
using ScreenAspect_t = float(__cdecl *)();
using DivideByScreenScaleX_t = float(__stdcall *)(float value);
using FireVariadic_t = void(__cdecl *)(void *frame, uint32_t *slotPtr, const char *fmt, ...);
PlaceCaret_t g_origPlaceCaret = nullptr;
PlaceSelectionPiece_t g_origPlaceSelectionPiece = nullptr;
IndexAtPoint_t g_origIndexAtPoint = nullptr;
void *AnchorOf(void *region) { return static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR; }
// A multi-line box's rows: its label, the buffer the line table indexes (the
// '*' copy of a password box) and the table — each line's first byte, the
// last entry the end.
struct Rows {
void *fs = nullptr;
const uint8_t *text = nullptr;
const uint32_t *starts = nullptr;
int count = 0; // entries; lines = count - 1
float scale = 1.0f; // the box's OFF_LAYOUT_SCALE
};
bool MultiLineRows(void *editBox, Rows &rows) {
const uint8_t mode = Game::Read<uint8_t>(editBox, Offsets::OFF_EDITBOX_MODE_FLAGS);
if ((mode & Offsets::EDITBOX_MODE_MULTILINE) == 0)
return false;
rows.fs = Game::Read<void *>(editBox, Offsets::OFF_EDITBOX_FONTSTRING);
rows.text = Game::Read<const uint8_t *>(editBox, (mode & Offsets::EDITBOX_MODE_PASSWORD)
? Offsets::OFF_EDITBOX_TEXT_MASKED
: Offsets::OFF_EDITBOX_TEXT);
rows.starts = Game::Read<const uint32_t *>(editBox, Offsets::OFF_EDITBOX_LINE_STARTS);
rows.count = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_LINE_COUNT);
rows.scale = Game::Read<float>(editBox, Offsets::OFF_LAYOUT_SCALE);
return rows.fs != nullptr && rows.text != nullptr && rows.starts != nullptr && rows.count >= 2 &&
rows.scale > 0.0f;
}
// The line holding byte `pos` — the placers' walk down the table.
int LineOf(const Rows &rows, uint32_t pos) {
int line = 0;
while (pos >= rows.starts[line + 1]) {
if (line >= rows.count - 2)
break;
++line;
}
return line;
}
bool LineShift(const Rows &rows, int line, float &before, float &after) {
before = 0.0f;
after = 0.0f;
const uint32_t start = rows.starts[line];
const uint32_t end = rows.starts[line + 1];
return end > start &&
Text::InlineTexture::LineIconShift(rows.fs, rows.text + start, static_cast<int>(end - start), before, after);
}
// How far the render moves line `line`'s text below its uniform row: the
// moves of every line above it, and its own text's move.
float ShiftOf(const Rows &rows, int line) {
float shift = 0.0f;
for (int i = 0; i <= line; ++i) {
float before = 0.0f;
float after = 0.0f;
if (!LineShift(rows, i, before, after))
continue;
shift += before;
if (i < line)
shift += after;
}
return shift;
}
// The placers' row in the box's parent units: the label's font height and
// spacing, each pixel-aligned at the box's scale (PlaceCaret
// 0x0077DAEB..0x0077DB0E).
float RowHeight(const Rows &rows) {
const float fontH = reinterpret_cast<FontHeight_t>(Offsets::FUN_FONTSTRING_FONT_HEIGHT)(rows.fs, nullptr, 1);
const float spacing = Game::Read<float>(rows.fs, Offsets::OFF_FONTSTRING_SPACING);
const auto align = reinterpret_cast<PixelAlignY_t>(Offsets::FUN_PIXEL_ALIGN_Y);
return align(fontH * rows.scale) + align(spacing * rows.scale);
}
float MeasureRun(const Rows &rows, uint32_t from, uint32_t to) {
return reinterpret_cast<MeasureRun_t>(Offsets::FUN_FONTSTRING_MEASURE_SUBSTRING)(rows.fs, nullptr, rows.text + from,
static_cast<int>(to - from));
}
// The x the placers anchor byte `pos` of `line` at, and the label point they
// anchor to, by the label's horizontal justify (OFF_FONTSTRING_FLAGS & 7).
// False for none, which anchors nothing.
bool AnchorX(const Rows &rows, int line, uint32_t pos, float &x, int &relPoint) {
const uint32_t start = rows.starts[line];
const uint32_t end = rows.starts[line + 1];
switch (Game::Read<uint32_t>(rows.fs, Offsets::OFF_FONTSTRING_FLAGS) & 7u) {
case 1: // LEFT
x = (pos == start) ? 0.0f : MeasureRun(rows, start, pos);
relPoint = Offsets::FRAMEPOINT_TOPLEFT;
return true;
case 2: // CENTER
x = (pos == start) ? 0.0f : MeasureRun(rows, start, pos);
x -= MeasureRun(rows, start, end) * 0.5f;
relPoint = Offsets::FRAMEPOINT_TOP;
return true;
case 4: // RIGHT
x = (pos == end) ? 0.0f : -MeasureRun(rows, pos, end);
relPoint = Offsets::FRAMEPOINT_TOPRIGHT;
return true;
default:
return false;
}
}
// Anchors `region` as the placer did, at `y`.
void Anchor(const Rows &rows, void *region, int relPoint, float x, float y, int flag) {
void *anchor = AnchorOf(region);
reinterpret_cast<ClearAllPoints_t>(Offsets::FUN_LAYOUT_CLEAR_ALL_POINTS)(anchor);
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, Offsets::FRAMEPOINT_TOPLEFT, AnchorOf(rows.fs),
relPoint, x, y, flag);
}
// --- the click ---------------------------------------------------------------
void __fastcall IndexAtPoint_h(void *editBox, void *edx, float x, float y, int *index) {
Rows rows;
if (!MultiLineRows(editBox, rows))
return g_origIndexAtPoint(editBox, edx, x, y, index);
float rect[4] = {0.0f, 0.0f, 0.0f, 0.0f}; // bottom, left, top, right
if (reinterpret_cast<GetRect_t>(Offsets::FUN_REGION_GET_RECT)(AnchorOf(rows.fs), nullptr, rect) == 0)
return g_origIndexAtPoint(editBox, edx, x, y, index);
// The engine walks rows down from the label's top while the click is
// below the row (strictly), stopping at the last line. Walk the rows as
// the render lays them out: each taller by its line's moves.
const float row = RowHeight(rows);
const float below = rect[2] - y;
int line = 0;
float top = 0.0f; // the icon-aware top of `line`, below the label's top
bool moved = false;
for (;;) {
float before = 0.0f;
float after = 0.0f;
if (LineShift(rows, line, before, after))
moved = true;
const float height = row + (before + after) * rows.scale;
if (!(below > top + height) || line >= rows.count - 2)
break;
top += height;
++line;
}
// Without a move the rows are the engine's own; with one, the click is
// moved onto the middle of the uniform row the engine maps to `line`.
const float yMapped = moved ? rect[2] - (static_cast<float>(line) + 0.5f) * row : y;
g_origIndexAtPoint(editBox, edx, x, yMapped, index);
}
// --- a selection piece --------------------------------------------------------
void __fastcall PlaceSelectionPiece_h(void *editBox, void *edx, void *piece, int from, int to) {
Rows rows;
if (!MultiLineRows(editBox, rows) || from < 0)
return g_origPlaceSelectionPiece(editBox, edx, piece, from, to);
const int line = LineOf(rows, static_cast<uint32_t>(from));
const float shift = ShiftOf(rows, line);
g_origPlaceSelectionPiece(editBox, edx, piece, from, to);
if (!(shift > 0.0f) || piece == nullptr)
return;
// The piece starts at `from`, clamped to its line.
uint32_t pos = static_cast<uint32_t>(from);
if (pos < rows.starts[line])
pos = rows.starts[line];
float x = 0.0f;
int relPoint = 0;
if (!AnchorX(rows, line, pos, x, relPoint))
return;
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
Anchor(rows, piece, relPoint, x, y, 0);
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(AnchorOf(piece), nullptr, 1);
}
// --- the caret ----------------------------------------------------------------
void __fastcall PlaceCaret_h(void *editBox) {
Rows rows;
if (!MultiLineRows(editBox, rows))
return g_origPlaceCaret(editBox);
const int cursor = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_CURSOR_BYTE);
const int visibleStart = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_VISIBLE_START);
const int visibleLength = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_VISIBLE_LENGTH);
// Outside the window the engine hides the caret and fires nothing.
if (cursor < visibleStart || cursor > visibleStart + visibleLength)
return g_origPlaceCaret(editBox);
const int line = LineOf(rows, static_cast<uint32_t>(cursor));
const float shift = ShiftOf(rows, line);
if (!(shift > 0.0f))
return g_origPlaceCaret(editBox);
uint32_t *slot = Game::Ptr<uint32_t>(editBox, Offsets::OFF_EDITBOX_ONCURSORCHANGED_SLOT);
const uint32_t handler = slot[0];
slot[0] = 0;
g_origPlaceCaret(editBox);
slot[0] = handler;
void *caret = Game::Read<void *>(editBox, Offsets::OFF_EDITBOX_CARET);
if (caret == nullptr)
return;
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
float x = 0.0f;
int relPoint = 0;
if (AnchorX(rows, line, static_cast<uint32_t>(cursor), x, relPoint))
Anchor(rows, caret, relPoint, x, y, 1);
if (handler == 0)
return;
// OnCursorChanged(x, y, width, height) as the engine fires it: each
// value by the anchor→script factor, pushed as a double.
void *anchor = AnchorOf(caret);
void **vtable = *static_cast<void ***>(anchor);
const float width = reinterpret_cast<Extent_t>(vtable[7])(anchor, nullptr); // +0x1C GetWidth
const float height = reinterpret_cast<Extent_t>(vtable[8])(anchor, nullptr); // +0x20 GetHeight
const auto aspect = reinterpret_cast<ScreenAspect_t>(Offsets::FUN_COORD_GET_SCALED_SCREEN_ASPECT);
const auto divide = reinterpret_cast<DivideByScreenScaleX_t>(Offsets::FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X);
const float factor = Game::Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE);
const auto push = [&](float value) { return static_cast<double>(divide(aspect() * factor * value)); };
const double args[4] = {push(x), push(y), push(width), push(height)};
void *L = Game::Lua::State();
const int savedTop = (L != nullptr) ? Game::Lua::GetTop(L) : 0;
reinterpret_cast<FireVariadic_t>(Offsets::FUN_FRAME_RUN_SCRIPT_VARIADIC)(
editBox, slot, reinterpret_cast<const char *>(Offsets::VAR_SCRIPT_FMT_FFFF), args[0], args[1], args[2],
args[3]);
if (L != nullptr)
Game::Lua::SetTop(L, savedTop);
}
const Game::HookAutoRegister _caretHook{Offsets::FUN_EDITBOX_PLACE_CARET, reinterpret_cast<void *>(&PlaceCaret_h),
reinterpret_cast<void **>(&g_origPlaceCaret)};
const Game::HookAutoRegister _pieceHook{Offsets::FUN_EDITBOX_PLACE_SELECTION_PIECE,
reinterpret_cast<void *>(&PlaceSelectionPiece_h),
reinterpret_cast<void **>(&g_origPlaceSelectionPiece)};
const Game::HookAutoRegister _clickHook{Offsets::FUN_EDITBOX_INDEX_AT_POINT,
reinterpret_cast<void *>(&IndexAtPoint_h),
reinterpret_cast<void **>(&g_origIndexAtPoint)};
} // namespace
} // namespace EditBox::IconRows
+152 -18
View File
@@ -543,46 +543,180 @@ int __fastcall Script_UseEquipmentSet(void *L) {
return 1;
}
const Game::Doc::Field kSetIDArgs[] = {
Game::Doc::Req("setID", "number"),
};
const Game::Doc::Field kSlotArgs[] = {
Game::Doc::Req("slot", "luaIndex", "An equipment slot, from 1 to 19."),
};
const Game::Doc::Field kCanUseRets[] = {
Game::Doc::Req("canUseEquipmentSets", "bool", "False while a set swap is in progress."),
};
const Game::Doc::Function kCanUseEquipmentSets{
"Returns whether an equipment set can be used now.", {}, kCanUseRets};
const Game::Doc::Field kNumRets[] = {
Game::Doc::Req("numEquipmentSets", "number"),
};
const Game::Doc::Function kGetNumEquipmentSets{
"Returns the number of equipment sets the character has.", {}, kNumRets};
const Game::Doc::Field kIDsRets[] = {
Game::Doc::Req("setIDs", "table", "The set IDs in the order the sets were created."),
};
const Game::Doc::Function kGetEquipmentSetIDs{
"Returns the IDs of all the character's equipment sets.", {}, kIDsRets};
const Game::Doc::Field kIDArgs[] = {
Game::Doc::Req("name", "cstring", "The exact name; case matters."),
};
const Game::Doc::Field kIDRets[] = {
Game::Doc::Opt("setID", "number", nullptr, "Nil when no set has that name."),
};
const Game::Doc::Function kGetEquipmentSetID{
"Returns the ID of the equipment set with a name.", kIDArgs, kIDRets};
const Game::Doc::Field kInfoRets[] = {
Game::Doc::Opt("name", "string", nullptr,
"Nil, with the rest, when the set does not exist."),
Game::Doc::Opt("icon", "string", nullptr, "The icon's texture name."),
Game::Doc::Opt("setID", "number"),
Game::Doc::Opt("isEquipped", "bool", nullptr,
"True when every slot that is not ignored holds what the set saved."),
Game::Doc::Opt("numItems", "number", nullptr,
"numEquipped plus numInInventory plus numMissing."),
Game::Doc::Opt("numEquipped", "number"),
Game::Doc::Opt("numInInventory", "number", nullptr,
"In the bags, or in the bank while the bank is open."),
Game::Doc::Opt("numMissing", "number"),
Game::Doc::Opt("numIgnored", "number"),
};
const Game::Doc::Function kGetEquipmentSetInfo{
"Returns an equipment set's name, icon, and where its items are.", kSetIDArgs,
kInfoRets};
const Game::Doc::Field kIgnoredRets[] = {
Game::Doc::Opt("ignoredSlots", "table", nullptr,
"The numbers of the ignored slots; nil when the set does not exist."),
};
const Game::Doc::Function kGetIgnoredSlots{
"Returns the slots an equipment set leaves alone.", kSetIDArgs, kIgnoredRets};
const Game::Doc::Field kItemIDsRets[] = {
Game::Doc::Opt("itemIDs", "table", nullptr,
"Keyed by slot: the item ID, 0 for an empty slot, 1 for an ignored slot, "
"or -1 when not known."),
};
const Game::Doc::Function kGetItemIDs{
"Returns the item ID an equipment set saved for each slot.", kSetIDArgs, kItemIDsRets};
const Game::Doc::Field kLocationsRets[] = {
Game::Doc::Opt("locations", "table", nullptr,
"Keyed by slot: a packed location, 0 for an empty slot, 1 for an ignored "
"slot, or -1 for a missing item."),
};
const Game::Doc::Function kGetItemLocations{
"Returns where each item of an equipment set is now.", kSetIDArgs, kLocationsRets};
const Game::Doc::Field kCreateArgs[] = {
Game::Doc::Req("name", "cstring", "Shorter than 32 characters, with no line break."),
Game::Doc::Opt("icon", "string", "\"INV_Misc_QuestionMark\"", "A texture name."),
};
const Game::Doc::Field kCreateRets[] = {
Game::Doc::Opt("setID", "number", nullptr, "Nil when the name is already in use."),
};
const Game::Doc::Function kCreateEquipmentSet{
"Saves the equipped items as a new equipment set.", kCreateArgs, kCreateRets};
const Game::Doc::Field kSaveArgs[] = {
Game::Doc::Req("setID", "number"),
Game::Doc::Opt("icon", "string", nullptr, "A new texture name; nil keeps the icon."),
};
const Game::Doc::Function kSaveEquipmentSet{
"Saves the equipped items into an existing equipment set.", kSaveArgs, {}};
const Game::Doc::Field kModifyArgs[] = {
Game::Doc::Req("setID", "number"),
Game::Doc::Req("newName", "cstring"),
};
const Game::Doc::Function kModifyEquipmentSet{
"Renames an equipment set; does nothing when another set has the name.", kModifyArgs,
{}};
const Game::Doc::Function kDeleteEquipmentSet{
"Deletes an equipment set and removes it from the action bars.", kSetIDArgs, {}};
const Game::Doc::Function kIgnoreSlotForSave{
"Makes the next save of an equipment set leave a slot alone.", kSlotArgs, {}};
const Game::Doc::Function kUnignoreSlotForSave{
"Makes the next save of an equipment set include a slot again.", kSlotArgs, {}};
const Game::Doc::Field kIsIgnoredRets[] = {
Game::Doc::Req("isSlotIgnored", "bool"),
};
const Game::Doc::Function kIsSlotIgnoredForSave{
"Returns whether the next save of an equipment set leaves a slot alone.", kSlotArgs,
kIsIgnoredRets};
const Game::Doc::Function kClearIgnoredSlotsForSave{
"Makes the next save of an equipment set include every slot.", {}, {}};
const Game::Doc::Field kLockedRets[] = {
Game::Doc::Opt("hasLockedItems", "bool", nullptr, "Nil when the set does not exist."),
};
const Game::Doc::Function kEquipmentSetContainsLockedItems{
"Returns whether an item of an equipment set is locked by a move in progress.",
kSetIDArgs, kLockedRets};
const Game::Doc::Field kUseRets[] = {
Game::Doc::Req("setWasEquipped", "bool",
"True when the swap starts; EQUIPMENT_SWAP_FINISHED gives the result."),
};
const Game::Doc::Function kUseEquipmentSet{
"Equips the items of an equipment set.", kSetIDArgs, kUseRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_EquipmentSet", "CanUseEquipmentSets",
&Script_CanUseEquipmentSets);
&Script_CanUseEquipmentSets, &kCanUseEquipmentSets);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetNumEquipmentSets",
&Script_GetNumEquipmentSets);
&Script_GetNumEquipmentSets, &kGetNumEquipmentSets);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetEquipmentSetIDs",
&Script_GetEquipmentSetIDs);
&Script_GetEquipmentSetIDs, &kGetEquipmentSetIDs);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetEquipmentSetID",
&Script_GetEquipmentSetID);
&Script_GetEquipmentSetID, &kGetEquipmentSetID);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetEquipmentSetInfo",
&Script_GetEquipmentSetInfo);
&Script_GetEquipmentSetInfo, &kGetEquipmentSetInfo);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetIgnoredSlots",
&Script_GetIgnoredSlots);
&Script_GetIgnoredSlots, &kGetIgnoredSlots);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetItemIDs",
&Script_GetItemIDs);
&Script_GetItemIDs, &kGetItemIDs);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetItemLocations",
&Script_GetItemLocations);
&Script_GetItemLocations, &kGetItemLocations);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "CreateEquipmentSet",
&Script_CreateEquipmentSet);
&Script_CreateEquipmentSet, &kCreateEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "SaveEquipmentSet",
&Script_SaveEquipmentSet);
&Script_SaveEquipmentSet, &kSaveEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "ModifyEquipmentSet",
&Script_ModifyEquipmentSet);
&Script_ModifyEquipmentSet, &kModifyEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "DeleteEquipmentSet",
&Script_DeleteEquipmentSet);
&Script_DeleteEquipmentSet, &kDeleteEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "IgnoreSlotForSave",
&Script_IgnoreSlotForSave);
&Script_IgnoreSlotForSave, &kIgnoreSlotForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "UnignoreSlotForSave",
&Script_UnignoreSlotForSave);
&Script_UnignoreSlotForSave, &kUnignoreSlotForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "IsSlotIgnoredForSave",
&Script_IsSlotIgnoredForSave);
&Script_IsSlotIgnoredForSave, &kIsSlotIgnoredForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "ClearIgnoredSlotsForSave",
&Script_ClearIgnoredSlotsForSave);
&Script_ClearIgnoredSlotsForSave,
&kClearIgnoredSlotsForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet",
"EquipmentSetContainsLockedItems",
&Script_EquipmentSetContainsLockedItems);
&Script_EquipmentSetContainsLockedItems,
&kEquipmentSetContainsLockedItems);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "UseEquipmentSet",
&Script_UseEquipmentSet);
&Script_UseEquipmentSet, &kUseEquipmentSet);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+25 -3
View File
@@ -151,8 +151,30 @@ constexpr int LOC_IGNORED = 1;
// when the swap settles — see
// `EquipmentSet::Pending`. `success` is
// 1 or nil, never 0 (a Lua 0 is true).
inline const Event::Custom::AutoReserve kEvtSetsChanged{"EQUIPMENT_SETS_CHANGED"};
inline const Event::Custom::AutoReserve kEvtSwapPending{"EQUIPMENT_SWAP_PENDING"};
inline const Event::Custom::AutoReserve kEvtSwapFinished{"EQUIPMENT_SWAP_FINISHED"};
inline const Game::Doc::Event kSetsChangedDoc{
"EquipmentSet",
"Fires when an equipment set is created, saved, renamed, or deleted, or when the slots "
"ignored for the next save change.",
{}};
inline const Game::Doc::Field kSwapPendingPayload[] = {
Game::Doc::Req("setID", "number"),
};
inline const Game::Doc::Event kSwapPendingDoc{
"EquipmentSet", "Fires when C_EquipmentSet.UseEquipmentSet has sent all its moves.",
kSwapPendingPayload};
inline const Game::Doc::Field kSwapFinishedPayload[] = {
Game::Doc::Opt("result", "number", nullptr, "1 when the swap succeeded, else nil."),
Game::Doc::Req("setID", "number"),
};
inline const Game::Doc::Event kSwapFinishedDoc{
"EquipmentSet", "Fires when the moves of an equipment set swap are done.",
kSwapFinishedPayload};
inline const Event::Custom::AutoReserve kEvtSetsChanged{"EQUIPMENT_SETS_CHANGED",
&kSetsChangedDoc};
inline const Event::Custom::AutoReserve kEvtSwapPending{"EQUIPMENT_SWAP_PENDING",
&kSwapPendingDoc};
inline const Event::Custom::AutoReserve kEvtSwapFinished{"EQUIPMENT_SWAP_FINISHED",
&kSwapFinishedDoc};
} // namespace EquipmentSet
+12 -1
View File
@@ -218,6 +218,16 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
{"SetEquipmentSet", &Script_GameTooltipSetEquipmentSet},
};
const Game::Doc::Field kSetEquipmentSetArgs[] = {
Game::Doc::Req("name", "cstring", "The name of the equipment set."),
};
const Game::Doc::Function kSetEquipmentSet{
"Shows an equipment set in the tooltip: its name and where its items are.",
kSetEquipmentSetArgs, {}};
const Game::Doc::Method g_methodDocs[] = {
{"SetEquipmentSet", &kSetEquipmentSet},
};
} // namespace
static void RegisterLuaFunctions() {
@@ -226,7 +236,8 @@ 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])));
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+207 -19
View File
@@ -669,54 +669,242 @@ static int __fastcall Script_C_Reputation_SetWatchedFactionByID(void *L) {
return 0;
}
const Game::Doc::Field kFactionDataFields[] = {
Game::Doc::Req("factionID", "number"),
Game::Doc::Req("name", "string"),
Game::Doc::Req("description", "string", "Empty when the faction has none."),
Game::Doc::Req("reaction", "luaIndex", "The rank, from 1 for Hated to 8 for Exalted."),
Game::Doc::Req("currentReactionThreshold", "number",
"The standing at the bottom of the current rank."),
Game::Doc::Req("nextReactionThreshold", "number",
"The standing at the top of the current rank."),
Game::Doc::Req("currentStanding", "number"),
Game::Doc::Req("atWarWith", "bool"),
Game::Doc::Req("canToggleAtWar", "bool",
"False below -3000 standing, when the war state is fixed, or with no "
"reputation slot."),
Game::Doc::Req("isChild", "bool", "Always false."),
Game::Doc::Req("isHeader", "bool", "True when the faction is a header in the reputation list."),
Game::Doc::Req("isHeaderWithRep", "bool", "Always false."),
Game::Doc::Req("isCollapsed", "bool", "True for a collapsed header."),
Game::Doc::Req("isWatched", "bool", "True when the faction is shown above the experience bar."),
Game::Doc::Req("hasBonusRepGain", "bool", "Always false."),
Game::Doc::Req("canSetInactive", "bool",
"True when the faction has a reputation slot and is not a header."),
Game::Doc::Req("isAccountWide", "bool", "Always false."),
};
const Game::Doc::Structure kFactionData{"FactionData", "Reputation", kFactionDataFields,
"A faction's reputation data."};
const Game::Doc::Field kFactionIDArgs[] = {
Game::Doc::Req("factionID", "number"),
};
const Game::Doc::Field kSortIndexArgs[] = {
Game::Doc::Req("factionSortIndex", "luaIndex", "A position in the reputation list."),
};
const Game::Doc::Field kFactionIDByIndexArgs[] = {
Game::Doc::Req("factionIndex", "luaIndex", "A position in the reputation list."),
};
const Game::Doc::Field kFactionIDByIndexRets[] = {
Game::Doc::Opt("factionID", "number", nullptr,
"0 for a header with no faction of its own, such as Other or Inactive; nil "
"when the index is out of range."),
};
const Game::Doc::Function kGetFactionIDByIndex{
"Returns the faction ID of the entry at a position in the reputation list.",
kFactionIDByIndexArgs, kFactionIDByIndexRets, "ReputationGlobals", true};
const Game::Doc::Field kFactionInfoByIDRets[] = {
Game::Doc::Opt("name", "string", nullptr, "Nil, with the rest, when no faction has the ID."),
Game::Doc::Opt("description", "string", nullptr, "Empty when the faction has none."),
Game::Doc::Opt("standingID", "number", nullptr, "The rank, from 1 for Hated to 8 for Exalted."),
Game::Doc::Opt("barMin", "number", nullptr, "The standing at the bottom of the current rank."),
Game::Doc::Opt("barMax", "number", nullptr, "The standing at the top of the current rank."),
Game::Doc::Opt("barValue", "number", nullptr, "The current standing."),
Game::Doc::Opt("atWarWith", "number", nullptr,
"1 when the player is at war with the faction, else nil."),
Game::Doc::Opt("canToggleAtWar", "number", nullptr,
"1 when the at-war state can be changed, else nil."),
Game::Doc::Opt("isHeader", "number", nullptr,
"1 when the faction is a header in the reputation list, else nil."),
Game::Doc::Opt("isCollapsed", "number", nullptr, "1 for a collapsed header, else nil."),
Game::Doc::Opt("isWatched", "number", nullptr,
"1 when the faction is shown above the experience bar, else nil."),
};
const Game::Doc::Function kGetFactionInfoByID{
"Returns the same values as GetFactionInfo for a faction ID, whether or not the faction "
"is in the reputation list.",
kFactionIDArgs, kFactionInfoByIDRets, "ReputationGlobals"};
const Game::Doc::Field kFactionParentIDRets[] = {
Game::Doc::Opt("parentFactionID", "number", nullptr,
"0 for a faction with no parent; nil when no faction has the ID."),
};
const Game::Doc::Function kGetFactionParentID{
"Returns the ID of a faction's parent faction, such as Alliance for Stormwind.",
kFactionIDArgs, kFactionParentIDRets, "ReputationGlobals", true};
const Game::Doc::Field kSetWatchedArgs[] = {
Game::Doc::Req("factionID", "number",
"0, or an ID with no reputation slot, clears the watched faction; a negative "
"ID does nothing."),
};
const Game::Doc::Function kSetWatchedFactionByID{
"Sets the faction shown above the experience bar.", kSetWatchedArgs, {}};
const Game::Doc::Field kSetSelectedByIDArgs[] = {
Game::Doc::Req("factionID", "number", "0 clears the selection; a negative ID does nothing."),
};
const Game::Doc::Function kSetSelectedFactionByID{
"Selects a faction in the reputation pane by ID, even when the faction is not listed.",
kSetSelectedByIDArgs, {}, nullptr, true};
const Game::Doc::Function kToggleFactionAtWarByID{
"Toggles whether the player is at war with a faction, by ID; the same limits apply as "
"for C_Reputation.ToggleFactionAtWar.",
kFactionIDArgs, {}, nullptr, true};
const Game::Doc::Field kIsActiveRets[] = {
Game::Doc::Req("isActive", "bool",
"False under the Inactive header, and for anything with no reputation slot."),
};
const Game::Doc::Function kIsFactionActive{
"Returns whether the faction at a position in the reputation list is active, that is, "
"not under the Inactive header.",
kSortIndexArgs, kIsActiveRets};
const Game::Doc::Function kIsFactionActiveByID{
"Returns whether a faction is active, that is, not under the Inactive header.",
kFactionIDArgs, kIsActiveRets, nullptr, true};
const Game::Doc::Field kIsInactiveRets[] = {
Game::Doc::Opt("isInactive", "number", nullptr,
"1 when the faction is under the Inactive header, else nil."),
};
const Game::Doc::Function kIsFactionInactive{
"Returns whether the faction at a position in the reputation list is under the Inactive "
"header, as 1 or nil.",
kSortIndexArgs, kIsInactiveRets, nullptr, true};
const Game::Doc::Function kSetFactionInactiveByID{
"Moves a faction under the Inactive header of the reputation list, by ID.",
kFactionIDArgs, {}, nullptr, true};
const Game::Doc::Function kSetFactionActiveByID{
"Moves a faction out from under the Inactive header of the reputation list, by ID.",
kFactionIDArgs, {}, nullptr, true};
const Game::Doc::Function kSetFactionInactive{
"Moves the faction at a position in the reputation list under the Inactive header.",
kSortIndexArgs, {}, nullptr, true};
const Game::Doc::Function kSetFactionActive{
"Moves the faction at a position in the reputation list out from under the Inactive "
"header.",
kSortIndexArgs, {}};
const Game::Doc::Function kToggleFactionAtWar{
"Toggles whether the player is at war with the faction at a position in the reputation "
"list.",
kSortIndexArgs, {}};
const Game::Doc::Function kSetSelectedFaction{
"Selects the faction at a position in the reputation list.", kSortIndexArgs, {}};
const Game::Doc::Field kWatchedFactionDataRets[] = {
Game::Doc::Opt("watchedFactionData", "FactionData", nullptr,
"Nil when no faction is watched."),
};
const Game::Doc::Function kGetWatchedFactionData{
"Returns the faction shown above the experience bar.", {}, kWatchedFactionDataRets};
const Game::Doc::Field kStandingsRets[] = {
Game::Doc::Req("standings", "table",
"Keyed by faction ID; each value is the faction's standing."),
};
const Game::Doc::Function kGetFactionStandings{
"Returns the standing of every faction the character has reputation with, keyed by "
"faction ID.",
{}, kStandingsRets, nullptr, true};
const Game::Doc::Field kFactionDataByIDRets[] = {
Game::Doc::Opt("factionData", "FactionData", nullptr, "Nil when no faction has the ID."),
};
const Game::Doc::Function kGetFactionDataByID{
"Returns a faction's reputation data by ID, whether or not the faction is in the "
"reputation list.",
kFactionIDArgs, kFactionDataByIDRets};
const Game::Doc::Field kFactionDataByIndexRets[] = {
Game::Doc::Opt("factionData", "FactionData", nullptr,
"Nil when the index is out of range or names the Other or Inactive header."),
};
const Game::Doc::Function kGetFactionDataByIndex{
"Returns the reputation data of the faction at a position in the reputation list.",
kSortIndexArgs, kFactionDataByIndexRets};
static void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("GetFactionIDByIndex", &Script_GetFactionIDByIndex);
Game::Lua::RegisterGlobalFunction("GetFactionInfoByID", &Script_GetFactionInfoByID);
Game::Lua::RegisterGlobalFunction("GetFactionParentID", &Script_GetFactionParentID);
Game::Lua::RegisterGlobalFunction("GetFactionIDByIndex", &Script_GetFactionIDByIndex,
&kGetFactionIDByIndex);
Game::Lua::RegisterGlobalFunction("GetFactionInfoByID", &Script_GetFactionInfoByID,
&kGetFactionInfoByID);
Game::Lua::RegisterGlobalFunction("GetFactionParentID", &Script_GetFactionParentID,
&kGetFactionParentID);
Game::Lua::RegisterTableFunction("C_Reputation", "SetWatchedFactionByID",
&Script_C_Reputation_SetWatchedFactionByID);
&Script_C_Reputation_SetWatchedFactionByID,
&kSetWatchedFactionByID);
Game::Lua::RegisterTableFunction("C_Reputation", "SetSelectedFactionByID",
&Script_C_Reputation_SetSelectedFactionByID);
&Script_C_Reputation_SetSelectedFactionByID,
&kSetSelectedFactionByID);
Game::Lua::RegisterTableFunction("C_Reputation", "ToggleFactionAtWarByID",
&Script_C_Reputation_ToggleFactionAtWarByID);
&Script_C_Reputation_ToggleFactionAtWarByID,
&kToggleFactionAtWarByID);
Game::Lua::RegisterTableFunction("C_Reputation", "IsFactionActive",
&Script_C_Reputation_IsFactionActive);
&Script_C_Reputation_IsFactionActive, &kIsFactionActive);
Game::Lua::RegisterTableFunction("C_Reputation", "IsFactionActiveByID",
&Script_C_Reputation_IsFactionActiveByID);
&Script_C_Reputation_IsFactionActiveByID,
&kIsFactionActiveByID);
Game::Lua::RegisterTableFunction(
"C_Reputation", "IsFactionInactive",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_IS_FACTION_INACTIVE)));
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_IS_FACTION_INACTIVE)),
&kIsFactionInactive);
Game::Lua::RegisterTableFunction("C_Reputation", "SetFactionInactiveByID",
&Script_C_Reputation_SetFactionInactiveByID);
&Script_C_Reputation_SetFactionInactiveByID,
&kSetFactionInactiveByID);
Game::Lua::RegisterTableFunction("C_Reputation", "SetFactionActiveByID",
&Script_C_Reputation_SetFactionActiveByID);
&Script_C_Reputation_SetFactionActiveByID,
&kSetFactionActiveByID);
Game::Lua::RegisterTableFunction(
"C_Reputation", "SetFactionInactive",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_INACTIVE)));
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_INACTIVE)),
&kSetFactionInactive);
Game::Lua::RegisterTableFunction(
"C_Reputation", "SetFactionActive",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_ACTIVE)));
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_ACTIVE)),
&kSetFactionActive);
Game::Lua::RegisterTableFunction(
"C_Reputation", "ToggleFactionAtWar",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_FACTION_TOGGLE_AT_WAR)));
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_FACTION_TOGGLE_AT_WAR)),
&kToggleFactionAtWar);
// The stock global, mirrored into the namespace — the engine's own
// handler already has the Lua C ABI, so it registers as-is.
Game::Lua::RegisterTableFunction(
"C_Reputation", "SetSelectedFaction",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_SELECTED_FACTION)));
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_SELECTED_FACTION)),
&kSetSelectedFaction);
Game::Lua::RegisterTableFunction("C_Reputation", "GetWatchedFactionData",
&Script_C_Reputation_GetWatchedFactionData);
&Script_C_Reputation_GetWatchedFactionData,
&kGetWatchedFactionData);
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionStandings",
&Script_C_Reputation_GetFactionStandings);
&Script_C_Reputation_GetFactionStandings,
&kGetFactionStandings);
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionDataByID",
&Script_C_Reputation_GetFactionDataByID);
&Script_C_Reputation_GetFactionDataByID,
&kGetFactionDataByID);
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionDataByIndex",
&Script_C_Reputation_GetFactionDataByIndex);
&Script_C_Reputation_GetFactionDataByIndex,
&kGetFactionDataByIndex);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+23 -2
View File
@@ -50,7 +50,15 @@ using GetStanding_t = int(__fastcall *)(int factionID);
constexpr const char *kEventName = "FACTION_STANDING_CHANGED";
const Event::Custom::AutoReserve _reserve{kEventName};
const Game::Doc::Field kPayload[] = {
Game::Doc::Req("factionID", "number"),
Game::Doc::Req("newStanding", "number", "The faction's standing after the change."),
Game::Doc::Req("repGained", "number", "Negative for a loss."),
};
const Game::Doc::Event kEventDoc{"Reputation", "Fires when the player's reputation changes.",
kPayload};
const Event::Custom::AutoReserve _reserve{kEventName, &kEventDoc};
// In-flight snapshot of the current rep change. Set on hook entry,
// cleared on hook exit. WoW's main thread is the only one that runs
@@ -67,9 +75,22 @@ int __fastcall Script_C_Reputation_GetLastStandingChange(void *L) {
return 3;
}
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("factionID", "number", nullptr,
"Nil, with the rest, outside a FACTION_STANDING_CHANGED or "
"CHAT_MSG_COMBAT_FACTION_CHANGE handler."),
Game::Doc::Opt("newStanding", "number", nullptr, "The faction's standing after the change."),
Game::Doc::Opt("repGained", "number", nullptr, "Negative for a loss."),
};
const Game::Doc::Function kGetLastStandingChange{
"Returns the reputation change that FACTION_STANDING_CHANGED and "
"CHAT_MSG_COMBAT_FACTION_CHANGE report, while their handlers run.",
{}, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Reputation", "GetLastStandingChange",
&Script_C_Reputation_GetLastStandingChange);
&Script_C_Reputation_GetLastStandingChange,
&kGetLastStandingChange);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+20 -3
View File
@@ -11,7 +11,7 @@
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Macro.SetMacroDisplay(macroSlot, value)` — the Lua door onto
// `C_Macro.SetMacroDisplay(macroSlot, value [, unit])` — the Lua door onto
// `Macro::ShowTooltip::Publish`, for an addon that has its own macro parser
// and wants the display done properly rather than by replacing the action
// globals. See the ShowTooltip header for what publishing buys.
@@ -27,6 +27,12 @@
// item-before-spell precedence, `item:N`, links, inventory slots and
// `bag slot` for free.
//
// `unit` is the `[@unit]` the publisher's clause matched: the unit the
// button's range check uses. Unlike `SetMacroSpell`'s target it stays a
// token, resolved at every lookup (`PublishOptions::liveUnit`), so a
// publisher whose clause still matches need not republish when the
// mouseover, focus or target moves.
//
// `SetMacroSpell(macro, spell [, target])` and `SetMacroItem(macro, item
// [, target])` are 4.3.4's narrower doors onto the same publish: a spellbook
// spell only, or an item only, aimed at `target` (the unit the button's range
@@ -37,6 +43,7 @@
#include "Offsets.h"
#include "macro/Arg.h"
#include "macro/ShowTooltip.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -46,7 +53,7 @@ namespace {
int __fastcall Script_C_Macro_SetMacroDisplay(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::Error(L, "Usage: C_Macro.SetMacroDisplay(macroSlot, value)");
Game::Lua::Error(L, "Usage: C_Macro.SetMacroDisplay(macroSlot, value [, unit])");
return 0;
}
const int macroSlot = static_cast<int>(Game::Lua::ToNumber(L, 1));
@@ -63,7 +70,17 @@ int __fastcall Script_C_Macro_SetMacroDisplay(void *L) {
}
const char *value = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
Game::Lua::PushBoolean(L, Macro::ShowTooltip::Publish(macroSlot, value) ? 1 : 0);
Macro::ShowTooltip::PublishOptions options;
// A value that names nothing aims at nothing, so its unit is not read.
if (value != nullptr && value[0] != '\0' && Game::Lua::IsString(L, 3)) {
options.unit = Game::Lua::ToString(L, 3);
options.liveUnit = true;
// The engine's resolver vets the token: anything that is not one
// raises its "Unknown unit name" error, exactly as UnitExists does,
// before anything is published.
Unit::Identity::GuidForToken(options.unit);
}
Game::Lua::PushBoolean(L, Macro::ShowTooltip::Publish(macroSlot, value, options) ? 1 : 0);
return 1;
}
+12 -2
View File
@@ -160,6 +160,9 @@ struct Entry {
uint32_t isPet = 0;
int itemID = 0;
uint64_t unit = 0; // Info::unit
// A published `PublishOptions::liveUnit` token, resolved afresh by every
// lookup in place of `unit`. Empty for none.
char unitToken[kOptionsMax] = {};
uint32_t lastEvalMs = 0;
};
@@ -1233,7 +1236,8 @@ bool LookupEntries(uint32_t macroID, Info *out) {
out->spellID = e.spellID;
out->isPet = e.isPet;
out->itemID = e.itemID;
out->unit = e.unit;
out->unit = e.unitToken[0] != '\0' ? ::Unit::Identity::GuidForToken(e.unitToken)
: e.unit;
// A publisher re-publishes the moment its answer changes, so a claimed
// macro never needs the polling refresh a condition of ours does.
out->conditional = false;
@@ -1327,7 +1331,13 @@ bool Publish(int macroSlot, const char *value, const PublishOptions &options) {
ResolveValue(value, &r, /*explicitValue=*/true,
/*itemOnly=*/options.accept == Accept::Item);
}
r.unit = UnitGuid(options.unit);
// Set on every publish, so one that changes only the unit still lands:
// `ApplyResolution` stores the unit before its unchanged-value early out.
if (options.liveUnit && options.unit != nullptr)
std::snprintf(e.unitToken, sizeof(e.unitToken), "%s", options.unit);
else
e.unitToken[0] = '\0';
r.unit = options.liveUnit ? 0 : UnitGuid(options.unit);
BusyScope busy;
ApplyResolution(L, e, r, matched);
+10 -2
View File
@@ -72,8 +72,10 @@ struct Info {
// from it has to keep re-reading while the cursor rests on the button.
bool conditional;
// The unit the macro's spell is aimed at, 4.3.4's MACRONODE::m_target: the
// `[@unit]` of the clause that matched, or the target a `SetMacroSpell` /
// `SetMacroItem` named. 0 means the current target, as for any action.
// `[@unit]` of the clause that matched, the target a `SetMacroSpell` /
// `SetMacroItem` named, or whoever the token a `SetMacroDisplay` named
// points at as of this lookup. 0 means the current target, as for any
// action.
uint64_t unit;
};
@@ -86,6 +88,12 @@ struct PublishOptions {
Accept accept = Accept::Any;
// A unit token or a name the spell is aimed at; null for the current target.
const char *unit = nullptr;
// Resolve `unit` at every lookup rather than once, here, so the macro
// keeps following whoever the token names (a new mouseover, focus or
// target) with no republish. `unit` must then be a token the engine's
// resolver accepts: one that resolves once never raises later, since the
// resolver tells a token's family from its spelling alone.
bool liveUnit = false;
// The claim ends when the macro's body is edited, as 4.3.4's SetMacroSpell
// and SetMacroItem last only until the macro is parsed again. Otherwise it
// lasts until `Release` or a reload.
+73
View File
@@ -0,0 +1,73 @@
// 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 "object/Corpse.h"
#include "Game.h"
#include "Offsets.h"
#include "object/Resolve.h"
namespace Object::Corpse {
namespace {
// `ClntObjMgrEnumVisibleObjects(cb, ctx)`; the callback's dummy EDX slot
// puts the trailing GUID where the engine passes it. Return non-zero to keep
// iterating.
using EnumCallback_t = int(__fastcall *)(void *ctx, void *unusedEdx, uint64_t guid);
using EnumVisibleObjects_t = int(__fastcall *)(EnumCallback_t cb, void *ctx);
const uint8_t *Fields(const void *corpse) {
return Game::Read<const uint8_t *>(corpse, Offsets::OFF_UNIT_DESCRIPTOR);
}
uint64_t Owner(const void *corpse) {
const uint8_t *fields = Fields(corpse);
return fields != nullptr ? Game::Read<uint64_t>(fields, Offsets::OFF_CORPSE_FIELD_OWNER) : 0;
}
struct Scan {
uint64_t owner;
void *corpse;
};
// Bones keep their owner (tortoise-wow Map.cpp, the corpse-to-bones
// conversion), so every earlier death leaves one beside the live corpse;
// 4.3.4 skips them.
int __fastcall ScanCallback(Scan *scan, void * /*unusedEdx*/, uint64_t guid) {
void *obj = Object::ByGuid(Offsets::TYPEMASK_CORPSE, guid, nullptr, 0);
if (obj == nullptr || Owner(obj) != scan->owner ||
(Game::Read<uint32_t>(Fields(obj), Offsets::OFF_CORPSE_FIELD_FLAGS) &
Offsets::CORPSE_FLAG_BONES) != 0)
return 1;
scan->corpse = obj;
return 0;
}
} // namespace
void *ByOwner(uint64_t owner) {
if (owner == 0 || !Object::RegistryReady())
return nullptr;
Scan scan{owner, nullptr};
reinterpret_cast<EnumVisibleObjects_t>(Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS)(
reinterpret_cast<EnumCallback_t>(&ScanCallback), &scan);
return scan.corpse;
}
uint64_t OwnerOf(uint64_t guid) {
const void *obj = Object::ByGuid(Offsets::TYPEMASK_CORPSE, guid);
return obj != nullptr ? Owner(obj) : 0;
}
} // namespace Object::Corpse
+30
View File
@@ -0,0 +1,30 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
// Player corpses (`TYPEMASK_CORPSE`) and the player each belongs to, read
// from the corpse's CORPSE_FIELD_OWNER.
namespace Object::Corpse {
// The visible corpse `owner` left, not counting bones, or nullptr — 4.3.4's
// ClntObjMgrFindCorpseByOwner, over the visible objects.
void *ByOwner(uint64_t owner);
// The player who left the corpse `guid`, or 0 when `guid` is not a visible
// corpse.
uint64_t OwnerOf(uint64_t guid);
} // namespace Object::Corpse
@@ -0,0 +1,182 @@
// 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/>.
// `smf:AddOnDisplayRefreshedCallback(callback)`, retail's: callback(frame)
// runs each time the frame refreshes what it shows.
//
// Retail's mixin (Blizzard_SharedXML/ScrollingMessageFrame.lua) marks its
// display dirty on each change - a message, a scroll, a resize, a fade reset -
// and refreshes in its OnPostUpdate. So the callbacks run once per update of
// a shown frame that changed, in the order they were added, each under the
// error handler (secureexecuterange). The mark is cleared after them, so a
// change a callback makes (a scroll bar setting the offset) does not run them
// again.
//
// 1.12 refreshes what the frame shows in its line layout
// (ScrollingMessageFrame::Layout), which marks the frame. A message added
// while the frame is scrolled up changes no line and runs no layout, but it
// moves the scroll offset and range, so AddMessage marks the frame too, as
// ScrollByAmount does for retail's fade reset. The callbacks run after the
// frame's own update (FUN_SCROLLINGMESSAGEFRAME_ON_UPDATE: OnUpdate, then the
// fader), which the engine's update pass calls once per frame for each shown
// frame - where retail's OnPostUpdate runs.
#include "scrollingmessageframe/DisplayRefreshed.h"
#include "Game.h"
#include "Offsets.h"
#include "scrollingmessageframe/Layout.h"
#include <unordered_map>
namespace ScrollingMessageFrame::DisplayRefreshed {
namespace {
// Frames with callbacks, and whether each has a refresh pending. Every frame
// is rebuilt on /reload, so the map is cleared then.
std::unordered_map<void *, bool> g_frames;
// The frame-table → callback-list table, in the registry of the Lua state
// that made it. LUA_NOREF until first use in each state.
constexpr int kNoRef = -2;
int g_callbacksRef = kNoRef;
// Runs the callbacks of the frame, each as callback(frame) under the
// engine's script error handler.
void RunCallbacks(void *smf) {
using namespace Game::Lua;
void *L = State();
if (L == nullptr || g_callbacksRef == kNoRef)
return;
const int top = GetTop(L);
PushObject(L, smf);
const int frame = GetTop(L);
RawGetI(L, REGISTRY_INDEX, g_callbacksRef);
PushValue(L, frame);
RawGet(L, -2);
const int callbacks = GetTop(L);
if (Type(L, callbacks) == TYPE_TABLE) {
const int errRef = Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
for (int i = 1;; ++i) {
int errIdx = 0;
if (errRef > 0) {
RawGetI(L, REGISTRY_INDEX, errRef);
errIdx = GetTop(L);
}
RawGetI(L, callbacks, i);
if (Type(L, -1) == TYPE_NIL)
break;
PushValue(L, frame);
PCall(L, 1, 0, errIdx);
SetTop(L, callbacks);
}
}
SetTop(L, top);
}
using OnUpdate_t = void(__fastcall *)(void *smf, void *edx, float elapsed);
OnUpdate_t g_origOnUpdate = nullptr;
void __fastcall OnUpdate_h(void *smf, void *edx, float elapsed) {
g_origOnUpdate(smf, edx, elapsed);
const auto it = g_frames.find(smf);
if (it == g_frames.end() || !it->second)
return;
RunCallbacks(smf);
const auto again = g_frames.find(smf); // a callback can add frames
if (again != g_frames.end())
again->second = false;
}
const Game::HookAutoRegister _updateHook{Offsets::FUN_SCROLLINGMESSAGEFRAME_ON_UPDATE,
reinterpret_cast<void *>(&OnUpdate_h),
reinterpret_cast<void **>(&g_origOnUpdate)};
const ScrollingMessageFrame::Layout::AutoSubscribe _layoutSub{&MarkDirty};
int __fastcall Script_AddOnDisplayRefreshedCallback(void *L) {
using namespace Game::Lua;
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
if (Type(L, 2) != TYPE_FUNCTION) {
Error(L, "Usage: %s:AddOnDisplayRefreshedCallback(callback)", ObjectDisplayName(frame));
return 0;
}
if (g_callbacksRef == kNoRef) {
NewTable(L);
g_callbacksRef = Ref(L, REGISTRY_INDEX);
}
RawGetI(L, REGISTRY_INDEX, g_callbacksRef);
const int frames = GetTop(L);
PushValue(L, 1);
RawGet(L, frames);
if (Type(L, -1) != TYPE_TABLE) {
SetTop(L, frames);
NewTable(L);
PushValue(L, 1);
PushValue(L, -2);
RawSet(L, frames); // frames[self] = callbacks
}
const int callbacks = GetTop(L);
int n = 1;
for (;; ++n) {
RawGetI(L, callbacks, n);
const bool end = Type(L, -1) == TYPE_NIL;
SetTop(L, -2);
if (end)
break;
}
PushNumber(L, n);
PushValue(L, 2);
RawSet(L, callbacks);
g_frames.emplace(frame, false);
return 0;
}
const Game::Lua::FrameMethodEntry kMethods[] = {
{"AddOnDisplayRefreshedCallback", &Script_AddOnDisplayRefreshedCallback},
};
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("callback", "function", "Called as callback(frame)."),
};
const Game::Doc::Function kAddOnDisplayRefreshedCallback{
"Adds a function that runs after the frame refreshes the messages it shows.", kArgs, {}};
const Game::Doc::Method kDocs[] = {
{"AddOnDisplayRefreshedCallback", &kAddOnDisplayRefreshedCallback},
};
void RegisterLuaFunctions() {
// Each registration pass is a new Lua state; the old state's ref is gone.
g_callbacksRef = kNoRef;
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
kMethods, 1, kDocs, 1);
}
void PrepareForReload() { g_frames.clear(); }
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
const Game::ReloadAutoRegister _reload{&PrepareForReload};
} // namespace
void MarkDirty(void *smf) {
const auto it = g_frames.find(smf);
if (it != g_frames.end())
it->second = true;
}
} // namespace ScrollingMessageFrame::DisplayRefreshed
@@ -0,0 +1,22 @@
// 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
namespace ScrollingMessageFrame::DisplayRefreshed {
// Marks a change to what the frame shows that its line layout does not
// report; the frame's display-refreshed callbacks run at its next update.
void MarkDirty(void *smf);
} // namespace ScrollingMessageFrame::DisplayRefreshed
+194
View File
@@ -0,0 +1,194 @@
// 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/>.
// Retail's insert mode on ScrollingMessageFrame: `SetInsertMode(mode)`,
// `GetInsertMode()` and the SCROLLING_MESSAGE_FRAME_INSERT_MODE_* globals.
//
// Retail's mixin (Blizzard_SharedXML/ScrollingMessageFrame.lua, RefreshLayout)
// stacks the lines up from the bottom in BOTTOM mode, the newest message at
// the bottom, and down from the top in TOP mode: the first line's TOPLEFT on
// the frame's TOPLEFT, each next line's TOPLEFT on the BOTTOMLEFT of the line
// before, the spacing below it. The scroll offset counts from the newest
// message in both modes. Setting the mode the frame has does nothing; a change
// lays every line out again and refreshes the display.
//
// 1.12's line layout is retail's BOTTOM mode. It anchors a line once, when it
// makes the line, through one SetPoint call (Offsets.h,
// CALL_SMF_LAYOUT_ANCHOR_LINE), so that call is redirected: a line of a frame
// in TOP mode takes the TOP anchors, every other line the engine's own. The
// layout makes the lines again when the frame changes size, which the
// redirect covers too. When a frame's mode changes, its lines are anchored
// again in order with the layout's own steps: clear the points, set the
// point, lay the line out now. The hyperlink buttons and inline textures are
// anchored to the lines, so they move with them. Retail's frame has no text
// insets; the TOP mode's first line sits the top inset below the frame's top,
// as the BOTTOM mode's sits the bottom inset above its bottom.
#include "Common.h"
#include "Game.h"
#include "Offsets.h"
#include "frame/Destruction.h"
#include "scrollingmessageframe/DisplayRefreshed.h"
#include <cstdint>
#include <unordered_set>
namespace ScrollingMessageFrame::InsertMode {
namespace {
// SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP / _BOTTOM.
constexpr int kInsertModeTop = 1;
constexpr int kInsertModeBottom = 2;
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint, float x, float y,
int flag);
using ClearAllPoints_t = void(__fastcall *)(void *anchor);
using Resize_t = void(__fastcall *)(void *anchor, void *edx, int force);
using PixelAlignY_t = float(__stdcall *)(float length);
// The frames in TOP mode. Every frame is rebuilt on /reload, so the set is
// cleared then.
std::unordered_set<void *> g_topFrames;
bool IsTop(void *smf) { return g_topFrames.count(smf) != 0; }
void *AnchorOf(void *region) { return static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR; }
// Sets a line's anchor as the layout gives it - (BOTTOMLEFT, the frame,
// BOTTOMLEFT, x, y) for the first line, (BOTTOMLEFT, the line before, TOPLEFT,
// x, y) for the others - or, for a frame in TOP mode, its mirror.
void SetLinePoint(void *smf, void *anchor, int point, void *relative, int relativePoint, float x, float y,
int flag) {
if (IsTop(smf)) {
point = Offsets::FRAMEPOINT_TOPLEFT;
if (relativePoint == Offsets::FRAMEPOINT_BOTTOMLEFT) { // the first line, on the frame
relativePoint = Offsets::FRAMEPOINT_TOPLEFT;
y = -Game::Read<float>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_INSET_TOP);
} else {
relativePoint = Offsets::FRAMEPOINT_BOTTOMLEFT;
y = -y;
}
}
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, point, relative, relativePoint, x, y, flag);
}
// The layout's SetPoint for a line it has just made; the line's parent is
// the frame.
void __fastcall AnchorNewLine(void *anchor, void *, int point, void *relative, int relativePoint, float x, float y,
int flag) {
void *line = static_cast<uint8_t *>(anchor) - Offsets::OFF_REGION_ANCHOR;
SetLinePoint(Game::Read<void *>(line, Offsets::OFF_REGION_PARENT), anchor, point, relative, relativePoint, x, y,
flag);
}
// Anchors the frame's lines again, in order, with the layout's arguments and
// steps.
void AnchorLines(void *smf) {
const int count = Game::Read<int>(smf, Offsets::OFF_SMF_LINE_COUNT);
const auto *lines = Game::Read<const uint8_t *>(smf, Offsets::OFF_SMF_LINE_ARRAY);
void *previous = nullptr;
for (int i = 0; i < count; ++i) {
void *line = Game::Read<void *>(lines + i * Offsets::SMF_LINE_STRIDE, Offsets::OFF_SMF_LINE_FONTSTRING);
void *anchor = AnchorOf(line);
reinterpret_cast<ClearAllPoints_t>(Offsets::FUN_LAYOUT_CLEAR_ALL_POINTS)(anchor);
if (previous == nullptr)
SetLinePoint(smf, anchor, Offsets::FRAMEPOINT_BOTTOMLEFT, AnchorOf(smf), Offsets::FRAMEPOINT_BOTTOMLEFT,
Game::Read<float>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_INSET_LEFT),
Game::Read<float>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_INSET_BOTTOM), 1);
else
SetLinePoint(smf, anchor, Offsets::FRAMEPOINT_BOTTOMLEFT, AnchorOf(previous), Offsets::FRAMEPOINT_TOPLEFT,
0.0f,
reinterpret_cast<PixelAlignY_t>(Offsets::FUN_PIXEL_ALIGN_Y)(
Game::Read<float>(line, Offsets::OFF_FONTSTRING_SPACING)),
1);
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(anchor, nullptr, 1);
previous = line;
}
}
int __fastcall Script_SetInsertMode(void *L) {
void *frame = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
const double mode = Game::Lua::IsNumber(L, 2) ? Game::Lua::ToNumber(L, 2) : 0;
if (mode != kInsertModeTop && mode != kInsertModeBottom) {
Game::Lua::Error(L,
"Usage: %s:SetInsertMode(SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP or "
"SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM)",
Game::Lua::ObjectDisplayName(frame));
return 0;
}
const bool top = mode == kInsertModeTop;
if (top == IsTop(frame))
return 0;
if (top)
g_topFrames.insert(frame);
else
g_topFrames.erase(frame);
AnchorLines(frame);
DisplayRefreshed::MarkDirty(frame);
return 0;
}
int __fastcall Script_GetInsertMode(void *L) {
void *frame = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
Game::Lua::PushNumber(L, IsTop(frame) ? kInsertModeTop : kInsertModeBottom);
return 1;
}
const Game::Lua::FrameMethodEntry kMethods[] = {
{"SetInsertMode", &Script_SetInsertMode},
{"GetInsertMode", &Script_GetInsertMode},
};
const Game::Doc::Field kModeArgs[] = {
Game::Doc::Req("mode", "number",
"SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP or SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM."),
};
const Game::Doc::Field kModeRets[] = {
Game::Doc::Req("mode", "number", "BOTTOM until SetInsertMode changes it."),
};
const Game::Doc::Function kSetInsertMode{
"Sets where the newest message shows: at the top, with older messages below it, or at the bottom.", kModeArgs,
{}};
const Game::Doc::Function kGetInsertMode{"Returns where the newest message shows.", {}, kModeRets};
const Game::Doc::Method kDocs[] = {
{"SetInsertMode", &kSetInsertMode},
{"GetInsertMode", &kGetInsertMode},
};
void RegisterLuaFunctions() {
void *L = Game::Lua::State();
Game::Lua::SetGlobalNumber(L, "SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP", kInsertModeTop);
Game::Lua::SetGlobalNumber(L, "SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM", kInsertModeBottom);
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
kMethods, 2, kDocs, 2);
Common::RedirectCall(Offsets::CALL_SMF_LAYOUT_ANCHOR_LINE, Offsets::FUN_REGION_SET_POINT,
reinterpret_cast<const void *>(&AnchorNewLine));
}
void PrepareForReload() { g_topFrames.clear(); }
void OnFrameDestroyed(void *frame) { g_topFrames.erase(frame); }
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
const Game::ReloadAutoRegister _reload{&PrepareForReload};
const Frame::Destruction::AutoSubscribe _destructionSub{&OnFrameDestroyed};
} // namespace
} // namespace ScrollingMessageFrame::InsertMode
+41
View File
@@ -0,0 +1,41 @@
// 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 "scrollingmessageframe/Layout.h"
#include "Game.h"
#include "Offsets.h"
namespace ScrollingMessageFrame::Layout {
namespace {
AutoSubscribe *g_head = nullptr;
using Layout_t = void(__fastcall *)(void *smf, void *edx, int newestRecord);
Layout_t g_origLayout = nullptr;
void __fastcall Layout_h(void *smf, void *edx, int newestRecord) {
g_origLayout(smf, edx, newestRecord);
for (const AutoSubscribe *s = g_head; s != nullptr; s = s->next)
s->fn(smf);
}
const Game::HookAutoRegister _hook{Offsets::FUN_SMF_DISPLAY_REFRESH, reinterpret_cast<void *>(&Layout_h),
reinterpret_cast<void **>(&g_origLayout)};
} // namespace
AutoSubscribe::AutoSubscribe(Listener fn) : fn(fn), next(g_head) { g_head = this; }
} // namespace ScrollingMessageFrame::Layout
+35
View File
@@ -0,0 +1,35 @@
// 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
// Shared observer for the ScrollingMessageFrame line layout
// FUN_SMF_DISPLAY_REFRESH (Offsets.h), which lays out the lines the frame
// shows after every change to them: a message added at the bottom, a scroll,
// a resize, a font change, Clear. A listener runs after it, with the frame.
//
// MinHook allows ONE hook per address and more than one feature follows the
// layout (Text::InlineTexture's early icon apply, the display-refreshed
// callbacks), so this module owns the hook and fans out. Declare a file-scope
// static const ScrollingMessageFrame::Layout::AutoSubscribe _sub{&OnLayout};
namespace ScrollingMessageFrame::Layout {
using Listener = void (*)(void *smf);
struct AutoSubscribe {
explicit AutoSubscribe(Listener fn);
Listener fn;
AutoSubscribe *next;
};
} // namespace ScrollingMessageFrame::Layout
+494
View File
@@ -0,0 +1,494 @@
// 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/>.
// `ScrollingMessageFrame:GetMessageInfo(index)`, retail's: the message's
// text, its colour, then every argument AddMessage took after the colour.
//
// Retail's ScrollingMessageFrame is a Lua mixin whose AddMessage(message, r,
// g, b, ...) packs the call into an entry and GetMessageInfo unpacks it
// (Blizzard_SharedXML/ScrollingMessageFrame.lua). Index 1 is the oldest
// message; the extra arguments come back as given, nils and count included;
// an index outside 1..GetNumMessages() returns nothing. (4.3.4's C method
// returns text, accessID, typeID, extraData instead.)
//
// The text and colour come from the engine's own record of the message
// (Offsets.h, OFF_SCROLLINGMESSAGEFRAME_MESSAGES), so a colour
// UpdateColorByID changed reads back changed, as retail's AdjustMessageColors
// does. The record keeps the colour as bytes, so a component reads back as
// byte/255. It keeps the first extra argument only as an integer (0 for
// anything else), so the extra arguments are kept beside the ring instead.
//
// AddMessage is registered again on the ScrollingMessageFrame registry, over
// the engine's. The registrar (0x00701D80) adds a node for the name whatever
// is there; Storm's TSHashTable::InternalNew links a new node at the head of
// its bucket (NewNode(1)), and the method lookup (0x00702000) returns the
// first match, so this one answers. It runs the engine's AddMessage, then
// records the extra arguments for the record the engine wrote. The engine
// adds a message exactly when the text is a nonempty string
// (0x0079299C..0x007929C7: lua_isstring, lua_tostring, first byte); a bad
// `self` raises a Lua error instead. The ring's one writer has no other
// caller, so every record was written through here.
//
// The extra arguments live in a registry table keyed by the frame's Lua table,
// then by record. A record's table is refilled when the record is overwritten,
// so a full ring makes no garbage.
//
// `BackFillMessage(text, r, g, b, ...)`, retail's, adds a message as the
// OLDEST — the combat log's refilter walks its history newest to oldest and
// fills the frame behind the lines it already shows (Blizzard_CombatLog's
// RefilterUpdate). It writes the ring itself; see the function.
//
// `RemoveMessagesByPredicate(predicate)`, retail's, removes the messages the
// predicate selects by their GetMessageInfo values. The ring has no
// compaction of its own; 4.3.4's RemoveLines loop is run over it, the
// extra-argument tables moving with their records. See the function.
#include "Game.h"
#include "Offsets.h"
#include "scrollingmessageframe/DisplayRefreshed.h"
#include <cstdint>
#include <cstring>
#include <vector>
namespace ScrollingMessageFrame::MessageInfo {
namespace {
constexpr int kFirstExtra = 6; // self, text, r, g, b, then the extra arguments
// The frame-table → record → extra-arguments table, in the registry of the Lua
// state that made it. LUA_NOREF until first use in each state.
constexpr int kNoRef = -2;
int g_extrasRef = kNoRef;
// Pushes the record → extra-arguments table of the frame whose Lua table is
// at stack slot 1, or nil when it has none and `create` is false.
void PushRecords(void *L, bool create) {
using namespace Game::Lua;
if (g_extrasRef == kNoRef) {
if (!create) {
PushNil(L);
return;
}
NewTable(L);
g_extrasRef = Ref(L, REGISTRY_INDEX);
}
RawGetI(L, REGISTRY_INDEX, g_extrasRef); // [frames]
PushValue(L, 1);
RawGet(L, -2); // [frames, records]
if (Type(L, -1) != TYPE_TABLE && create) {
SetTop(L, -2);
NewTable(L);
PushValue(L, 1);
PushValue(L, -2);
RawSet(L, -4); // frames[self] = records
}
Remove(L, -2); // [records]
}
// Pushes the extra-argument table of record `record` of the frame whose Lua
// table is at stack slot 1, or nil when it has none and `create` is false.
void PushExtras(void *L, int record, bool create) {
using namespace Game::Lua;
PushRecords(L, create); // [records]
if (Type(L, -1) != TYPE_TABLE)
return;
RawGetI(L, -1, record); // [records, extras]
if (Type(L, -1) != TYPE_TABLE && create) {
SetTop(L, -2);
NewTable(L);
PushNumber(L, record);
PushValue(L, -2);
RawSet(L, -4); // records[record] = extras
}
Remove(L, -2); // [extras]
}
// The `n` of the extra-argument table at absolute stack slot `extras`.
int ExtraCount(void *L, int extras) {
using namespace Game::Lua;
PushString(L, "n");
RawGet(L, extras);
const int n = static_cast<int>(ToNumber(L, -1));
SetTop(L, -2);
return n;
}
// Stores stack slots kFirstExtra..top as the extra arguments of `record`.
void StoreExtras(void *L, int record, int top) {
using namespace Game::Lua;
const int count = top >= kFirstExtra ? top - kFirstExtra + 1 : 0;
PushExtras(L, record, count > 0);
const int extras = GetTop(L);
if (Type(L, extras) == TYPE_TABLE) {
const int was = ExtraCount(L, extras);
for (int i = 1; i <= count; ++i) {
PushNumber(L, i);
PushValue(L, kFirstExtra - 1 + i);
RawSet(L, extras);
}
for (int i = count + 1; i <= was; ++i) {
PushNumber(L, i);
PushNil(L);
RawSet(L, extras);
}
PushString(L, "n");
PushNumber(L, count);
RawSet(L, extras);
}
SetTop(L, -2);
}
int __fastcall Script_AddMessage(void *L) {
const int top = Game::Lua::GetTop(L);
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_SCROLLINGMESSAGEFRAME_ADD_MESSAGE)(L);
const char *text = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
if (text == nullptr || *text == '\0')
return 0;
void *frame = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
StoreExtras(L, Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS), top);
// Scrolled up, the engine adds the message without a layout.
DisplayRefreshed::MarkDirty(frame);
return 0;
}
// AddMessage's colour byte (0x00792A1B..0x00792ACC): the value clamped to
// 0..1 (NaN to 1), then trunc(v × 255 + 0.5).
uint32_t ColorByte(double value) {
double clamped = 1.0;
if (value < 0.0)
clamped = 0.0;
else if (value < 1.0)
clamped = static_cast<float>(value);
return static_cast<uint32_t>(static_cast<int>(clamped * 255.0 + 0.5)) & 0xFFu;
}
// The colour AddMessage builds from stack slots 3..5 — alpha 0xFF, R, G, B —
// or the frame font's when they are not all numbers (0x007929CD..0x00792B45).
uint32_t MessageColor(void *L, void *frame) {
using namespace Game::Lua;
if (IsNumber(L, 3) && IsNumber(L, 4) && IsNumber(L, 5))
return 0xFF000000u | (ColorByte(ToNumber(L, 3)) << 16) | (ColorByte(ToNumber(L, 4)) << 8) |
ColorByte(ToNumber(L, 5));
const void *font = Game::Read<const void *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_FONT);
return font != nullptr ? Game::Read<uint32_t>(font, Offsets::OFF_SCROLLINGMESSAGEFRAME_FONT_COLOR) : 0xFFFFFFFFu;
}
// `ScrollingMessageFrame:BackFillMessage(text, r, g, b, ...)`, retail's: the
// message becomes the OLDEST. Retail's mixin pushes the entry at the back of
// its history buffer and drops it when the buffer is full ("Won't overwrite
// front"); 4.3.4's AddMessage(..., addToTop) writes the same slot but, full,
// overwrites the oldest. The slot is 4.3.4's: once the count has grown, the
// free one before the oldest, `(pos - count + 1) mod max` (pos 0 for an empty
// ring), and the record is written as the ring writer 0x00788150 writes it —
// the text a Storm copy (the ring frees its texts with SMemFree), AddMessage's
// colour, shown, the frame's two times, id 0. Nothing already in the ring
// moves, so a scrolled-up frame keeps its view; at the bottom the lines are
// laid out again from the newest, as the writer does, which shows the new
// line when there is room above the others.
int __fastcall Script_BackFillMessage(void *L) {
using namespace Game::Lua;
using StormAlloc_t = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
using StormFree_t = int(__stdcall *)(void *ptr, const char *file, int line, int flags);
using Layout_t = void(__fastcall *)(void *smf, void *edx, int newestRecord);
static const char kStormFile[] = "ClassicAPI ScrollingMessageFrame:BackFillMessage";
const int top = GetTop(L);
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
const char *text = IsString(L, 2) ? ToString(L, 2) : nullptr;
if (text == nullptr || *text == '\0')
return 0;
const int max = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
int &count = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
if (max <= 0 || count >= max)
return 0; // full: the message is older than every kept one
const uint32_t color = MessageColor(L, frame);
const size_t bytes = std::strlen(text) + 1;
char *copy = static_cast<char *>(
reinterpret_cast<StormAlloc_t>(Offsets::FUN_STORM_SMEM_ALLOC)(bytes, kStormFile, __LINE__, 0));
if (copy == nullptr)
return 0;
std::memcpy(copy, text, bytes);
++count;
int &pos = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS);
if (pos == -1)
pos = 0;
int slot = pos - count + 1;
if (slot < 0)
slot += max;
uint8_t *record = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES) +
slot * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
char *&stored = *reinterpret_cast<char **>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT);
if (stored != nullptr)
reinterpret_cast<StormFree_t>(Offsets::FUN_STORM_SMEM_FREE)(stored, kStormFile, __LINE__, 0);
stored = copy;
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COLOR) = color;
*reinterpret_cast<int *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_SHOWN) = 1;
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_VISIBLE_TIME) =
Game::Read<uint32_t>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_VISIBLE_TIME);
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_FADE_TIME) =
Game::Read<uint32_t>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_FADE_TIME);
*reinterpret_cast<int *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_ID) = 0;
StoreExtras(L, slot, top);
if (Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_AT_BOTTOM) != 0)
reinterpret_cast<Layout_t>(Offsets::FUN_SMF_DISPLAY_REFRESH)(frame, nullptr, pos);
// Scrolled up, the message lands without a layout.
DisplayRefreshed::MarkDirty(frame);
return 0;
}
// The record of message `index`, 1 the oldest: (position - count + index),
// plus the limit when negative.
int RecordOf(void *frame, int index) {
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
int record = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS) - count + index;
if (record < 0)
record += Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
return record;
}
// Pushes a record's GetMessageInfo values — its text, r, g, b, then its extra
// arguments — for the frame whose Lua table is at stack slot 1, and returns
// how many.
int PushMessage(void *L, void *frame, int record) {
using namespace Game::Lua;
const auto *message = Game::Read<const uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES) +
record * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
const uint32_t color = Game::Read<uint32_t>(message, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COLOR);
PushExtras(L, record, false);
const int extras = GetTop(L);
const int n = Type(L, extras) == TYPE_TABLE ? ExtraCount(L, extras) : 0;
CheckStack(L, 4 + n);
PushString(L, Game::Read<const char *>(message, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT));
PushNumber(L, ((color >> 16) & 0xFF) / 255.0);
PushNumber(L, ((color >> 8) & 0xFF) / 255.0);
PushNumber(L, (color & 0xFF) / 255.0);
for (int i = 1; i <= n; ++i)
RawGetI(L, extras, i);
Remove(L, extras);
return 4 + n;
}
int __fastcall Script_GetMessageInfo(void *L) {
using namespace Game::Lua;
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
if (!IsNumber(L, 2)) {
Error(L, "Usage: %s:GetMessageInfo(index)", ObjectDisplayName(frame));
return 0;
}
const double index = ToNumber(L, 2);
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
if (!(index >= 1 && index <= count) || index != static_cast<int>(index))
return 0;
return PushMessage(L, frame, RecordOf(frame, static_cast<int>(index)));
}
// `ScrollingMessageFrame:RemoveMessagesByPredicate(predicate)`, retail's: the
// predicate is called with each message's GetMessageInfo values, oldest
// first, and the messages it returns true for are removed; the rest keep
// their order. Retail's mixin rebuilds its history buffer and re-renders at
// the kept scroll offset. The removal is 4.3.4's RemoveLines over 1.12's
// ring: every kept record moves forward in ring order to the first free slot
// (freeing the text it lands on, nulling the one it left), the position moves
// back by the removed count, and the extra-argument tables move with their
// records. Then the engine's own layout from the record at the kept offset
// (clamped to the oldest) and its re-arm — a layout at the bottom keeps the
// fade state, so 4.3.4 re-arms after — or its Clear when nothing is kept.
//
// The predicate runs for every message before anything moves (retail builds
// its new list first). A predicate that errors keeps that message, as
// retail's safesecurecall does; the engine's error handler reports it.
int __fastcall Script_RemoveMessagesByPredicate(void *L) {
using namespace Game::Lua;
using Layout_t = void(__fastcall *)(void *smf, void *edx, int newestRecord);
using Smf_t = void(__fastcall *)(void *smf);
using ScrollOffset_t = int(__fastcall *)(void *smf);
using StormFree_t = int(__stdcall *)(void *ptr, const char *file, int line, int flags);
static const char kStormFile[] = "ClassicAPI ScrollingMessageFrame:RemoveMessagesByPredicate";
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
if (Type(L, 2) != TYPE_FUNCTION) {
Error(L, "Usage: %s:RemoveMessagesByPredicate(predicate)", ObjectDisplayName(frame));
return 0;
}
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
const int max = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
if (count <= 0 || max <= 0)
return 0;
const int top = GetTop(L);
// The predicate over every message, through the engine's error handler.
int errIdx = 0;
const int errRef = Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
if (errRef > 0) {
RawGetI(L, REGISTRY_INDEX, errRef);
errIdx = GetTop(L);
}
std::vector<uint8_t> remove(static_cast<size_t>(count), 0);
int kept = count;
for (int i = 0; i < count; ++i) {
CheckStack(L, 2);
PushValue(L, 2);
const int args = PushMessage(L, frame, RecordOf(frame, i + 1));
if (PCall(L, args, 1, errIdx) == 0 && ToBoolean(L, -1) != 0) {
remove[static_cast<size_t>(i)] = 1;
--kept;
}
SetTop(L, -2);
}
SetTop(L, top);
if (kept == count)
return 0;
// 4.3.4's RemoveLines: compact the kept records forward in ring order.
const int offset = reinterpret_cast<ScrollOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET)(frame);
int &pos = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS);
uint8_t *records = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES);
PushRecords(L, false);
const int table = GetTop(L);
const bool extras = Type(L, table) == TYPE_TABLE;
int src = pos - count + 1;
if (src < 0)
src += max;
int dst = src;
for (int i = 0; i < count; ++i) {
if (!remove[static_cast<size_t>(i)]) {
if (src != dst) {
uint8_t *to = records + dst * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
uint8_t *from = records + src * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
char *&landedOn = *reinterpret_cast<char **>(to + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT);
if (landedOn != nullptr)
reinterpret_cast<StormFree_t>(Offsets::FUN_STORM_SMEM_FREE)(landedOn, kStormFile, __LINE__, 0);
std::memcpy(to, from, Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE);
*reinterpret_cast<char **>(from + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT) = nullptr;
if (extras) {
PushNumber(L, dst);
RawGetI(L, table, src);
RawSet(L, table); // records[dst] = records[src]
PushNumber(L, src);
PushNil(L);
RawSet(L, table); // records[src] = nil
}
}
dst = (dst + 1) % max;
} else if (extras) {
PushNumber(L, src);
PushNil(L);
RawSet(L, table); // records[src] = nil
}
src = (src + 1) % max;
}
SetTop(L, top);
if (kept == 0) {
reinterpret_cast<Smf_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_CLEAR)(frame);
DisplayRefreshed::MarkDirty(frame);
return 0;
}
pos += kept - count;
if (pos < 0)
pos += max;
Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT) = kept;
// The lines again from the record at the kept offset, then re-armed.
const int shown = (offset < kept) ? (offset < 0 ? 0 : offset) : kept - 1;
int record = pos - shown;
if (record < 0)
record += max;
reinterpret_cast<Layout_t>(Offsets::FUN_SMF_DISPLAY_REFRESH)(frame, nullptr, record);
reinterpret_cast<Smf_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_REARM_LINES)(frame);
DisplayRefreshed::MarkDirty(frame);
return 0;
}
const Game::Lua::FrameMethodEntry kMethods[] = {
{"AddMessage", &Script_AddMessage},
{"BackFillMessage", &Script_BackFillMessage},
{"GetMessageInfo", &Script_GetMessageInfo},
{"RemoveMessagesByPredicate", &Script_RemoveMessagesByPredicate},
};
const Game::Doc::Field kAddMessageArgs[] = {
Game::Doc::Req("text", "string"),
Game::Doc::Opt("r", "number", nullptr, "Without r, g and b the message takes the frame's font colour."),
Game::Doc::Opt("g", "number"),
Game::Doc::Opt("b", "number"),
Game::Doc::Vararg("any", "Kept with the message; GetMessageInfo returns them after its colour."),
};
const Game::Doc::Function kAddMessage{"Adds a message to the frame.", kAddMessageArgs, {}};
const Game::Doc::Field kBackFillMessageArgs[] = {
Game::Doc::Req("text", "string"),
Game::Doc::Opt("r", "number", nullptr, "Without r, g and b the message takes the frame's font colour."),
Game::Doc::Opt("g", "number"),
Game::Doc::Opt("b", "number"),
Game::Doc::Vararg("any", "Kept with the message; GetMessageInfo returns them after its colour."),
};
const Game::Doc::Function kBackFillMessage{
"Adds a message as the oldest, to fill in history behind the messages the frame has. A frame at "
"its line limit drops it.",
kBackFillMessageArgs, {}};
const Game::Doc::Field kGetMessageInfoArgs[] = {
Game::Doc::Req("index", "luaIndex", "1 is the oldest message."),
};
const Game::Doc::Field kGetMessageInfoRets[] = {
Game::Doc::Req("text", "string"),
Game::Doc::Req("r", "number"),
Game::Doc::Req("g", "number"),
Game::Doc::Req("b", "number"),
Game::Doc::Vararg("any", "The arguments AddMessage took after the colour."),
};
const Game::Doc::Function kGetMessageInfo{
"Returns a message's text, its colour, and the extra arguments it was added with, or nothing "
"when there is no message at that index.",
kGetMessageInfoArgs, kGetMessageInfoRets};
const Game::Doc::Field kRemoveMessagesByPredicateArgs[] = {
Game::Doc::Req("predicate", "function",
"Called with each message's GetMessageInfo values, oldest first; true removes the message."),
};
const Game::Doc::Function kRemoveMessagesByPredicate{
"Removes the messages the predicate returns true for. The rest keep their order, and the frame "
"keeps its scroll offset.",
kRemoveMessagesByPredicateArgs, {}};
const Game::Doc::Method kDocs[] = {
{"AddMessage", &kAddMessage},
{"BackFillMessage", &kBackFillMessage},
{"GetMessageInfo", &kGetMessageInfo},
{"RemoveMessagesByPredicate", &kRemoveMessagesByPredicate},
};
void RegisterLuaFunctions() {
// Each registration pass is a new Lua state; the old state's ref is gone.
g_extrasRef = kNoRef;
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
kMethods, 4, kDocs, 4);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace ScrollingMessageFrame::MessageInfo
+142
View File
@@ -0,0 +1,142 @@
// 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/>.
// Retail's scroll offset on ScrollingMessageFrame: `SetScrollOffset(offset)`,
// `GetScrollOffset()`, `GetMaxScrollRange()` and `ScrollByAmount(amount)`.
//
// Retail's offset (Blizzard_SharedXML/ScrollingMessageFrame.lua) counts the
// lines the frame is scrolled up from the newest message, 0 at the bottom, up
// to GetMaxScrollRange() = max(GetNumMessages() - 1, 0), where the oldest
// message is the bottom line - the place 1.12's own ScrollToTop goes. 1.12
// keeps the same offset: ScrollToOffset and ScrollOffset (Offsets.h) are its
// two directions, and GetScrollOffset is the engine's GetCurrentScroll under
// retail's name. Retail clamps the offset to its range; the engine wraps one
// outside it, so the setters clamp first. SetScrollOffset to the offset the
// frame has does nothing, as on retail. ScrollByAmount always calls the
// engine, which re-arms the shown lines' fade when the offset stays: retail's
// ResetAllFadeTimes.
#include "Game.h"
#include "Offsets.h"
#include "scrollingmessageframe/DisplayRefreshed.h"
namespace ScrollingMessageFrame::Scroll {
namespace {
using ScrollToOffset_t = void(__fastcall *)(void *smf, void *edx, int offset);
using ScrollOffset_t = int(__fastcall *)(void *smf);
int ScrollOffset(void *smf) {
return reinterpret_cast<ScrollOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET)(smf);
}
int MaxScrollRange(void *smf) {
const int count = Game::Read<int>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
return count > 1 ? count - 1 : 0;
}
// `offset` clamped to 0..MaxScrollRange (NaN to 0).
int Clamped(void *smf, double offset) {
const int max = MaxScrollRange(smf);
return offset >= 0 ? (offset < max ? static_cast<int>(offset) : max) : 0;
}
void ScrollTo(void *smf, int offset) {
reinterpret_cast<ScrollToOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_TO_OFFSET)(smf, nullptr, offset);
}
void *ResolveSmf(void *L) {
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
}
int __fastcall Script_SetScrollOffset(void *L) {
void *frame = ResolveSmf(L);
if (frame == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: %s:SetScrollOffset(offset)", Game::Lua::ObjectDisplayName(frame));
return 0;
}
const int offset = Clamped(frame, Game::Lua::ToNumber(L, 2));
if (offset != ScrollOffset(frame))
ScrollTo(frame, offset);
return 0;
}
int __fastcall Script_GetMaxScrollRange(void *L) {
void *frame = ResolveSmf(L);
if (frame == nullptr)
return 0;
Game::Lua::PushNumber(L, MaxScrollRange(frame));
return 1;
}
int __fastcall Script_ScrollByAmount(void *L) {
void *frame = ResolveSmf(L);
if (frame == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: %s:ScrollByAmount(amount)", Game::Lua::ObjectDisplayName(frame));
return 0;
}
ScrollTo(frame, Clamped(frame, ScrollOffset(frame) + Game::Lua::ToNumber(L, 2)));
DisplayRefreshed::MarkDirty(frame);
return 0;
}
const Game::Lua::FrameMethodEntry kMethods[] = {
{"SetScrollOffset", &Script_SetScrollOffset},
{"GetScrollOffset",
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_SCROLLINGMESSAGEFRAME_GET_CURRENT_SCROLL)},
{"GetMaxScrollRange", &Script_GetMaxScrollRange},
{"ScrollByAmount", &Script_ScrollByAmount},
};
const Game::Doc::Field kOffsetArgs[] = {
Game::Doc::Req("offset", "number", "Lines from the newest message; clamped to 0..GetMaxScrollRange()."),
};
const Game::Doc::Field kOffsetRets[] = {
Game::Doc::Req("offset", "number", "Lines from the newest message; 0 when not scrolled."),
};
const Game::Doc::Field kRangeRets[] = {
Game::Doc::Req("maxScrollRange", "number"),
};
const Game::Doc::Field kAmountArgs[] = {
Game::Doc::Req("amount", "number", "Positive scrolls toward older messages."),
};
const Game::Doc::Function kSetScrollOffset{"Scrolls the frame to a number of lines from the newest message.",
kOffsetArgs, {}};
const Game::Doc::Function kGetScrollOffset{"Returns how many lines the frame is scrolled from the newest message.",
{}, kOffsetRets};
const Game::Doc::Function kGetMaxScrollRange{"Returns the largest scroll offset: one less than the message count.",
{}, kRangeRets};
const Game::Doc::Function kScrollByAmount{"Scrolls the frame by a number of lines and shows the faded lines again.",
kAmountArgs, {}};
const Game::Doc::Method kDocs[] = {
{"SetScrollOffset", &kSetScrollOffset},
{"GetScrollOffset", &kGetScrollOffset},
{"GetMaxScrollRange", &kGetMaxScrollRange},
{"ScrollByAmount", &kScrollByAmount},
};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
kMethods, 4, kDocs, 4);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace ScrollingMessageFrame::Scroll
+2
View File
@@ -88,6 +88,7 @@
#include "time/Clock.h"
#include "unit/Focus.h"
#include "unit/Identity.h"
#include "unit/IncomingResurrection.h"
#include <cstdint>
@@ -691,6 +692,7 @@ void ParseChannelUpdate(Net::CDataStore *packet) {
void HandleCastAborted(uint64_t guid, int spellID, bool castSlotOnly = false) {
if (guid == 0 || spellID == 0)
return;
Unit::IncomingResurrection::OnCastEnded(guid, spellID);
// Remote units: invalidate the cached cast/channel so
// UnitCastingInfo/UnitChannelInfo stop reporting it this tick.
for (auto &e : g_remoteCasts) {
+10
View File
@@ -52,6 +52,16 @@ bool IsFitToFamily(const uint8_t *spellRecord, uint32_t family,
return (flags & flagMask) != 0;
}
bool HasEffect(const uint8_t *spellRecord, int effect) {
if (spellRecord == nullptr)
return false;
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
if (Game::Read<int>(spellRecord, Offsets::OFF_SPELL_RECORD_EFFECT + i * 4) == effect)
return true;
}
return false;
}
int SpellbookSlotToID(int slot1Based, int bookType) {
const int slot = slot1Based - 1;
if (slot < 0 || slot >= Offsets::SPELLBOOK_MAX_SLOTS)
+4
View File
@@ -31,6 +31,10 @@ const uint8_t *RecordForID(int spellID);
bool IsFitToFamily(const uint8_t *spellRecord, uint32_t family,
uint64_t flagMask);
// Whether one of the spell's three effects is `effect` — 4.3.4's
// SpellHasEffect. False for a null record.
bool HasEffect(const uint8_t *spellRecord, int effect);
// Resolves a 1-based Lua-facing spellbook slot to the spellID stored
// there. `bookType` is `0` for the player spellbook, `1` for the pet
// spellbook (matches the engine's encoding from `Script_GetSpellName`).
+2 -25
View File
@@ -15,6 +15,7 @@
#include "Game.h"
#include "Offsets.h"
#include "object/Corpse.h"
#include "object/Resolve.h"
#include "spell/Lookup.h"
#include "unit/Position.h"
@@ -114,8 +115,6 @@ constexpr int32_t TARGET_PET = 5;
using Relation_t = bool(__thiscall *)(void *caster, void *other);
using CreatureType_t = int(__thiscall *)(void *unit);
using EnumCallback_t = int(__fastcall *)(void *ctx, void *unusedEdx, uint64_t guid);
using EnumVisibleObjects_t = int(__fastcall *)(EnumCallback_t cb, void *ctx);
bool Relation(uintptr_t fn, void *caster, void *other) {
return reinterpret_cast<Relation_t>(fn)(caster, other);
@@ -181,28 +180,6 @@ uint32_t TargetWord(const uint8_t *rec, bool &aimsAtPet) {
return word;
}
struct CorpseScan {
uint64_t owner;
void *corpse;
};
int __fastcall CorpseScanCallback(CorpseScan *scan, void * /*unusedEdx*/, uint64_t guid) {
void *obj = Object::ByGuid(Offsets::TYPEMASK_CORPSE, guid, nullptr, 0);
if (obj == nullptr ||
Game::Read<uint64_t>(Fields(obj), Offsets::OFF_CORPSE_FIELD_OWNER) != scan->owner)
return 1;
scan->corpse = obj;
return 0;
}
// 4.3.4's ClntObjMgrFindCorpseByOwner, over the visible objects.
void *CorpseOf(uint64_t owner) {
CorpseScan scan{owner, nullptr};
reinterpret_cast<EnumVisibleObjects_t>(Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS)(
reinterpret_cast<EnumCallback_t>(&CorpseScanCallback), &scan);
return scan.corpse;
}
// A unit target (0x006E4511..0x006E469A).
bool AdmitsUnit(void *caster, const uint8_t *rec, uint32_t word, void *unit) {
const uint8_t *fields = Fields(unit);
@@ -281,7 +258,7 @@ void *AdmittedTarget(void *caster, const uint8_t *rec, void *target) {
if (info != nullptr &&
(Game::Read<uint32_t>(info, Offsets::OFF_PLAYER_INFO_FLAGS) &
Offsets::PLAYER_FLAG_GHOST) != 0) {
if (void *corpse = CorpseOf(Game::Read<uint64_t>(Head(target), 0)))
if (void *corpse = Object::Corpse::ByOwner(Game::Read<uint64_t>(Head(target), 0)))
return corpse;
}
}
+31 -9
View File
@@ -79,6 +79,7 @@
#include "Common.h"
#include "Game.h"
#include "Offsets.h"
#include "scrollingmessageframe/Layout.h"
#include "text/InlineTexturePool.h"
#include "text/PtrProbe.h"
#include "texture/Atlas.h"
@@ -2085,7 +2086,8 @@ static const Game::HookAutoRegister _paintHook{Offsets::FUN_TEXT_PAINT,
// So a line's icons are recorded — and its regions placed — one frame AFTER its
// glyphs draw, because a frame's textures (our icon regions) draw before its
// fontstring glyphs and the placement isn't computed until the glyph build runs
// mid-paint. This co-hook closes it: after the refresh sets up the lines (still
// mid-paint. This layout listener (ScrollingMessageFrame::Layout, which owns
// the hook) closes it: after the refresh sets up the lines (still
// PRE-RENDER — the refresh runs from the SMF's Lua/event-driven methods, never
// mid-render), force each icon-bearing line to resolve+build and apply its icon
// placement synchronously, so the icons draw with their glyphs the same frame.
@@ -2096,9 +2098,6 @@ static const Game::HookAutoRegister _paintHook{Offsets::FUN_TEXT_PAINT,
// disabled independently of the whole feature; the SEH latch trips it on a fault.
bool g_earlyApply = true;
using SmfRefresh_t = void(__fastcall *)(void *smf, void *edx, int newestIdx);
SmfRefresh_t g_smfRefreshOriginal = nullptr;
using EnsureBuilt_t = int(__fastcall *)(void *node);
using RealizeAnchor_t = void(__thiscall *)(void *anchor, int flag);
using RebuildStringFs_t = void(__fastcall *)(void *fs);
@@ -2183,15 +2182,12 @@ void SafeSmfEarlyApply(void *smf) {
}
}
void __fastcall SmfRefresh_h(void *smf, void *edx, int newestIdx) {
g_smfRefreshOriginal(smf, edx, newestIdx);
void OnSmfLayout(void *smf) {
if (g_inlineEnabled && g_earlyApply)
SafeSmfEarlyApply(smf);
}
static const Game::HookAutoRegister _smfRefreshHook{
Offsets::FUN_SMF_DISPLAY_REFRESH, reinterpret_cast<void *>(&SmfRefresh_h),
reinterpret_cast<void **>(&g_smfRefreshOriginal)};
static const ScrollingMessageFrame::Layout::AutoSubscribe _smfLayoutSub{&OnSmfLayout};
// --- Lua control surface ---------------------------------------------------
@@ -2238,6 +2234,32 @@ void PrepareForReload() {
g_nodeBakedColor.clear();
}
bool LineIconShift(void *fs, const uint8_t *text, int len, float &before, float &after) {
before = 0.0f;
after = 0.0f;
if (!g_inlineEnabled || !LooksReadable(fs) || !LooksReadable(text) || len <= 0 ||
!HasInlineTexture(text, len))
return false;
FsPenFont pf;
if (!ResolveFsPenFont(fs, pf))
return false;
const float overflow = IconOverflowPx(text, len, pf.fontHPen, pf.layoutScale);
if (!(overflow > 0.0f))
return false;
// The node's justify-V as the block creator (FUN_0044d420) reads the
// style word: TOP (8) wins, then BOTTOM (0x20), else middle — the value
// the emitter reads from the node to split the move.
const uint32_t style = Game::Read<uint32_t>(fs, Offsets::OFF_FONTSTRING_FLAGS);
const int justifyV = (style & 0x8u) ? 0 : (style & 0x20u) ? 2 : 1;
const float move = (justifyV == 1) ? std::floor(overflow * 0.5f) : overflow;
const float toFs = pf.fontHInt / pf.fontHPen; // pen px → the getter's units
if (justifyV != 0)
before = move * toFs;
if (justifyV != 2)
after = move * toFs;
return true;
}
static const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
} // namespace Text::InlineTexture
+11
View File
@@ -13,6 +13,8 @@
#pragma once
#include <cstdint>
namespace Text::InlineTexture {
// Called from FrameScript_Initialize_h before the /reload teardown: the reload
@@ -20,4 +22,13 @@ namespace Text::InlineTexture {
// icon records are dropped (they rebuild as the reloaded UI re-emits its text).
void PrepareForReload();
// The room the tall inline icons on the line [text, text + len) take when
// `fs` renders it, in the font string's own units (its height getter's):
// `before` is how far the render moves that line's text down, `after` how
// far it moves the lines after it — 4.3.4's InitializeText offsets, split by
// the font string's vertical justify (top: none / all; middle: half / half,
// floored; bottom: all / none). Both 0, and false, without an icon taller
// than the font or with the feature off.
bool LineIconShift(void *fs, const uint8_t *text, int len, float &before, float &after);
} // namespace Text::InlineTexture
+217
View File
@@ -0,0 +1,217 @@
// 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/>.
// `UnitHasIncomingResurrection(unit)` and `INCOMING_RESURRECT_CHANGED` —
// 4.3.4's CGIncomingResurrection (Ui/IncomingResurrection.cpp) and the two
// calls its spell code makes into it:
// - SMSG_SPELL_START: a spell with a resurrect effect and an object target
// adds {caster, spell} under that target
// (PendingSpellCast::OnSpellStartPacket, 0x008ACD80);
// - the cast's end removes it (FinishPendingSpellCast, 0x008AF840, which
// SPELL_GO and every failure run).
// Each change signals INCOMING_RESURRECT_CHANGED to every token naming the
// target, or the corpse's owner when the target is a corpse
// (Script_SendUnitSignal). The query answers for the unit itself, then for
// its corpse (HasIncomingResurrection, 0x009FCF80).
//
// Where 1.12 differs:
// - Resurrect effects. 4.3.4 tests effect 18 alone. 1.12's class
// resurrections (Resurrection, Redemption, Ancestral Spirit, Rebirth,
// every rank) carry 113, RESURRECT_NEW, and 18 only item and NPC spells
// (Spell.dbc), so both count.
// - The cast's end. 1.12 keeps no pending cast. Its end is the caster's
// SMSG_SPELL_GO, or CGUnit_C::ClearCastingSpell and the failure packets,
// which `Spell::Cast` owns and reports through OnCastEnded. Turtle sends
// observers no failure packet on an interrupt; SuperWoW calls
// ClearCastingSpell instead (docs/notes/cast-channel.md).
// - Lifetime. 4.3.4 empties the table on entering and leaving the world. A
// cast whose caster leaves view never reports its end, so an entry whose
// caster is no longer visible ends on the next world tick, which covers
// a world change as well.
// - One cast per caster, as the unit's single precast slot (+0xC8C) holds:
// a caster's new resurrection replaces its last.
#include "unit/IncomingResurrection.h"
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "net/PacketDispatch.h"
#include "object/Corpse.h"
#include "object/Resolve.h"
#include "spell/Lookup.h"
#include "tick/WorldTick.h"
#include "unit/Identity.h"
#include <cstddef>
#include <cstdint>
#include <vector>
namespace Unit::IncomingResurrection {
namespace {
constexpr int SPELL_EFFECT_RESURRECT = 18;
constexpr int SPELL_EFFECT_RESURRECT_NEW = 113;
const Game::Doc::Field kChangedPayload[] = {
Game::Doc::Req("unitTarget", "UnitToken", "The unit, or the player whose corpse it is."),
};
const Game::Doc::Event kChangedDoc{
"Unit", "A resurrection cast at the unit began or ended. Read UnitHasIncomingResurrection.",
kChangedPayload};
const Event::Custom::AutoReserve kChanged{"INCOMING_RESURRECT_CHANGED", &kChangedDoc};
struct Entry {
uint64_t target; // the cast's object target: a unit or a corpse
uint64_t unit; // who hears the change: the corpse's owner, else target
uint64_t caster;
int spellID;
};
std::vector<Entry> g_entries;
void Signal(uint64_t unit) {
const int slot = kChanged.Slot();
if (!Event::Custom::HasListeners(slot))
return;
char tokens[16][32];
const int n = Unit::Identity::TokensForGUID(unit, tokens, 16);
for (int i = 0; i < n; ++i)
Event::Custom::Fire(slot, "%s", tokens[i]);
}
// Ends `caster`'s resurrection: of `spellID`, or any for 0.
void End(uint64_t caster, int spellID) {
for (auto it = g_entries.begin(); it != g_entries.end(); ++it) {
if (it->caster == caster && (spellID == 0 || it->spellID == spellID)) {
const uint64_t unit = it->unit;
g_entries.erase(it);
Signal(unit);
return;
}
}
}
void Begin(uint64_t target, uint64_t caster, int spellID) {
for (const Entry &e : g_entries) {
if (e.caster == caster && e.target == target && e.spellID == spellID)
return;
}
End(caster, 0);
const uint64_t owner = Object::Corpse::OwnerOf(target);
const uint64_t unit = owner != 0 ? owner : target;
g_entries.push_back({target, unit, caster, spellID});
Signal(unit);
}
bool Targeted(uint64_t target) {
for (const Entry &e : g_entries) {
if (e.target == target)
return true;
}
return false;
}
bool HasIncomingResurrection(uint64_t guid) {
if (guid == 0 || g_entries.empty())
return false;
if (Targeted(guid))
return true;
const void *corpse = Object::Corpse::ByOwner(guid);
return corpse != nullptr && Targeted(Unit::Identity::GuidForObject(corpse));
}
bool IsResurrect(int spellID) {
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
return Spell::Lookup::HasEffect(rec, SPELL_EFFECT_RESURRECT) ||
Spell::Lookup::HasEffect(rec, SPELL_EFFECT_RESURRECT_NEW);
}
// SMSG_SPELL_START / SMSG_SPELL_GO: packed item or caster, packed caster,
// spell, then for SPELL_START u16 flags, u32 cast time and the targets.
void OnPacket(uint32_t opcode, Net::CDataStore *p) {
if (opcode != Offsets::SMSG_SPELL_START && opcode != Offsets::SMSG_SPELL_GO)
return;
if (opcode == Offsets::SMSG_SPELL_GO && g_entries.empty())
return;
Net::ReadPackedGuid(p);
const uint64_t caster = Net::ReadPackedGuid(p);
const int spellID = static_cast<int>(Net::Read<uint32_t>(p));
if (opcode == Offsets::SMSG_SPELL_GO) {
if (p->m_read <= p->m_size)
End(caster, spellID);
return;
}
Net::Read<uint16_t>(p);
Net::Read<uint32_t>(p);
const uint16_t targetMask = Net::Read<uint16_t>(p);
const uint64_t target =
(targetMask & Offsets::SPELL_TARGET_FLAGS_OBJECT) ? Net::ReadPackedGuid(p) : 0;
if (p->m_read > p->m_size || caster == 0 || target == 0 || !IsResurrect(spellID))
return;
Begin(target, caster, spellID);
}
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
void OnWorldTick() {
for (size_t i = 0; i < g_entries.size();) {
if (Object::ByGuid(Offsets::TYPEMASK_UNIT, g_entries[i].caster) != nullptr) {
++i;
continue;
}
const uint64_t unit = g_entries[i].unit;
g_entries.erase(g_entries.begin() + static_cast<std::ptrdiff_t>(i));
Signal(unit);
}
}
const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
int __fastcall Script_UnitHasIncomingResurrection(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: UnitHasIncomingResurrection(\"unit\")");
return 0;
}
Game::Lua::PushBool(L, HasIncomingResurrection(
Unit::Identity::GuidForToken(Game::Lua::ToString(L, 1))));
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("hasIncomingResurrection", "bool"),
};
const Game::Doc::Function kUnitHasIncomingResurrection{
"Whether a resurrection is being cast at the unit, or at the corpse of a released player.",
kArgs, kRets, "UnitGlobals"};
void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("UnitHasIncomingResurrection",
&Script_UnitHasIncomingResurrection,
&kUnitHasIncomingResurrection);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
void OnCastEnded(uint64_t caster, int spellID) {
if (caster != 0 && spellID != 0 && !g_entries.empty())
End(caster, spellID);
}
} // namespace Unit::IncomingResurrection
+28
View File
@@ -0,0 +1,28 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
// 4.3.4's CGIncomingResurrection: the resurrections being cast at each unit
// or corpse. Backs `UnitHasIncomingResurrection` and
// `INCOMING_RESURRECT_CHANGED`.
namespace Unit::IncomingResurrection {
// `caster` stopped casting `spellID`: CGUnit_C::ClearCastingSpell or a
// failure packet, both owned by `Spell::Cast`. Ends the resurrection it was
// casting, if that is one.
void OnCastEnded(uint64_t caster, int spellID);
} // namespace Unit::IncomingResurrection