mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
Compare commits
92
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a4d291748 | ||
|
|
1cdd46958f | ||
|
|
72f0e5d98f | ||
|
|
ceb6aaf46b | ||
|
|
84c936faa4 | ||
|
|
957d66af49 | ||
|
|
3849b7513b | ||
|
|
1cdd443e53 | ||
|
|
ada33825ad | ||
|
|
dd584703a2 | ||
|
|
5911298acd | ||
|
|
a6fa06faa0 | ||
|
|
045ab77508 | ||
|
|
ac5df0fab8 | ||
|
|
48822978c6 | ||
|
|
f061179646 | ||
|
|
f8a796ae04 | ||
|
|
aff52e0ef6 | ||
|
|
aef68d5ea4 | ||
|
|
cc1e70a641 | ||
|
|
f1843afc0f | ||
|
|
f8304c06f7 | ||
|
|
2396d44f10 | ||
|
|
1ee80b74e9 | ||
|
|
932e6ed82f | ||
|
|
b032dc09ac | ||
|
|
eb0b70bc5e | ||
|
|
238e35ae53 | ||
|
|
5b7fe94d4d | ||
|
|
43594398f0 | ||
|
|
2eef939b5a | ||
|
|
36f3661b98 | ||
|
|
4f3ee595e7 | ||
|
|
99f72aa631 | ||
|
|
9e5c0c558a | ||
|
|
b3b950f2d8 | ||
|
|
5dcf10a9fd | ||
|
|
3ef6c4ffed | ||
|
|
1ef8c5597a | ||
|
|
9b0aa46ff0 | ||
|
|
84171bef2d | ||
|
|
f19f13d44c | ||
|
|
4509da0966 | ||
|
|
38d5c6c7b6 | ||
|
|
be5ebd4017 | ||
|
|
44f9bcd9af | ||
|
|
f9ec92014c | ||
|
|
264af04ee3 | ||
|
|
2063507fb4 | ||
|
|
090cfc3bfd | ||
|
|
d1cdb47c3e | ||
|
|
237e5a7500 | ||
|
|
d339f4486d | ||
|
|
92025896b5 | ||
|
|
4eb214f7f1 | ||
|
|
706fb1c540 | ||
|
|
e37052c67d | ||
|
|
f8f6dc28b8 | ||
|
|
4472c81137 | ||
|
|
1e1463cf9a | ||
|
|
c21e44e086 | ||
|
|
e76616de83 | ||
|
|
92794a1822 | ||
|
|
ab0991f054 | ||
|
|
26c201b95b | ||
|
|
09693af5b9 | ||
|
|
f24b7e594b | ||
|
|
35c8ba5688 | ||
|
|
e2a34bd18a | ||
|
|
2f787889f7 | ||
|
|
9967cc0839 | ||
|
|
e47e881ae1 | ||
|
|
d9da100110 | ||
|
|
00cfe0c45b | ||
|
|
d92464b86c | ||
|
|
fec127c1af | ||
|
|
37ee71c663 | ||
|
|
6259e22480 | ||
|
|
474da1a5b5 | ||
|
|
6cee968a8a | ||
|
|
635c74d518 | ||
|
|
260d40f49b | ||
|
|
8f6ff33a39 | ||
|
|
5082f3c829 | ||
|
|
5d0568f759 | ||
|
|
9a78e159e7 | ||
|
|
503368623f | ||
|
|
735d304a20 | ||
|
|
29ddd78a80 | ||
|
|
b6c21216e3 | ||
|
|
6cc9d0db90 | ||
|
|
c6c201e575 |
+1
-1
@@ -87,7 +87,7 @@ include(cmake/embed_resources.cmake)
|
||||
embed_resources(
|
||||
TARGET ${PROJECT_NAME}
|
||||
ADDONS_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/src/embed/AddOns"
|
||||
ADDONS "!!!ClassicAPI" "ClassicAPI_DebugTools"
|
||||
ADDONS "!!!ClassicAPI" "ClassicAPI_DebugTools" "ClassicAPI_EventTrace"
|
||||
ADDON_VERSION "${CLASSICAPI_TOC_VERSION}"
|
||||
BINDINGS_XML "${CMAKE_CURRENT_SOURCE_DIR}/src/bindings/Bindings.xml"
|
||||
OUT_DIR "${CMAKE_BINARY_DIR}/generated")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ClassicAPI
|
||||
|
||||
ClassicAPI is a DLL for World of Warcraft 1.12.1 (Vanilla / Turtle WoW). It
|
||||
backports a large part of the modern WoW API — 550+ functions and 50+ events,
|
||||
backports a large part of the modern WoW API — 950+ functions and 80+ events,
|
||||
plus much of Lua 5.1 — into the 1.12 client, so addons written for later
|
||||
versions (3.3.5+ or Classic Era) run with little or no change.
|
||||
|
||||
@@ -23,7 +23,7 @@ reference is in **[docs/API.md](docs/API.md)**.
|
||||
| [Soft targeting](docs/API.md#soft-interact-softinteract--anyinteract) | The Interact Key and soft targets. Bind **Interact With Target** to a key to interact with the nearest NPC, corpse, mailbox, chest or gathering node in front of you. The [`softenemy` and `softfriend`](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend) tokens point at the nearest enemy and friendly unit without a change of target. With no target, harmful spells go to `softenemy` and helpful spells go to `softfriend`. `PLAYER_SOFT_*_CHANGED` events fire when the units change. |
|
||||
| [Animations](docs/API.md#animation) | Animation groups on frames, textures and font strings, made in Lua or XML: `CreateAnimationGroup`, looping, smoothing, scripts and templates, with `Alpha`, `Translation`, `Scale`, `Rotation` and `Path` applied. |
|
||||
| Tooltip line cap | Removes `GameTooltip`'s 30-line limit for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). A tooltip adds a new line pair when it needs one, so long tooltips and comparison blocks (for example, pfUI's eqcompare) keep all their lines. |
|
||||
| [XML attributes](docs/API.md#parentkey-in-xml) | More XML attributes work: `parentKey="Name"` makes `parent.Name` point to a frame, texture or font string, `<OnLoad function="Name"/>` uses a global function as a script handler, `<OnLoad method="Name"/>` uses the frame's own function, `mixin="A, B"` copies mixins onto the object, `<KeyValues>` sets fields on the object, a button's `<NormalFont style="..."/>` sets its font, and `inherits="A, B"` applies more than one template. `parentArray="Name"` adds the object to a list on its parent. An `<Anchor>` takes `x` and `y` offsets and `relativeKey="$parent.Key"`. `<OnEnter inherit="prepend">` runs the template's handler and then its own, and `inherit="append"` runs them in the other order. |
|
||||
| [XML attributes](docs/API.md#parentkey-in-xml) | More XML attributes work: `parentKey="Name"` makes `parent.Name` point to a frame, texture or font string, `<OnLoad function="Name"/>` uses a global function as a script handler, `<OnLoad method="Name"/>` uses the frame's own function, `mixin="A, B"` copies mixins onto the object, `<KeyValues>` sets fields on the object, a button's `<NormalFont style="..."/>` sets its font, and `inherits="A, B"` applies more than one template. `parentArray="Name"` adds the object to a list on its parent. An `<Anchor>` takes `x` and `y` offsets and `relativeKey="$parent.Key"`. `<OnEnter inherit="prepend">` runs the template's handler and then its own, and `inherit="append"` runs them in the other order. A frame with `intrinsic="true"` makes a new frame type. Frames take `scale`, `registerForDrag`, `clipChildren`, `useParentLevel`, `dontSavePosition`, `propagateKeyboardInput`, `propagateMouseInput`, `enableMouseClicks`, `enableMouseMotion` and `motionScriptsWhileDisabled`, and buttons take `registerForClicks`. Textures take `alpha`, `atlas`, `useAtlasSize`, `mask`, `desaturated`, `rotation`, `horizTile`, `vertTile`, `hWrapMode` and `vWrapMode`. Font strings take `alpha`, `wordwrap`, `indented`, `degrees` and `radians`. Status bars take `reverseFill`, `rotatesTexture` and `fillStyle`, and edit boxes take `countInvisibleLetters`. |
|
||||
| [Macro conditions](docs/API.md#more-commands-with-conditions) | `[conditions]` and `@unit` work on 38 slash commands, from `/cast` and `/use` to `/target`, `/follow`, `/cancelaura` and the `/pet` family. With `/cast [@mouseover,harm][] Fireball`, the spell goes to the mouseover unit. With no mouseover unit, it goes to your target. `@cursor` places a ground-target spell or item under the mouse and `@player` places it at your feet. A character name works where a unit token does (`[target=Feral]`). Addons read the same rules through `SecureCmdOptionParse`. |
|
||||
| [`#showtooltip`](docs/API.md#showtooltip-and-show) | A macro button shows the spell or item that its directive resolves to, conditions included: icon, tooltip, cooldown, range, usable state and auto-repeat glow. Action-bar addons get this with no code of their own. Addons with their own macro parser can publish through `C_Macro.SetMacroDisplay`. |
|
||||
| [Event-driven nameplates](docs/API.md#nameplate) | The modern `C_NamePlate` API, driven by real events. `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` fire as plates appear and vanish, `nameplate1`..`nameplateN` tokens resolve with every `UnitX` function (and fire `UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "nameplateN"`), and `C_NamePlate.GetNamePlateForUnit` / `GetNamePlates` hand back the live frames. |
|
||||
@@ -45,17 +45,18 @@ heading to expand it. For signatures and return values, see the per-function
|
||||
reference in **[docs/API.md](docs/API.md)**.
|
||||
|
||||
<details>
|
||||
<summary><b>In-game Lua calls</b> — 550+ functions across ~60 namespaces</summary>
|
||||
<summary><b>In-game Lua calls</b> — 950+ functions across ~75 namespaces</summary>
|
||||
|
||||
In the Frame rows, a name that starts with `:` is a method of the widget that the row names, for example `:SetShown` is `frame:SetShown`.
|
||||
|
||||
| Namespace | Calls |
|
||||
|-----------|-------|
|
||||
| [Action](docs/API.md#action) | `GetActionInfo`, `IsItemAction`, `HasBonusActionBar` |
|
||||
| [Action](docs/API.md#action) | `GetActionInfo`, `IsItemAction`, `HasBonusActionBar`, `C_ActionBar.EnableActionRangeCheck` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnInterfaceVersion`, `C_AddOns.GetAddOnLocalTable`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded`, `C_AddOns.LoadAddOn` |
|
||||
| [APIDocumentation](docs/API.md#apidocumentation) | `/classicapi` in-game browser, `C_APIDocumentation.GetSystem`, `C_APIDocumentation.GetSystems` |
|
||||
| [Animation](docs/API.md#animation) | `region:CreateAnimationGroup`, `region:GetAnimationGroups`, `region:StopAnimating`, `SetMaxAnimFramerate`, `GetMaxAnimFramerate`, `AnimationGroup` and `Animation` / `Alpha` / `Translation` / `Scale` / `Rotation` / `Path` / `ControlPoint` methods |
|
||||
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem`, `CanCancelAuction` |
|
||||
| [AutoComplete](docs/API.md#autocomplete) | `C_AutoComplete.GetAutoCompleteResults`, `C_AutoComplete.IsRecognizedName`, `C_AutoComplete.GetAutoCompletePresenceID`, `C_AutoComplete.GetAutoCompleteRealms` |
|
||||
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings`, `GetBindingAction` (`checkOverride`), `GetBindingByKey`, `IsModifiedClick`, `GetModifiedClick`, `SetModifiedClick`, `GetNumModifiedClickActions`, `GetModifiedClickAction` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatEvent`, `GetCurrentChatGUID`, `IsChatAFK`, `IsChatDND`, `SendSystemMessage` |
|
||||
| [ChatBubbles](docs/API.md#chatbubbles) | `C_ChatBubbles.GetAllChatBubbles` |
|
||||
@@ -69,21 +70,22 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Cursor](docs/API.md#cursor) | `CursorHasMacro`, `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.AutoStoreItem`, `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemEquipmentSetInfo`, `C_Container.GetContainerItemQuestInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerFreeSlots`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetBackpackAutosortDisabled`, `C_Container.GetBankAutosortDisabled`, `C_Container.GetItemCooldown`, `C_Container.GetSortBagsRightToLeft`, `C_Container.HasContainerItem`, `C_Container.IsContainerFiltered`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SetBackpackAutosortDisabled`, `C_Container.SetBagPortraitTexture`, `C_Container.SetBankAutosortDisabled`, `C_Container.SetItemSearch`, `C_Container.SetSortBagsRightToLeft`, `C_Container.SortBags`, `C_Container.SortBankBags`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown`, `SetBagPortraitTexture` |
|
||||
| [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` |
|
||||
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString`, `Constants.MoneyFormattingConstants` |
|
||||
| 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` |
|
||||
| [DurationUtil](docs/API.md#durationutil) | `C_DurationUtil.CreateDuration`, `C_DurationUtil.CreateDurationTextBinding` |
|
||||
| [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` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByID`, `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.IsFactionActive`, `C_Reputation.IsFactionActiveByID`, `C_Reputation.IsFactionInactive`, `C_Reputation.SetFactionActive`, `C_Reputation.SetFactionActiveByID`, `C_Reputation.SetFactionInactive`, `C_Reputation.SetFactionInactiveByID`, `C_Reputation.SetSelectedFaction`, `C_Reputation.SetSelectedFactionByID`, `C_Reputation.SetWatchedFactionByID`, `C_Reputation.ToggleFactionAtWar`, `C_Reputation.ToggleFactionAtWarByID`, `CollapseAllFactionHeaders`, `ExpandAllFactionHeaders`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
| [Frame: Region](docs/API.md#regionsetpointpoint-one-argument-form) | `:SetPoint("point")` (one-arg form), `:SetSize` / `:GetSize`, `ignoreRect` on `:GetSize` / `:GetWidth` / `:GetHeight`, `:GetRect`, `:GetScaledRect`, `:IsRectValid`, `:GetPointByName`, `:ClearPoint`, `:GetScale` / `:GetEffectiveScale` (on textures and font strings), `:GetDebugName`, `:IsMouseOver`, `:IsDragging`, `:IsProtected` / `:CanChangeProtectedState` |
|
||||
| [Frame: Region](docs/API.md#regionsetpointpoint-one-argument-form) | `:SetPoint("point")` (one-arg form), `:SetSize` / `:GetSize`, `ignoreRect` on `:GetSize` / `:GetWidth` / `:GetHeight`, `:GetUnscaledWidth` / `:GetUnscaledHeight` / `:GetUnscaledSize`, `:GetRect`, `:GetScaledRect`, `:IsRectValid`, `:GetPointByName`, `:ClearPoint`, `:GetScale` / `:GetEffectiveScale` (on textures and font strings), `:GetDebugName`, `:IsMouseOver`, `:IsDragging`, `:IsProtected` / `:CanChangeProtectedState` |
|
||||
| [Frame: Frame](docs/API.md#createframeframetype--name-parent-template-id) | `CreateFrame` template lists and `id`, `GetMouseFoci`, `:SetShown`, `:SetClampRectInsets` / `:GetClampRectInsets`, `:SetResizeBounds`, `:SetClipsChildren` / `:DoesClipChildren`, `:GetBoundsRect`, `:GetEffectiveAlpha`, `:SetAlphaGradient` / `:ClearAlphaGradient` / `:HasAlphaGradient`, `:DesaturateHierarchy`, `:HookScript`, `:IsEventRegistered`, `:RegisterUnitEvent`, `:SetPropagateKeyboardInput` / `:GetPropagateKeyboardInput`, `:SetDontSavePosition` / `:GetDontSavePosition`, `:SetAttribute`, `:SetAttributeNoHandler`, `:ClearAttribute`, `:GetAttribute`, `:CanChangeAttribute`, `:AllowAttributeChanges` |
|
||||
| [Frame: Button](docs/API.md#buttonsetnormalfontobjectfont--buttongetnormalfontobject) | `:SetEnabled`, `:SetFormattedText`, `:SetNormalFontObject` / `:GetNormalFontObject`, `:SetMotionScriptsWhileDisabled` / `:GetMotionScriptsWhileDisabled`, `:RegisterForClicks("AnyUp"/"AnyDown")`, `:RegisterForMouse`, `GetClickFrame` |
|
||||
| [Frame: FontString](docs/API.md#fontstringgetstringheight) | `:GetStringHeight`, `:GetUnboundedStringWidth`, `:GetWrappedWidth`, `:GetNumLines`, `:GetLineHeight`, `:GetFontHeight` / `:SetFontHeight` (also on fonts), `:GetFieldSize`, `:IsTruncated`, `:SetMaxLines` / `:GetMaxLines`, `:SetWordWrap` / `:CanWordWrap`, `:SetIndentedWordWrap` / `:GetIndentedWordWrap` (also on fonts, edit boxes, message frames, and SimpleHTML), `:SetFormattedText`, `:SetRotation` / `:GetRotation` |
|
||||
| [Frame: Texture](docs/API.md#texturesetrotationangle--cx-cy) | `:SetRotation` / `:GetRotation`, `:SetVertexOffset` / `:GetVertexOffset`, `:SetColorTexture`, `:SetAtlas` / `:GetAtlas`, `:ResetTexCoord`, `:SetSpriteSheetCell`, `:SetDesaturation` / `:GetDesaturation`, `IsDesaturateSupported`, `:SetMask`, `frame:CreateMaskTexture`, `:AddMaskTexture` / `:RemoveMaskTexture`, `:GetNumMaskTextures`, `:GetMaskTexture` |
|
||||
| [Frame: Texture](docs/API.md#texturesetrotationangle--cx-cy) | `:SetRotation` / `:GetRotation`, `:SetVertexOffset` / `:GetVertexOffset`, `:SetColorTexture`, `:SetAtlas` / `:GetAtlas`, `:ResetTexCoord`, `:SetSpriteSheetCell`, `:SetTexture` wrap modes, `:SetHorizTile` / `:GetHorizTile`, `:SetVertTile` / `:GetVertTile`, `:SetDesaturation` / `:GetDesaturation`, `IsDesaturateSupported`, `:SetMask`, `frame:CreateMaskTexture`, `:AddMaskTexture` / `:RemoveMaskTexture`, `:GetNumMaskTextures`, `:GetMaskTexture` |
|
||||
| [Frame: EditBox](docs/API.md#editboxsetcursorpositionposition) | `:SetCursorPosition` / `:GetCursorPosition`, `:GetUTF8CursorPosition`, `:ClearHighlightText`, `:SetHighlightColor` / `:GetHighlightColor`, `:HasFocus`, `GetCurrentKeyBoardFocus`, `:HasText`, `:ClearHistory`, `:Enable` / `:Disable` / `:IsEnabled`, `:SetCountInvisibleLetters` / `:IsCountInvisibleLetters`, `:IsInIMECompositionMode` |
|
||||
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation` |
|
||||
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation`, `:SetTimerDuration` / `:GetTimerDuration`, `:SetValue` / `:SetMinMaxValues` with `interpolation`, `:GetInterpolatedValue`, `:IsInterpolating`, `:SetToTargetValue` |
|
||||
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--sliderisenabled) | `:Enable` / `:Disable` / `:IsEnabled` |
|
||||
| [Frame: Cooldown](docs/API.md#createframecooldown--name-parent-template) | `CreateFrame("Cooldown")`, `:SetCooldown`, `:SetCooldownDuration`, `:SetCooldownFromExpirationTime`, `:Clear`, `:GetCooldownTimes`, `:GetCooldownDuration`, `:GetCooldownDisplayDuration`, `:Pause` / `:Resume`, `:SetPaused` / `:IsPaused`, `:SetReverse` / `:GetReverse`, `:SetDrawEdge` / `:GetDrawEdge`, `:SetDrawSwipe` / `:GetDrawSwipe`, `:SetDrawBling` / `:GetDrawBling`, `:SetSwipeColor`, `:SetEdgeColor`, `:SetSwipeTexture`, `:SetEdgeTexture`, `:SetBlingTexture`, `:SetRotation` / `:GetRotation`, `:SetHideCountdownNumbers` / `:GetHideCountdownNumbers`, `:GetCountdownFontString`, `:SetCountdownAbbrevThreshold` / `:GetCountdownAbbrevThreshold`, `:SetCountdownMillisecondsThreshold` / `:GetCountdownMillisecondsThreshold`, `:SetMinimumCountdownDuration` / `:GetMinimumCountdownDuration`, `OnCooldownDone`, `CooldownFrame_Set`, `CooldownFrame_Clear`, `CooldownFrame_SetDisplayAsPercentage` |
|
||||
| [Frame: 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` |
|
||||
@@ -100,10 +102,11 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Item: info](docs/API.md#getitemclassinfoclassid) | `GetItemClassInfo`, `GetItemSubClassInfo`, `GetItemIcon`, `IsConsumableItem`, `IsCurrentItem`, `IsDressableItem`, `IsEquippedItemType`, `IsHarmfulItem`, `IsHelpfulItem`, `IsUsableItem` |
|
||||
| [Item: inventory](docs/API.md#getaverageitemlevel) | `GetAverageItemLevel`, `GetInventoryItemID`, `GetInventoryItemDurability`, `GetInventoryItemRepairCost`, `GetInventoryItemsForSlot`, `IsDualWielding`, `SetInventoryPortraitTexture` |
|
||||
| [Item: item IDs](docs/API.md#getitemid--companions-to-the-engines-getitemlink-family) | `GetAuctionItemID`, `GetAuctionSellItemID`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeTargetItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID` |
|
||||
| [Localization](docs/API.md#localization) | `AbbreviateLargeNumbers`, `AbbreviateNumbers`, `BreakUpLargeNumbers`, `C_StringUtil.GetDefaultAbbreviationBreakpoints` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootAllCorpses`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount`, `C_LossOfControl.GetSchoolLockout` |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `strcmputf8i`, `strconcat`, `string.concat`, `string.gmatch`, `string.gsub` (table replacement), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strlenutf8`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strsplittable`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `xpcall` (argument forwarding) |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `GetCallstackHeight`, `GetErrorCallstackHeight`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `SetErrorCallstackHeight`, `strcmputf8i`, `strconcat`, `string.concat`, `string.gmatch`, `string.gsub` (table replacement), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strlenutf8`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strsplittable`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `wipe`, `xpcall` (argument forwarding) |
|
||||
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `C_Macro.SetMacroDisplay`, `DeleteMacro` / `EditMacro` / `GetMacroInfo` / `PickupMacro` by name, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroBody`, `GetMacroIcons`, `GetMacroItem`, `GetMacroItemIcons`, `GetMacroSpell`, `GetRunningMacro`, `GetRunningMacroButton`, `RunMacroText`, `SetMacroItem`, `SetMacroSpell`, `StopMacro` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
| [Map](docs/API.md#map) | `C_Map.CanSetUserWaypointOnMap`, `C_Map.ClearUserWaypoint`, `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetFallbackWorldMapID`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapArtLayers`, `C_Map.GetMapArtLayerTextures`, `C_Map.GetMapChildrenInfo`, `C_Map.GetMapInfo`, `C_Map.GetMapInfoAtPosition`, `C_Map.GetMapOverlays`, `C_Map.GetMapPosFromWorldPos`, `C_Map.GetMapRectOnMap`, `C_Map.GetMapWorldSize`, `C_Map.GetPlayerMapPosition`, `C_Map.GetUserWaypoint`, `C_Map.GetUserWaypointFromHyperlink`, `C_Map.GetUserWaypointHyperlink`, `C_Map.GetUserWaypointPositionForMap`, `C_Map.GetWorldPosFromMapPos`, `C_Map.HasUserWaypoint`, `C_Map.MapHasArt`, `C_Map.SetUserWaypoint` |
|
||||
@@ -119,7 +122,8 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Profiling](docs/API.md#profiling) | `GetAddOnCPUUsage`, `GetAddOnMemoryUsage`, `GetEventCPUUsage`, `GetFrameCPUUsage`, `GetFunctionCPUUsage`, `GetScriptCPUUsage`, `ResetCPUUsage`, `UpdateAddOnCPUUsage`, `UpdateAddOnMemoryUsage` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetInfo`, `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestLogTitle`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogCompletionText`, `GetQuestLogLeaderBoardID` (Turtle WoW only: `C_QuestLog.GetAllCompletedQuestIDs`, `C_QuestLog.IsQuestFlaggedCompleted`, `GetQuestsCompleted`, `QueryQuestsCompleted`) |
|
||||
| [Sound](docs/API.md#sound) | `C_Sound.GetRecentSoundFiles`, `C_Sound.GetSoundScaledVolume`, `C_Sound.IsPlaying`, `C_Sound.PlayItemSound`, `C_Sound.PlaySound`, `C_Sound.PlaySoundWithOptions`, `C_Sound.PlayVocalErrorSound`, `MuteSoundFile`, `PlaySound`, `PlaySoundFile`, `StopSound`, `UnmuteSoundFile` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCastCount`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectInfo`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellLossOfControlCooldown`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CancelUnitBuff`, `CastSpellByID`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetCritChance`, `GetRangedCritChance`, `GetSpellBaseCooldown`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsAttackSpell`, `IsAutoRepeatSpell`, `IsConsumableSpell`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCastCount`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectInfo`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellLossOfControlCooldown`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CancelUnitBuff`, `CastSpellByID`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetCritChance`, `GetRangedCritChance`, `GetSpellBaseCooldown`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsAttackSpell`, `IsAutoRepeatSpell`, `IsConsumableSpell`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange`, `UnitCastingDuration`, `UnitChannelDuration` |
|
||||
| [SpellActivationOverlay](docs/API.md#spellactivationoverlay) | `C_SpellActivationOverlay.IsSpellOverlayed`, `IsSpellOverlayed` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.ContainsAnyDisenchantSpell`, `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetNumSpellBookSkillLines`, `C_SpellBook.GetPlayerSpellsByAura`, `C_SpellBook.GetSkillLineIndexByID`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellBookItemCastCount`, `C_SpellBook.GetSpellBookItemInfo`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownDuration`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownInfo`, `C_SpellBook.GetSpellBookItemSkillLineIndex`, `C_SpellBook.GetSpellBookSkillLineInfo`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsClassTalentSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftForm`, `GetShapeshiftFormID`, `GetSheathState`, `IsAssistingRitual`, `IsFalling`, `IsIndoors`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsLoggingOut`, `IsMounted`, `IsOutdoors`, `IsPlayerInWorld`, `IsStealthed`, `IsSwimming` |
|
||||
| [SwingTimer](docs/API.md#swingtimer) | `C_SwingTimer.EnableRangeCheck`, `C_SwingTimer.IsTargetWithinSwingRange` |
|
||||
@@ -131,10 +135,10 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime`, `GetTickTime`, `GetTimeCached` |
|
||||
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
|
||||
| [Tracking](docs/API.md#tracking) | `GetNumTrackingTypes`, `GetTrackingInfo`, `SetTracking` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `GetTradeSkillListLink`, `GetCraftListLink`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked`, `IsCraftLinked` |
|
||||
| [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`, `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` |
|
||||
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitFacing`, `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` |
|
||||
@@ -207,16 +211,24 @@ for what each command does, and
|
||||
|
||||
Each setting is saved in `WTF\Config.wtf`. Read and write them with `GetCVar`
|
||||
/ `SetCVar` or `/console`. Click a group name for the full rules. To change
|
||||
the most common settings and the player cache in the game, open the game
|
||||
menu and click AddOn Options, then ClassicAPI.
|
||||
these settings and the player cache in the game, open the game menu and
|
||||
click AddOn Options, then ClassicAPI.
|
||||
|
||||
| Group | CVar | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| [Bindings](docs/API.md#actionbuttonusekeydown) | `ActionButtonUseKeyDown` | `0` | `1` uses an action-bar key binding when you press the key. `0` uses it when you release the key. |
|
||||
| [Combat](docs/API.md#stopautoattackontargetchange) | `stopAutoAttackOnTargetChange` | `0` | `1` stops melee auto-attack, Auto Shot and wand shots when you change target. |
|
||||
| [Combat log](docs/API.md#kept-entries) | `combatLogRetentionTime` | `300` | The number of seconds that the combat log keeps an entry. |
|
||||
| [Cast settings](docs/API.md#cast-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when a spell cannot be cast in it. |
|
||||
| [Bag sorting](docs/API.md#c_containergetsortbagsrighttoleft--c_containersetsortbagsrighttoleftenable) | `sortBagsRightToLeft` | `0` | `1` makes a sort fill your bags from the last slot of the last bag. |
|
||||
| | `backpackAutosortDisabled` | `0` | `1` keeps the items in your backpack in place when you sort your bags. |
|
||||
| | `bankAutosortDisabled` | `0` | `1` keeps the items in the main bank slots in place when you sort your bank. |
|
||||
| [Equipment sets](docs/API.md#equipment-sets-on-item-tooltips) | `dontShowEquipmentSetsOnItems` | `0` | `1` removes the Equipment Sets line from item tooltips. |
|
||||
| [Name completion](docs/API.md#name-completion-settings) | `autoCompleteResortNamesOnRecency` | `1` | `1` puts the players that you whispered first in `C_AutoComplete` results, the most recent first. `0` uses alphabetical order. |
|
||||
| | `autoCompleteWhenEditingFromCenter` | `1` | `1` also matches the text after the cursor when you type inside a name. |
|
||||
| | `autoCompleteUseContext` | `1` | `0` makes `C_AutoComplete` ignore `includeFlags` and `excludeFlags`. |
|
||||
| [Profiling](docs/API.md#scriptprofile) | `scriptProfile` | `0` | `1` turns on CPU profiling for the `*CPUUsage` functions. The change starts after `/reload`. |
|
||||
| [Soft interact](docs/API.md#soft-interact-softinteract--anyinteract) | `SoftTargetInteract` | `3` | Turns `softinteract` on: `2` keyboard and mouse, `3` always, `0` / `1` off. |
|
||||
| | `SoftTargetInteractArc` | `0` | Where the object can be: `0` straight ahead of the camera, `1` within 30° of it, `2` any direction. |
|
||||
@@ -233,7 +245,9 @@ menu and click AddOn Options, then ClassicAPI.
|
||||
| | `SoftTargetIconEnemy` / `SoftTargetIconFriend` | `0` | `1` shows an icon over `softenemy` / `softfriend`. |
|
||||
| | `SoftTargetLowPriorityIcons` | `0` | `1` also shows the normal pointer as an icon. |
|
||||
| | `SoftTargetNameplateSize` | `19` | Size of all soft-target icons. |
|
||||
| [Localization](docs/API.md#breakuplargenumbers-cvar) | `breakUpLargeNumbers` | `1` | `1` puts a separator between each group of three digits in `BreakUpLargeNumbers`. `0` writes no separator. |
|
||||
| [Cooldown](docs/API.md#createframecooldown--name-parent-template) | `countdownForCooldowns` | `0` | `1` shows the time left as numbers on cooldown frames. |
|
||||
| `ScriptAnimationUtil` | `ShakeStrengthUI` | `1` | Scales how far `ScriptAnimationUtil.ShakeFrame` moves a frame. `0` turns the shake off. |
|
||||
| [NamePlate](docs/API.md#nameplate) | `nameplateNotSelectedAlpha` | `0.5` | Alpha of a non-target default nameplate, `0` to `1`. |
|
||||
| | `nameplateSelectedAlpha` | `1` | Alpha of the target's default nameplate, `0` to `1`. |
|
||||
| [Text to speech](docs/API.md#tts-cvars) | `ttsVoice` | `0` | The voice that `C_VoiceChat.SpeakText` uses. |
|
||||
@@ -266,6 +280,8 @@ when launching with `-console`), not as Lua functions. See the
|
||||
|
||||
| Event | Payload |
|
||||
|-------|---------|
|
||||
| `ACTION_RANGE_CHECK_UPDATE` | `slot, isInRange, checksRange` (slots with `C_ActionBar.EnableActionRangeCheck` on) |
|
||||
| `ACTION_USABLE_CHANGED` | `changes` (a `{ slot, usable, noMana }` table for each slot that changed) |
|
||||
| `BAG_NEW_ITEMS_UPDATED` | *(none)* |
|
||||
| `BAG_UPDATE_DELAYED` | *(none)* |
|
||||
| `CHAT_MSG_*` | adds `lineID, guid` as `arg11, arg12` after the existing ten ([details](docs/API.md#chat_msg_-arguments-lineid-arg11-and-guid-arg12)) |
|
||||
@@ -297,6 +313,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `PLAYER_AVG_ITEM_LEVEL_READY` | *(none)* |
|
||||
| `PLAYER_EQUIPMENT_CHANGED` | `equipmentSlot, hasCurrent` |
|
||||
| `PLAYER_FOCUS_CHANGED` | *(none)* |
|
||||
| `PLAYER_MOUNT_DISPLAY_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_ENEMY_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_FRIEND_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_INTERACT_CHANGED` | `oldTarget, newTarget` |
|
||||
@@ -316,7 +333,16 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
|
||||
| `SELF_RES_SPELL_CHANGED` | *(none)* |
|
||||
| `SOUNDKIT_FINISHED` | `soundHandle` (only for sounds played with `runFinishCallback`) |
|
||||
| `SPELL_ACTIVATION_OVERLAY_SHOW` | `spellID, texture, locationType, scale, r, g, b` ([details](docs/API.md#spell_activation_overlay_show--spell_activation_overlay_hide-events)) |
|
||||
| `SPELL_ACTIVATION_OVERLAY_HIDE` | `spellID` (`nil` hides every alert) |
|
||||
| `SPELL_ACTIVATION_OVERLAY_GLOW_SHOW` | `spellID` ([details](docs/API.md#spell_activation_overlay_glow_show--spell_activation_overlay_glow_hide-events)) |
|
||||
| `SPELL_ACTIVATION_OVERLAY_GLOW_HIDE` | `spellID` |
|
||||
| `UI_SCALE_CHANGED` | *(none)* |
|
||||
| `UNIT_AREA_CHANGED` | `unitTarget` |
|
||||
| `UNIT_AURA_DELTA` | `unitTarget, updateInfo` (the instance IDs of the auras added, updated, and removed) |
|
||||
| `UNIT_CONNECTION` | `unitTarget, isConnected` |
|
||||
| `UNIT_DISTANCE_CHECK_UPDATE` | `unitTarget, isInDistance` |
|
||||
| `UNIT_IN_RANGE_UPDATE` | `unitTarget, isInRange` |
|
||||
| `UNIT_SPELLCAST_SENT` | `"player", target, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_START` | `unit, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_STOP` | `unit, castGUID, spellID, spellName, rank` |
|
||||
@@ -332,6 +358,12 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `UNIT_SPELLCAST_RETICLE_CLEAR` | `"player", "", spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_INTERRUPTIBLE` | `unit` |
|
||||
| `UNIT_SPELLCAST_NOT_INTERRUPTIBLE` | `unit` |
|
||||
| `UNIT_MAXPOWER` | `unitTarget, powerType` |
|
||||
| `UNIT_POWER_FREQUENT` | `unitTarget, powerType` |
|
||||
| `UNIT_POWER_UPDATE` | `unitTarget, powerType` |
|
||||
| `UNIT_SPELL_HASTE` | `unitTarget` |
|
||||
| `UNIT_TARGET` | `unitTarget` |
|
||||
| `UNIT_TARGETABLE_CHANGED` | `unitTarget` |
|
||||
| `UPDATE_INVENTORY_DURABILITY` | *(none)* |
|
||||
| `UPDATE_SHAPESHIFT_FORM` | *(none)* |
|
||||
| `USER_WAYPOINT_UPDATED` | *(none)* |
|
||||
@@ -362,7 +394,10 @@ 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) |
|
||||
| Screen location type | `Enum.ScreenLocationType.{Center,Left,Right,Top,Bottom,TopLeft,TopRight,LeftOutside,RightOutside,LeftRight,TopBottom,LeftRightOutside}` (0–11) |
|
||||
| Self-resurrect option type | `Enum.SelfResurrectOptionType.{Spell,Item}` (0–1) |
|
||||
| Autocomplete entry flag | `Enum.AutoCompleteEntryFlag.{InGroup,InGuild,Friend,Bnet,InteractedWith,Online,InAOI,AccountCharacter,RecentPlayer}` (bits 1–256) |
|
||||
| Autocomplete priority | `Enum.AutoCompletePriority.{Other,Interacted,InGroup,Guild,Friend,AccountCharacter,AccountCharacterSameRealm}` (0–6) |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -418,10 +453,10 @@ Download the prebuilt `ClassicAPI.dll` from the
|
||||
3. Add `ClassicAPI.dll` to `dlls.txt`.
|
||||
4. Launch the game with `VanillaFixes.exe`.
|
||||
|
||||
The `!!!ClassicAPI` and `ClassicAPI_DebugTools` addons are *inside* the
|
||||
DLL. You do not download or install them. The `src/embed/AddOns/` folder
|
||||
in this repository is source code for the DLL. Do not copy it into your
|
||||
game.
|
||||
The `!!!ClassicAPI`, `ClassicAPI_DebugTools` and `ClassicAPI_EventTrace`
|
||||
addons are *inside* the DLL. You do not download or install them. The
|
||||
`src/embed/AddOns/` folder in this repository is source code for the DLL.
|
||||
Do not copy it into your game.
|
||||
|
||||
## Bundled addon: !!!ClassicAPI
|
||||
|
||||
@@ -432,8 +467,9 @@ functions but that consumer code still expects to find as globals —
|
||||
`CallbackRegistryMixin`, `EventRegistry`, `ColorMixin` + `CreateColor`,
|
||||
`Item` / `ItemLocation`, `MathUtil`(`Lerp` / `Clamp` / `CreateCounter`),
|
||||
`TableUtil` (`tCompare`, `MergeTable`, `SafePack`, etc.), `EventUtil`
|
||||
(`ContinueOnAddOnLoaded` and more), `DataProvider`, `CircularBuffer`,
|
||||
`EasingUtil`, `Interpolator`, `EnumUtil`, `StringUtil`, `AnchorUtil`,
|
||||
(`ContinueOnAddOnLoaded` and more), `FormattingUtil` (`GetMoneyString`,
|
||||
`FormatLargeNumber` and more), `FrameUtil`, `DataProvider`, `CircularBuffer`,
|
||||
`EasingUtil`, `ScriptAnimationUtil`, `Interpolator`, `EnumUtil`, `StringUtil`, `AnchorUtil`,
|
||||
`GridLayoutUtil`, and the layout frame templates (`VerticalLayoutFrame`,
|
||||
`HorizontalLayoutFrame`, `ResizeLayoutFrame`, `GridLayoutFrame`,
|
||||
`StaticGridLayoutFrame`). It also holds the conditional slash
|
||||
@@ -465,22 +501,40 @@ without a DLL rebuild. Without this file, the game uses the copy with the
|
||||
newer `## Version:`. The copy is hidden and always enabled, the same as
|
||||
the embedded addon.
|
||||
|
||||
## Lua errors
|
||||
|
||||
ClassicAPI replaces the game's Lua error box with a Lua error window. The
|
||||
window opens when an error occurs. It shows the message, the time, the count
|
||||
and the stack of each error. You can select and copy the text. The arrow
|
||||
buttons show the previous and the next error. The **Reload UI** button
|
||||
reloads the interface.
|
||||
|
||||
If you use `!BugGrabber` (with BugSack), BugGrabber gets the errors and the
|
||||
window does not open. If an addon sets its own error handler, the window also
|
||||
does not open.
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/luaerrors` (or `/scripterrors`) | Opens or closes the Lua error window. |
|
||||
| `/luaerrors clear` | Removes all errors from the window. |
|
||||
|
||||
## Debug tools
|
||||
|
||||
ClassicAPI contains a backport of Blizzard's `Blizzard_DebugTools` addon,
|
||||
with the name `ClassicAPI_DebugTools`. The DLL contains this addon, so you do
|
||||
not install it. The addon is not in the AddOns list, and you cannot disable it.
|
||||
ClassicAPI contains backports of two Blizzard addons: `Blizzard_DebugTools`,
|
||||
with the name `ClassicAPI_DebugTools`, and `Blizzard_EventTrace`, with the
|
||||
name `ClassicAPI_EventTrace`. The DLL contains these addons, so you do not
|
||||
install them. They are not in the AddOns list, and you cannot disable them.
|
||||
|
||||
The slash commands are always available. The tools load the first time
|
||||
that you use one of these commands:
|
||||
The slash commands are always available. Each addon loads the first time
|
||||
that you use one of its commands:
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/dump <expr>` | Shows a Lua value in the chat frame. It shows tables and all return values. Use it to examine the return values of `GetSpellInfo`, `C_Item.GetItemInfoInstant`, and other functions. |
|
||||
| `/etrace` | Opens the event trace window. Other forms: `/etrace start`, `/etrace stop`, `/etrace add EVENT`, and more. |
|
||||
| `/framestack [showHidden [showRegions]]` (or `/fstack`) | Shows a tooltip with a list of the frames under the cursor. A `-->` mark shows one frame, with its anchors and the file and line that made it. Left Alt and Right Alt move the mark. Ctrl opens the marked frame in the table inspector. Shift shows its texture files. |
|
||||
| `/etrace` (or `/eventtrace`) | Opens or closes the event trace window. The window logs each event with its time and its arguments. Move the mouse over an event to see its arguments, and the contents of each table argument. You can search the log, hide events with the filter, and pause the log. `/etrace mark <text>` adds a line of text to the log. |
|
||||
| `/framestack [showHidden [showRegions]]` (or `/fstack`) | Shows a tooltip with a list of the frames under the cursor. A `-->` mark shows one frame, with its anchors and the file and line that made it. Left Alt and Right Alt move the mark. Ctrl opens the marked frame in the table inspector. Shift shows its texture files. The tooltip also shows the addon that made the marked frame. When `scriptProfile` is `1`, it shows the CPU time of the frame scripts. |
|
||||
| `/tinspect [expr]` (or `/tins`) | Opens a table inspector on the result of `expr`. With no table, it opens on the frame under the cursor. The window shows the anchors and the fields of the table, grouped by type. You can edit numbers, strings and booleans, filter the lines, and open a child table or frame. |
|
||||
| `/luaerrors` (or `/scripterrors`) | Opens the Lua error window. |
|
||||
| `/profiler` (or `/prof`) | Opens the profiler. The profiler shows the Lua memory, the allocations and the CPU time of each addon, frame, event and global function. `/profiler widget` shows a small window with the Lua memory, the frame rate and the addons that use the most. `/profiler reset` sets the counters to zero. CPU times need `scriptProfile` `1`. `/profiler profile on` sets it and reloads the UI. |
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ on a frame timer" goal landed as the request/event pair below:
|
||||
cache (or synchronously if it was already loaded).
|
||||
|
||||
Together those replace the polyfill's 180-second polling frame
|
||||
(`ItemEventListener` in `Util/ItemUtil.lua:254-372`) — addons can now
|
||||
(`ItemEventListener`, now in `Util/AsyncCallbackSystem.lua`) — addons can now
|
||||
fire `RequestLoadItemDataByID(id)` and listen for
|
||||
`ITEM_DATA_LOAD_RESULT` instead.
|
||||
|
||||
|
||||
+66
-49
@@ -3,8 +3,8 @@
|
||||
#
|
||||
# Runs at configure time. It writes two files into OUT_DIR:
|
||||
#
|
||||
# embedded.rc one `<id> RCDATA "<path>"` line per file; rc.exe
|
||||
# reads each file's gzipped copy from disk.
|
||||
# embedded.rc one `<id> RCDATA "<path>"` line per pack; rc.exe
|
||||
# reads each gzipped pack from disk.
|
||||
# embedded_resources.h the IDs the DLL looks up at runtime through
|
||||
# src/embed/Resource.h:
|
||||
#
|
||||
@@ -12,14 +12,24 @@
|
||||
# namespace Addons::Embedded::Manifest {
|
||||
# constexpr const char *kAddons[] = { ... }; // in ADDONS order
|
||||
# constexpr size_t kAddonCount = ...;
|
||||
# struct File { const char *path; int resourceId; };
|
||||
# struct File { const char *path; int resourceId; int index; };
|
||||
# constexpr File kFiles[] = { ... };
|
||||
# constexpr size_t kFileCount = ...;
|
||||
# }
|
||||
#
|
||||
# Each resource is a pack: one gzip stream of several files, which
|
||||
# pack_files.cmake joins at build time into `OUT_DIR/embed/<name>.pack.gz`.
|
||||
# Resource 1 is the bindings XML, a pack of one; then each addon is one
|
||||
# pack, in ADDONS order. A pack compresses each file against the files
|
||||
# before it, so the addon text packs to about 22% of its size, against 27%
|
||||
# for one gzip stream per file. The DLL inflates a whole pack with the
|
||||
# engine's zlib on first use (src/embed/Resource.cpp), so the
|
||||
# load-on-demand addon costs nothing until it loads.
|
||||
#
|
||||
# File paths are relative to ADDONS_ROOT, in backslash form
|
||||
# (`!!!ClassicAPI\Util\Print.lua`) — the suffix the engine builds after
|
||||
# `Interface\AddOns\`. The DLL compares them ignoring case.
|
||||
# `Interface\AddOns\`. The DLL compares them ignoring case. A file's index
|
||||
# is its place in its addon's pack, which is in path order.
|
||||
#
|
||||
# Each addon's `.toc` is the one file that is not embedded verbatim: its
|
||||
# `## Version:` line is stamped with ADDON_VERSION into a copy under
|
||||
@@ -28,30 +38,37 @@
|
||||
# never embedded: it must reflect on-disk presence only (see
|
||||
# DiskHasDevMarker in src/addons/Embedded.cpp).
|
||||
#
|
||||
# Every resource is gzipped at build time (gzip_file.cmake) into
|
||||
# `OUT_DIR/embed/<path>.gz`, and the DLL inflates it with the engine's zlib on
|
||||
# first use (src/embed/Resource.cpp). The addon text compresses to about a
|
||||
# quarter of its size.
|
||||
#
|
||||
# Rebuild triggers: adding or removing a file reconfigures through the
|
||||
# CONFIGURE_DEPENDS glob; editing a toc reconfigures through
|
||||
# CMAKE_CONFIGURE_DEPENDS; editing any other file re-gzips it through its
|
||||
# custom command, which recompiles the .rc through its OBJECT_DEPENDS.
|
||||
# CMAKE_CONFIGURE_DEPENDS; editing any other file re-packs its addon through
|
||||
# the pack's custom command, which recompiles the .rc through its
|
||||
# OBJECT_DEPENDS.
|
||||
#
|
||||
# Usage:
|
||||
# embed_resources(TARGET <t> ADDONS_ROOT <dir> ADDONS <folder names...>
|
||||
# ADDON_VERSION <v> BINDINGS_XML <file> OUT_DIR <dir>)
|
||||
|
||||
set(_EMBED_GZIP_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/gzip_file.cmake")
|
||||
set(_EMBED_PACK_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/pack_files.cmake")
|
||||
|
||||
# Add the build-time command that gzips `src` into `gz`.
|
||||
function(_embed_gzip src gz)
|
||||
# Add the build-time command that packs `ARGN` (absolute paths, in pack
|
||||
# order) into `<out_dir>/embed/<name>.pack.gz`, and set `gz` in the caller
|
||||
# to that path. The paths reach the script through a generated list file:
|
||||
# an addon's paths are too long for one command line.
|
||||
function(_embed_pack out_dir name)
|
||||
set(pack "${out_dir}/embed/${name}.pack")
|
||||
set(list "set(FILES\n")
|
||||
foreach(path ${ARGN})
|
||||
string(APPEND list " [==[${path}]==]\n")
|
||||
endforeach()
|
||||
string(APPEND list ")\n")
|
||||
_embed_write_if_changed("${pack}.cmake" "${list}")
|
||||
add_custom_command(
|
||||
OUTPUT "${gz}"
|
||||
COMMAND "${CMAKE_COMMAND}" "-DIN=${src}" "-DOUT=${gz}"
|
||||
-P "${_EMBED_GZIP_SCRIPT}"
|
||||
DEPENDS "${src}" "${_EMBED_GZIP_SCRIPT}"
|
||||
OUTPUT "${pack}.gz"
|
||||
COMMAND "${CMAKE_COMMAND}" "-DLIST=${pack}.cmake" "-DOUT=${pack}.gz"
|
||||
-P "${_EMBED_PACK_SCRIPT}"
|
||||
DEPENDS ${ARGN} "${pack}.cmake" "${_EMBED_PACK_SCRIPT}"
|
||||
VERBATIM)
|
||||
set(gz "${pack}.gz" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Write `content` to `path` only when it differs, so an unchanged
|
||||
@@ -71,56 +88,55 @@ function(embed_resources)
|
||||
set(addon_names "")
|
||||
set(manifest "")
|
||||
set(inputs "")
|
||||
set(id 1)
|
||||
|
||||
set(gz "${E_OUT_DIR}/embed/Bindings.xml.gz")
|
||||
_embed_gzip("${E_BINDINGS_XML}" "${gz}")
|
||||
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
|
||||
set(bindings_id 1)
|
||||
_embed_pack("${E_OUT_DIR}" "Bindings.xml" "${E_BINDINGS_XML}")
|
||||
string(APPEND rc "${bindings_id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
set(bindings_id ${id})
|
||||
math(EXPR id "${id} + 1")
|
||||
|
||||
set(files "")
|
||||
set(id 2)
|
||||
foreach(addon ${E_ADDONS})
|
||||
string(APPEND addon_names " \"${addon}\",\n")
|
||||
file(GLOB_RECURSE addon_files CONFIGURE_DEPENDS RELATIVE
|
||||
file(GLOB_RECURSE files CONFIGURE_DEPENDS RELATIVE
|
||||
"${E_ADDONS_ROOT}" "${E_ADDONS_ROOT}/${addon}/*")
|
||||
list(FILTER addon_files EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
|
||||
list(APPEND files ${addon_files})
|
||||
endforeach()
|
||||
list(SORT files) # deterministic IDs
|
||||
foreach(rel ${files})
|
||||
set(abs "${E_ADDONS_ROOT}/${rel}")
|
||||
if(rel MATCHES "\\.toc$")
|
||||
file(READ "${abs}" toc)
|
||||
string(REGEX REPLACE "## Version:[^\n\r]*"
|
||||
"## Version: ${E_ADDON_VERSION}" toc "${toc}")
|
||||
set(stamped "${E_OUT_DIR}/embed/${rel}")
|
||||
_embed_write_if_changed("${stamped}" "${toc}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${abs}")
|
||||
set(abs "${stamped}")
|
||||
endif()
|
||||
set(gz "${E_OUT_DIR}/embed/${rel}.gz")
|
||||
_embed_gzip("${abs}" "${gz}")
|
||||
list(FILTER files EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
|
||||
list(SORT files) # deterministic indexes
|
||||
set(paths "")
|
||||
set(index 0)
|
||||
foreach(rel ${files})
|
||||
set(abs "${E_ADDONS_ROOT}/${rel}")
|
||||
if(rel MATCHES "\\.toc$")
|
||||
file(READ "${abs}" toc)
|
||||
string(REGEX REPLACE "## Version:[^\n\r]*"
|
||||
"## Version: ${E_ADDON_VERSION}" toc "${toc}")
|
||||
set(stamped "${E_OUT_DIR}/embed/${rel}")
|
||||
_embed_write_if_changed("${stamped}" "${toc}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${abs}")
|
||||
set(abs "${stamped}")
|
||||
endif()
|
||||
list(APPEND paths "${abs}")
|
||||
string(REPLACE "/" "\\\\" rel_bs "${rel}")
|
||||
string(APPEND manifest " { \"${rel_bs}\", ${id}, ${index} },\n")
|
||||
math(EXPR index "${index} + 1")
|
||||
endforeach()
|
||||
_embed_pack("${E_OUT_DIR}" "${addon}" ${paths})
|
||||
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
string(REPLACE "/" "\\\\" rel_bs "${rel}")
|
||||
string(APPEND manifest " { \"${rel_bs}\", ${id} },\n")
|
||||
math(EXPR id "${id} + 1")
|
||||
endforeach()
|
||||
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded.rc" "${rc}")
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded_resources.h"
|
||||
"// Generated by cmake/embed_resources.cmake — do not edit.
|
||||
// Resource IDs for the RCDATA entries in embedded.rc. Load them through
|
||||
// src/embed/Resource.h.
|
||||
// Resource IDs for the RCDATA packs in embedded.rc, and each embedded file's
|
||||
// place in them. Load them through src/embed/Resource.h.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Embed::Ids {
|
||||
constexpr int kBindingsXml = ${bindings_id};
|
||||
constexpr int kBindingsXml = ${bindings_id}; // a pack of the one file
|
||||
} // namespace Embed::Ids
|
||||
|
||||
namespace Addons::Embedded::Manifest {
|
||||
@@ -133,7 +149,8 @@ constexpr size_t kAddonCount = sizeof(kAddons) / sizeof(kAddons[0]);
|
||||
|
||||
struct File {
|
||||
const char *path;
|
||||
int resourceId;
|
||||
int resourceId; // its addon's pack
|
||||
int index; // its place in the pack
|
||||
};
|
||||
|
||||
constexpr File kFiles[] = {
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# gzip_file.cmake — gzip one file at build time, for embed_resources.cmake.
|
||||
#
|
||||
# cmake -DIN=<file> -DOUT=<file.gz> -P gzip_file.cmake
|
||||
#
|
||||
# The raw format holds the one file's bytes with no archive entry around
|
||||
# them, so OUT is a plain gzip stream of IN.
|
||||
file(ARCHIVE_CREATE OUTPUT "${OUT}" PATHS "${IN}" FORMAT raw
|
||||
COMPRESSION GZip COMPRESSION_LEVEL 9)
|
||||
@@ -0,0 +1,50 @@
|
||||
# pack_files.cmake — join files into one gzipped pack at build time, for
|
||||
# embed_resources.cmake.
|
||||
#
|
||||
# cmake -DLIST=<list.cmake> -DOUT=<pack.gz> -P pack_files.cmake
|
||||
#
|
||||
# LIST sets FILES to the paths to join, in order. The pack is one gzip
|
||||
# stream, so each file compresses against the files before it: the addon
|
||||
# text packs about 16% smaller than when each file is gzipped alone. Inside
|
||||
# the stream is an index, each file's size as a decimal line and then an
|
||||
# empty line, followed by the files' bytes in FILES order. The DLL splits
|
||||
# it in src/embed/Resource.cpp.
|
||||
|
||||
include("${LIST}")
|
||||
|
||||
set(index "")
|
||||
foreach(file IN LISTS FILES)
|
||||
file(SIZE "${file}" size)
|
||||
string(APPEND index "${size}\n")
|
||||
endforeach()
|
||||
# Not file(WRITE): on Windows it writes each \n as \r\n.
|
||||
file(CONFIGURE OUTPUT "${OUT}.index" CONTENT "${index}\n" @ONLY NEWLINE_STYLE UNIX)
|
||||
|
||||
# Join `ARGN` byte for byte into `out`.
|
||||
function(cat out)
|
||||
execute_process(COMMAND "${CMAKE_COMMAND}" -E cat ${ARGN}
|
||||
OUTPUT_FILE "${out}" RESULT_VARIABLE rc)
|
||||
if(NOT rc EQUAL 0)
|
||||
message(FATAL_ERROR "pack_files.cmake: cannot join into ${out}: ${rc}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# `cmake -E cat` takes its files on the command line, which Windows limits
|
||||
# to 32767 characters: join in batches of 100, then join the batches.
|
||||
set(parts "${OUT}.index")
|
||||
list(LENGTH FILES count)
|
||||
set(first 0)
|
||||
while(first LESS count)
|
||||
list(SUBLIST FILES ${first} 100 batch)
|
||||
set(part "${OUT}.part${first}")
|
||||
cat("${part}" ${batch})
|
||||
list(APPEND parts "${part}")
|
||||
math(EXPR first "${first} + 100")
|
||||
endwhile()
|
||||
cat("${OUT}.raw" ${parts})
|
||||
|
||||
# The raw format holds the one file's bytes with no archive entry around
|
||||
# them, so OUT is a plain gzip stream.
|
||||
file(ARCHIVE_CREATE OUTPUT "${OUT}" PATHS "${OUT}.raw" FORMAT raw
|
||||
COMPRESSION GZip COMPRESSION_LEVEL 9)
|
||||
file(REMOVE ${parts} "${OUT}.raw")
|
||||
+2058
-204
File diff suppressed because it is too large
Load Diff
@@ -27,24 +27,22 @@ The DLL hooks the engine's own text pipeline — no companion addon:
|
||||
pen is lazy and omits the last glyph's own advance, so a coin sat on its
|
||||
digits; the emitter now adds the true terminal advance from the engine's own
|
||||
glyph-advance helpers (`550cf5e`).
|
||||
- **Rendering** — the paint-pass co-hook `FUN_005c8fe0` walks the icon records
|
||||
and queues each to `Text::InlineTexturePool`, which places the region on the
|
||||
next frame tick (never mid-render).
|
||||
- **Chat has no one-frame lag** — a ScrollingMessageFrame re-`SetText`s every
|
||||
visible line on each new message, and the engine builds each line lazily at
|
||||
paint, so a line's icons would land one frame after its glyphs (glyphs draw
|
||||
synchronously; the icon regions are separate objects whose placement is only
|
||||
known once the line builds mid-paint, after the frame's texture pass already
|
||||
drew). A co-hook on the SMF display refresh (`FUN_00788750`, which runs
|
||||
pre-render from the frame's own methods) forces each icon-bearing line to
|
||||
resolve+build and applies its icon placement synchronously — before the frame
|
||||
draws — so icons move in lockstep with their text. It reuses the engine's own
|
||||
`Realize(0)` → `RebuildString` → ensure-built path and the same placement math
|
||||
the paint flush uses (`ComputePlacementsForNode`), so the later flush dedups to
|
||||
a no-op. Additive and best-effort: any line whose rect isn't resolvable in time
|
||||
falls back to the paint + tick pipeline (correct, one-frame lagged). A pure
|
||||
scroll or drag never lagged — the icons are anchored to the line and the engine
|
||||
moves them for free; only content changes on a reused line needed this.
|
||||
- **Rendering** — 4.3.4's `CSimpleEmbeddedTexture` model. Each icon is a
|
||||
pooled `CSimpleTexture` with no frame, anchored to its fontstring: it is in
|
||||
no draw layer, so the fontstring draws it. The fontstring's batch call
|
||||
(`FUN_FONTSTRING_EMIT` `0x00772E50`, 4.3.4's `CSimpleFontString::Draw` →
|
||||
`QueueTextures`) adds the icons right after its text, and only when the text
|
||||
went in, so an icon shows exactly when its line does — hidden, faded out, off
|
||||
screen, or scrolled off the top of a chat frame — in the same frame.
|
||||
- **No one-frame lag** — 4.3.4 sets the embedded textures up in `UpdateString`,
|
||||
right after it builds the string. The `RebuildString` co-hook does the same:
|
||||
a rebuild drops the fontstring's icons, lays the new node out now (the
|
||||
ensure-built call the paint makes) and applies the placements at once, so a
|
||||
string's icons draw with its text wherever it was rebuilt (a chat line laid
|
||||
out by its frame, a tooltip line, a rect change). The paint-pass co-hook
|
||||
`FUN_005c8fe0` still walks the icon records and queues them to
|
||||
`Text::InlineTexturePool` for the next frame tick: the backstop for a string
|
||||
whose placement can't be computed at rebuild time.
|
||||
- **Placement coordinates** — text pen space IS render-target pixels. The
|
||||
engine stores node positions NORMALIZED (fs-local anchor ÷
|
||||
`[0x832A44]`/`[0x832A48]` per axis, `FUN_0041ade0`), and the origin finalize
|
||||
@@ -107,10 +105,8 @@ Every fs-level measure path now counts icons: width (`FUN_00772890`), height
|
||||
refute that: the GameTooltip auto-size measures each WRAPPED SEGMENT of a
|
||||
wrap-enabled line through it, so icon-bearing wrapped tooltip lines
|
||||
undersized their tooltip until it was hooked too). The Lua control surface is
|
||||
two toggles: `_classicapi_InlineTexEnable` (the whole-feature kill switch; the
|
||||
SEH latch trips it on a flush fault) and `_classicapi_InlineTexEarly` (the
|
||||
SMF-refresh early apply above — on by default, toggle off to A/B the chat lag
|
||||
against the baseline paint+tick pipeline, which keeps running either way). The
|
||||
one toggle: `_classicapi_InlineTexEnable` (the whole-feature kill switch; the
|
||||
SEH latch trips it on a fault). The
|
||||
bring-up tune / stat / probe functions were removed (`5e5677b`, `6800a9a`).
|
||||
|
||||
## REMOVED (bring-up record) — the raw-GxU-quad rendering primitive
|
||||
|
||||
+12
-9
@@ -234,16 +234,19 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
|
||||
CloseHandle(worker);
|
||||
}
|
||||
} else if (reason == DLL_PROCESS_DETACH) {
|
||||
// Clean /quit path — only tear down if we actually initialized.
|
||||
// Flush before MH_Uninitialize — the cache's file I/O uses Win32
|
||||
// directly (no MinHook involvement), so either order works in
|
||||
// practice, but flushing first lets us bail early if the hook
|
||||
// teardown ever grows side effects. Hard process termination (task
|
||||
// manager kill) bypasses this path; that's an inherent OS
|
||||
// limitation, and the 5-minute backstop flush in Remember() covers
|
||||
// the worst case there.
|
||||
// Process exit — only tear down if we actually initialized. A
|
||||
// clean /quit already saved the name cache from its atexit handler,
|
||||
// which runs before this; this save covers an exit that skips the
|
||||
// atexit handlers (Storm's fatal exit 0x0064CC80 calls ExitProcess
|
||||
// itself) and a save that failed there. Save before
|
||||
// MH_Uninitialize — the cache's file I/O uses Win32 directly (no
|
||||
// MinHook involvement), so either order works in practice, but
|
||||
// saving first lets us bail early if the hook teardown ever grows
|
||||
// side effects. Hard process termination (task manager kill)
|
||||
// bypasses this path; that's an inherent OS limitation, and the
|
||||
// 5-minute backstop flush covers the worst case there.
|
||||
if (g_mhInitialized) {
|
||||
Player::NameCache::Flush();
|
||||
Player::NameCache::Shutdown();
|
||||
MH_Uninitialize();
|
||||
}
|
||||
}
|
||||
|
||||
+27
-6
@@ -15,6 +15,7 @@
|
||||
#include "MinHook.h"
|
||||
#include "Offsets.h"
|
||||
#include "api/Documentation.h"
|
||||
#include "compat/WowTurbo.h"
|
||||
|
||||
namespace Game {
|
||||
|
||||
@@ -89,6 +90,9 @@ namespace Lua {
|
||||
F(ToPointer, lua_topointer, LUA_TO_POINTER) \
|
||||
F(ToThread, lua_tothread, LUA_TO_THREAD) \
|
||||
F(IsCFunction, lua_iscfunction, LUA_IS_C_FUNCTION)\
|
||||
F(ToCFunction, lua_tocfunction, LUA_TO_C_FUNCTION)\
|
||||
F(GetStack, lua_getstack, LUA_GET_STACK) \
|
||||
F(GetInfo, lua_getinfo, LUA_GET_INFO) \
|
||||
F(XMove, lua_xmove, LUA_X_MOVE) \
|
||||
F(NewThread, lua_newthread, LUA_NEW_THREAD) \
|
||||
F(Resume, lua_resume, LUA_RESUME) \
|
||||
@@ -98,7 +102,9 @@ namespace Lua {
|
||||
F(Ref, luaL_ref, LUA_REF_REF) \
|
||||
F(Unref, luaL_unref, LUA_REF_UNREF) \
|
||||
F(CheckStack, lua_checkstack, LUA_CHECK_STACK) \
|
||||
F(Concat, lua_concat, LUA_CONCAT)
|
||||
F(Concat, lua_concat, LUA_CONCAT) \
|
||||
F(NewUserdata, lua_newuserdata, LUA_NEW_USERDATA) \
|
||||
F(ToUserdata, lua_touserdata, FUN_FRAMESCRIPT_GET_OBJECT)
|
||||
|
||||
#define CLASSICAPI_BIND_LUA(Name, Typedef, Offset) \
|
||||
const Typedef##_t Name = reinterpret_cast<Typedef##_t>(Offsets::Offset);
|
||||
@@ -401,12 +407,12 @@ void RegisterGlobalAlias(const char *alias, const char *tableName, const char *m
|
||||
Api::Documentation::RecordGlobal(alias, doc);
|
||||
}
|
||||
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem) {
|
||||
// `_G[parent][sub] = { entries }`, creating `_G[parent]` if needed.
|
||||
static bool SetIntegerTable(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count) {
|
||||
void *L = State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
return false;
|
||||
EnsureGlobalTable(L, parent); // [parentTbl]
|
||||
PushString(L, sub); // [parentTbl, subName]
|
||||
NewTable(L); // [parentTbl, subName, subTbl]
|
||||
@@ -417,7 +423,20 @@ void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
}
|
||||
SetTable(L, -3); // parentTbl[sub] = subTbl
|
||||
SetTop(L, -2); // pop parentTbl
|
||||
Api::Documentation::RecordEnum(parent, sub, entries, count, docSystem);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem) {
|
||||
if (SetIntegerTable(parent, sub, entries, count))
|
||||
Api::Documentation::RecordEnum(parent, sub, entries, count, docSystem);
|
||||
}
|
||||
|
||||
void RegisterConstants(const char *sub, const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem) {
|
||||
if (SetIntegerTable("Constants", sub, entries, count))
|
||||
Api::Documentation::RecordConstants(sub, entries, count, docSystem);
|
||||
}
|
||||
|
||||
void SetFieldNumber(void *L, const char *key, double value) {
|
||||
@@ -568,6 +587,8 @@ HookAutoRegister::HookAutoRegister(uintptr_t target, void *hook, void **original
|
||||
bool RunHookRegistrations() {
|
||||
for (auto *node = g_hookHead; node != nullptr; node = node->next) {
|
||||
auto *targetPtr = reinterpret_cast<LPVOID>(node->target);
|
||||
// A hook must wrap the engine's body, not wow_turbo's reimplementation.
|
||||
Compat::WowTurbo::Reclaim(node->target);
|
||||
if (MH_CreateHook(targetPtr, node->hook,
|
||||
reinterpret_cast<LPVOID *>(node->original)) != MH_OK)
|
||||
return false;
|
||||
|
||||
+23
-3
@@ -298,6 +298,11 @@ using lua_error_t = void(__cdecl *)(void *L, const char *fmt, ...);
|
||||
using lua_topointer_t = const void *(__fastcall *)(void *L, int idx);
|
||||
using lua_tothread_t = void *(__fastcall *)(void *L, int idx);
|
||||
using lua_iscfunction_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_tocfunction_t = CFunction(__fastcall *)(void *L, int idx);
|
||||
// Fill a lua_Debug (Offsets::SIZEOF_LUA_DEBUG bytes) for a call level /
|
||||
// with the fields `what` names. Level 0 is the running C function.
|
||||
using lua_getstack_t = int(__fastcall *)(void *L, int level, void *ar);
|
||||
using lua_getinfo_t = int(__fastcall *)(void *L, const char *what, void *ar);
|
||||
using lua_xmove_t = void(__fastcall *)(void *from, void *to, int n);
|
||||
// `lua_newthread` returns the new `lua_State *` in eax, but Ghidra
|
||||
// infers `void` because the inner call's value just flows through.
|
||||
@@ -319,6 +324,10 @@ using lua_checkstack_t = int(__fastcall *)(void *L, int size);
|
||||
// Pops the top `n` values and pushes their `..` concatenation; n == 0 pushes
|
||||
// "" and n == 1 leaves the value untouched.
|
||||
using lua_concat_t = void(__fastcall *)(void *L, int n);
|
||||
// Pushes a new full userdata of `size` bytes and returns its block.
|
||||
using lua_newuserdata_t = void *(__fastcall *)(void *L, unsigned int size);
|
||||
// The block of the full userdata at idx, the pointer of a light one, else null.
|
||||
using lua_touserdata_t = void *(__fastcall *)(void *L, int idx);
|
||||
|
||||
extern const lua_isnumber_t IsNumber;
|
||||
extern const lua_isstring_t IsString;
|
||||
@@ -366,6 +375,9 @@ extern const lua_error_t Error;
|
||||
extern const lua_topointer_t ToPointer;
|
||||
extern const lua_tothread_t ToThread;
|
||||
extern const lua_iscfunction_t IsCFunction;
|
||||
extern const lua_tocfunction_t ToCFunction;
|
||||
extern const lua_getstack_t GetStack;
|
||||
extern const lua_getinfo_t GetInfo;
|
||||
extern const lua_xmove_t XMove;
|
||||
extern const lua_newthread_t NewThread;
|
||||
extern const lua_resume_t Resume;
|
||||
@@ -376,6 +388,8 @@ extern const luaL_ref_t Ref;
|
||||
extern const luaL_unref_t Unref;
|
||||
extern const lua_checkstack_t CheckStack;
|
||||
extern const lua_concat_t Concat;
|
||||
extern const lua_newuserdata_t NewUserdata;
|
||||
extern const lua_touserdata_t ToUserdata;
|
||||
|
||||
// Returns the global `lua_State *` (read on demand from the engine's global).
|
||||
// Callable outside a Lua callback, e.g. during LoadScriptFunctions setup.
|
||||
@@ -516,9 +530,9 @@ void RegisterTableFunction(const char *tableName, const char *methodName,
|
||||
void RegisterGlobalAlias(const char *alias, const char *tableName, const char *methodName,
|
||||
const Doc::Function *doc = nullptr);
|
||||
|
||||
// Key/value pair for `RegisterIntegerEnum`. `key` becomes a field name
|
||||
// (PascalCase, matching Blizzard's `Enum.*` naming) and `value` is the
|
||||
// integer the enum field maps to.
|
||||
// Key/value pair for `RegisterIntegerEnum` and `RegisterConstants`. `key`
|
||||
// becomes a field name (PascalCase for Blizzard's `Enum.*`, UPPER_CASE for
|
||||
// its `Constants.*`) and `value` is the integer the field maps to.
|
||||
struct EnumIntegerEntry {
|
||||
const char *key;
|
||||
int value;
|
||||
@@ -533,6 +547,12 @@ void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem = nullptr);
|
||||
|
||||
// Registers `Constants[sub] = { entries }`, Blizzard's engine constant
|
||||
// tables (`Constants.MoneyFormattingConstants`). Documented as a "Constants"
|
||||
// table of `docSystem`.
|
||||
void RegisterConstants(const char *sub, const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem);
|
||||
|
||||
// Set `t[key] = value` on the table currently at stack[-1] (the most
|
||||
// common shape used when populating a struct-style table mid-build).
|
||||
// Pushes the key + value, calls `SetTable(L, -3)`, which pops both and
|
||||
|
||||
@@ -32,9 +32,8 @@ namespace {
|
||||
// that are not base-library functions at all. So do not derive this from
|
||||
// the engine's `luaopen_base` table (`0x00811E28`, 36 entries — see
|
||||
// BlizzardScriptAPI.md §3); that is a different set with a different
|
||||
// purpose. Names with no 1.12 counterpart (`wipe` is `table.wipe` here; the
|
||||
// secure family does not exist) stay for parity of the error an addon
|
||||
// sees. Sorted alphabetically.
|
||||
// purpose. Names with no 1.12 counterpart (the secure family does not
|
||||
// exist) stay for parity of the error an addon sees. Sorted alphabetically.
|
||||
const char *const kUnhookableNames[] = {
|
||||
"getfenv", "getmetatable", "hooksecurefunc", "ipairs", "issecurevalue",
|
||||
"issecurevariable", "next", "pairs", "pcall", "pcallwithenv", "rawget",
|
||||
|
||||
+674
-104
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,8 @@
|
||||
// The function's one caller, Script_IsActionInRange, is redirected rather than
|
||||
// the function hooked.
|
||||
|
||||
#include "action/Range.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -107,4 +109,6 @@ const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
int Answer(int slot0, int *inRange) { return SlotRange_h(slot0, inRange); }
|
||||
|
||||
} // namespace Action::Range
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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 Action::Range {
|
||||
|
||||
// IsActionInRange's answer for a slot (0-based, 0..119): 0 when it has none
|
||||
// (IsActionInRange's nil), else 1 with `*inRange` 0 or 1. A macro aimed at a
|
||||
// unit is checked against that unit.
|
||||
int Answer(int slot0, int *inRange);
|
||||
|
||||
} // namespace Action::Range
|
||||
@@ -0,0 +1,144 @@
|
||||
// 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_ActionBar.EnableActionRangeCheck(slot, enable)` and
|
||||
// `ACTION_RANGE_CHECK_UPDATE(slot, isInRange, checksRange)`.
|
||||
//
|
||||
// Nothing in the client reacts to a distance (see Unit::InRangeEvent), so the
|
||||
// enabled slots are checked every world tick, while a frame is registered for
|
||||
// the event, and a slot fires when its answer changes. The answer is
|
||||
// IsActionInRange's (Action::Range::Answer), macro unit included, so a slot
|
||||
// whose action changes fires when the new action's answer differs.
|
||||
//
|
||||
// Enabling is counted per slot, as retail's ActionBarButtonRangeCheckFrame
|
||||
// expects: it enables once for each button on a slot and disables once as
|
||||
// each button leaves. Retail's buttons never call IsActionInRange; they take
|
||||
// their first state from this event. So each enable makes the slot's next
|
||||
// check fire, changed or not. A new Lua state (`/reload`) starts with no slot
|
||||
// enabled.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "action/Range.h"
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::RangeCheck {
|
||||
|
||||
namespace {
|
||||
|
||||
enum class State : uint8_t { NoCheck, OutOfRange, InRange };
|
||||
|
||||
struct Slot {
|
||||
int refs = 0;
|
||||
bool pending = false; // fire on the next check, changed or not
|
||||
State last = State::NoCheck;
|
||||
};
|
||||
|
||||
Slot g_slots[Offsets::ACTION_TABLE_MAX_SLOTS];
|
||||
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
Game::Doc::Req("slot", "luaIndex"),
|
||||
Game::Doc::Req("isInRange", "bool",
|
||||
"Whether the action's target is in range. Should not be used if checksRange "
|
||||
"is false."),
|
||||
Game::Doc::Req("checksRange", "bool",
|
||||
"False when no range check was made, for example when there is no target."),
|
||||
};
|
||||
const Game::Doc::Event kDoc{
|
||||
"ActionBar",
|
||||
"Fires when an action that has EnableActionRangeCheck on goes in or out of range of its "
|
||||
"target.",
|
||||
kPayload};
|
||||
const Event::Custom::AutoReserve kEvent{"ACTION_RANGE_CHECK_UPDATE", &kDoc};
|
||||
|
||||
State Evaluate(int slot0) {
|
||||
int inRange = 0;
|
||||
if (Action::Range::Answer(slot0, &inRange) == 0)
|
||||
return State::NoCheck;
|
||||
return inRange != 0 ? State::InRange : State::OutOfRange;
|
||||
}
|
||||
|
||||
void OnWorldTick() {
|
||||
const int event = kEvent.Slot();
|
||||
if (!Event::Custom::HasListeners(event))
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
for (int slot0 = 0; slot0 < Offsets::ACTION_TABLE_MAX_SLOTS; ++slot0) {
|
||||
Slot &slot = g_slots[slot0];
|
||||
if (slot.refs == 0)
|
||||
continue;
|
||||
const State state = Evaluate(slot0);
|
||||
if (!slot.pending && state == slot.last)
|
||||
continue;
|
||||
slot.pending = false;
|
||||
slot.last = state;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slot0 + 1));
|
||||
Game::Lua::PushBool(L, state == State::InRange);
|
||||
Game::Lua::PushBool(L, state != State::NoCheck);
|
||||
Event::Custom::FireValues(event, L, top + 1, 3);
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
|
||||
|
||||
// --- C_ActionBar.EnableActionRangeCheck(slot, enable) -----------------------
|
||||
|
||||
int __fastcall Script_EnableActionRangeCheck(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_ActionBar.EnableActionRangeCheck(slot, enable)");
|
||||
return 0;
|
||||
}
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
|
||||
return 0;
|
||||
Slot &slot = g_slots[slot0];
|
||||
if (Game::Lua::ToBoolean(L, 2) != 0) {
|
||||
++slot.refs;
|
||||
slot.pending = true;
|
||||
} else if (slot.refs > 0) {
|
||||
--slot.refs;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kEnableArgs[] = {
|
||||
Game::Doc::Req("slot", "luaIndex"),
|
||||
Game::Doc::Req("enable", "bool",
|
||||
"True if changes in range for the action should dispatch "
|
||||
"ACTION_RANGE_CHECK_UPDATE. False if the action no longer needs the event."),
|
||||
};
|
||||
const Game::Doc::Function kEnableDoc{
|
||||
"Used with ACTION_RANGE_CHECK_UPDATE to be informed when an action goes in or out of range "
|
||||
"of its target.",
|
||||
kEnableArgs};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
for (Slot &slot : g_slots)
|
||||
slot = Slot{};
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "EnableActionRangeCheck",
|
||||
&Script_EnableActionRangeCheck, &kEnableDoc);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Action::RangeCheck
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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/>.
|
||||
|
||||
// `ACTION_USABLE_CHANGED(changes)` — the slots whose usable / noMana pair
|
||||
// changed, as `{ slot, usable, noMana }` entries.
|
||||
//
|
||||
// The engine's bulk refresh FUN_ACTION_UPDATE_USABLE already finds them: it
|
||||
// computes each slot with FUN_ACTION_SLOT_USABLE, compares the pair against
|
||||
// the caches IsUsableAction reads, stores a changed pair, and signals the
|
||||
// argument-less ACTIONBAR_UPDATE_USABLE once when any changed. Two of its
|
||||
// calls are redirected. The per-slot call records the slots whose pair differs
|
||||
// from the caches, the same compare the engine makes next. The signal sends
|
||||
// ACTIONBAR_UPDATE_USABLE unchanged, then this event with the recorded slots.
|
||||
//
|
||||
// The per-slot refresh in FUN_ACTION_SLOT_CHANGED_NOTIFY stores its pair
|
||||
// without this event; it signals ACTIONBAR_SLOT_CHANGED for the slot instead.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Action::UsableChanged {
|
||||
|
||||
namespace {
|
||||
|
||||
using SlotUsable_t = uint32_t(__fastcall *)(uint32_t slot0, uint32_t *outNoMana);
|
||||
using FireEvent_t = void(__fastcall *)(int eventID);
|
||||
|
||||
struct Change {
|
||||
uint32_t slot0;
|
||||
bool usable;
|
||||
bool noMana;
|
||||
};
|
||||
|
||||
// This refresh's changes. The loop runs slots 0..119 in order, so slot 0
|
||||
// starts a new list.
|
||||
Change g_changes[Offsets::ACTION_TABLE_MAX_SLOTS];
|
||||
int g_count = 0;
|
||||
|
||||
const Game::Doc::Field kStateFields[] = {
|
||||
Game::Doc::Req("slot", "luaIndex"),
|
||||
Game::Doc::Req("usable", "bool"),
|
||||
Game::Doc::Req("noMana", "bool"),
|
||||
};
|
||||
const Game::Doc::Structure kState{"ActionUsableState", "ActionBar", kStateFields};
|
||||
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
Game::Doc::Req("changes", "table", "An ActionUsableState for each slot that changed."),
|
||||
};
|
||||
const Game::Doc::Event kDoc{
|
||||
"ActionBar", "Fires when the usable or noMana state of one or more action slots changes.",
|
||||
kPayload};
|
||||
const Event::Custom::AutoReserve kEvent{"ACTION_USABLE_CHANGED", &kDoc};
|
||||
|
||||
uint32_t __fastcall SlotUsable_h(uint32_t slot0, uint32_t *outNoMana) {
|
||||
const uint32_t usable =
|
||||
reinterpret_cast<SlotUsable_t>(Offsets::FUN_ACTION_SLOT_USABLE)(slot0, outNoMana);
|
||||
if (slot0 == 0)
|
||||
g_count = 0;
|
||||
const auto *usableCache =
|
||||
reinterpret_cast<const uint32_t *>(static_cast<uintptr_t>(Offsets::VAR_ACTION_USABLE));
|
||||
const auto *noManaCache =
|
||||
reinterpret_cast<const uint32_t *>(static_cast<uintptr_t>(Offsets::VAR_ACTION_NO_MANA));
|
||||
if (slot0 < static_cast<uint32_t>(Offsets::ACTION_TABLE_MAX_SLOTS) &&
|
||||
g_count < Offsets::ACTION_TABLE_MAX_SLOTS &&
|
||||
(usable != usableCache[slot0] || *outNoMana != noManaCache[slot0]))
|
||||
g_changes[g_count++] = {slot0, usable != 0, *outNoMana != 0};
|
||||
return usable;
|
||||
}
|
||||
|
||||
void __fastcall FireUsable_h(int eventID) {
|
||||
// Copied first: the handlers below can run another refresh.
|
||||
Change changes[Offsets::ACTION_TABLE_MAX_SLOTS];
|
||||
const int count = g_count;
|
||||
std::memcpy(changes, g_changes, sizeof(Change) * count);
|
||||
g_count = 0;
|
||||
|
||||
reinterpret_cast<FireEvent_t>(Offsets::FUN_FIRE_EVENT_NO_ARGS)(eventID);
|
||||
|
||||
const int event = kEvent.Slot();
|
||||
void *L = Game::Lua::State();
|
||||
if (count == 0 || !Event::Custom::HasListeners(event) || L == nullptr)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
Game::Lua::NewTable(L);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "slot", static_cast<double>(changes[i].slot0 + 1));
|
||||
Game::Lua::SetFieldBool(L, "usable", changes[i].usable);
|
||||
Game::Lua::SetFieldBool(L, "noMana", changes[i].noMana);
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
Game::Lua::SetN(L, -1, count);
|
||||
Event::Custom::FireValues(event, L, top + 1, 1);
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Common::RedirectCall(Offsets::CALL_UPDATE_USABLE_SLOT_USABLE, Offsets::FUN_ACTION_SLOT_USABLE,
|
||||
reinterpret_cast<const void *>(&SlotUsable_h));
|
||||
Common::RedirectCall(Offsets::CALL_UPDATE_USABLE_FIRE, Offsets::FUN_FIRE_EVENT_NO_ARGS,
|
||||
reinterpret_cast<const void *>(&FireUsable_h));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Action::UsableChanged
|
||||
+62
-43
@@ -15,18 +15,19 @@
|
||||
// this module makes the engine think they do — without writing anything
|
||||
// to the filesystem. The addons are `Manifest::kAddons` (the `ADDONS`
|
||||
// list in CMakeLists.txt): `!!!ClassicAPI`, which loads at login, and
|
||||
// `ClassicAPI_DebugTools`, which is `## LoadOnDemand` behind the
|
||||
// `/dump` / `/fstack` / `/etrace` stubs in `!!!ClassicAPI` — retail's
|
||||
// chat-frame + Blizzard_DebugTools split. (The `Blizzard_` name is not
|
||||
// `ClassicAPI_DebugTools` and `ClassicAPI_EventTrace`, which are
|
||||
// `## LoadOnDemand` behind the `/dump` / `/fstack` / `/etrace` stubs in
|
||||
// `!!!ClassicAPI` — retail's chat-frame + Blizzard_DebugTools /
|
||||
// Blizzard_EventTrace split. (The `Blizzard_` name is not
|
||||
// available to us: the engine moves loose `Interface\AddOns\Blizzard*`
|
||||
// folders aside at startup, `MoveAsideLooseInterface`, and refuses a
|
||||
// `Blizzard_*` TOC without a signature.)
|
||||
//
|
||||
// How:
|
||||
//
|
||||
// 1. The CMake build embeds every file of every listed addon as an
|
||||
// RCDATA resource, plus a generated `{path, resourceId}` manifest
|
||||
// (`embedded_resources.h`).
|
||||
// 1. The CMake build embeds each listed addon as one RCDATA resource,
|
||||
// a gzipped pack of its files, plus a generated
|
||||
// `{path, resourceId, index}` manifest (`embedded_resources.h`).
|
||||
//
|
||||
// 2. We hook the engine's file-read function at `FUN_FILE_READ`.
|
||||
// When the engine asks for a path under
|
||||
@@ -34,6 +35,9 @@
|
||||
// via the same allocator the engine itself uses, copy the
|
||||
// embedded content in, and hand it back. The caller's normal
|
||||
// `SMemFree` reclaims it on completion — no special lifetime.
|
||||
// A TOC then goes through `AddOns::TocRewrite`, as an installed
|
||||
// addon's does, so its file lines can carry `[AllowLoadGameType ...]`
|
||||
// / `[ExcludeLoadGameType ...]` conditions.
|
||||
//
|
||||
// 3. We hook the exists-check `FUN_FILE_EXISTS` so it answers the same
|
||||
// way for the same paths. Without it every embedded file reads fine
|
||||
@@ -153,7 +157,8 @@ int MatchAddonPath(const char *path, const char **relPath) {
|
||||
Embed::Bytes LookupEmbedded(const char *relPath) {
|
||||
for (size_t i = 0; i < Manifest::kFileCount; ++i) {
|
||||
if (PathEqualsCI(relPath, Manifest::kFiles[i].path))
|
||||
return Embed::Resource(Manifest::kFiles[i].resourceId);
|
||||
return Embed::File(Manifest::kFiles[i].resourceId,
|
||||
Manifest::kFiles[i].index);
|
||||
}
|
||||
return {nullptr, 0};
|
||||
}
|
||||
@@ -301,6 +306,49 @@ void DecideSource(int addon) {
|
||||
g_source[addon] = (cmp > 0) ? Source::Embedded : Source::Disk;
|
||||
}
|
||||
|
||||
// Serves one file of embedded addon `addon` from whichever source won
|
||||
// DecideSource.
|
||||
int ReadAddonFile(int addon, const char *relPath, int unused, const char *path,
|
||||
void **outBuf, size_t *outSize, size_t extraBytes, int flag1,
|
||||
int flag2) {
|
||||
DecideSource(addon);
|
||||
|
||||
if (g_source[addon] == Source::Disk) {
|
||||
// Disk version is at least as new — serve disk. If disk
|
||||
// doesn't have this specific file (e.g. embedded has files
|
||||
// disk doesn't), fall through and try embedded.
|
||||
const int diskResult = FileRead_o(unused, path, outBuf, outSize,
|
||||
extraBytes, flag1, flag2);
|
||||
if (diskResult != 0) return diskResult;
|
||||
}
|
||||
|
||||
// Source::Embedded path, OR Source::Disk-but-file-missing fallback.
|
||||
// Look up the embedded version and synthesize a Storm buffer the
|
||||
// caller can free normally.
|
||||
const Embed::Bytes entry = LookupEmbedded(relPath);
|
||||
if (entry.data == nullptr) {
|
||||
// Not in our embedded set either — let the engine try disk
|
||||
// one more time (returns 0 if that also fails). Covers
|
||||
// pathological cases like an empty disk install + embedded
|
||||
// missing some file.
|
||||
return FileRead_o(unused, path, outBuf, outSize, extraBytes,
|
||||
flag1, flag2);
|
||||
}
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAllocFn>(
|
||||
Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
const size_t totalSize = entry.size + extraBytes;
|
||||
void *buf = SMemAlloc(totalSize, __FILE__, __LINE__, 0);
|
||||
if (buf == nullptr) return 0;
|
||||
std::memcpy(buf, entry.data, entry.size);
|
||||
if (extraBytes > 0) {
|
||||
std::memset(static_cast<uint8_t *>(buf) + entry.size, 0, extraBytes);
|
||||
}
|
||||
if (outBuf != nullptr) *outBuf = buf;
|
||||
if (outSize != nullptr) *outSize = entry.size;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
size_t *outSize, size_t extraBytes,
|
||||
int flag1, int flag2) {
|
||||
@@ -344,42 +392,13 @@ int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
return result;
|
||||
}
|
||||
|
||||
DecideSource(addon);
|
||||
|
||||
if (g_source[addon] == Source::Disk) {
|
||||
// Disk version is at least as new — serve disk. If disk
|
||||
// doesn't have this specific file (e.g. embedded has files
|
||||
// disk doesn't), fall through and try embedded.
|
||||
const int diskResult = FileRead_o(unused, path, outBuf, outSize,
|
||||
extraBytes, flag1, flag2);
|
||||
if (diskResult != 0) return diskResult;
|
||||
}
|
||||
|
||||
// Source::Embedded path, OR Source::Disk-but-file-missing fallback.
|
||||
// Look up the embedded version and synthesize a Storm buffer the
|
||||
// caller can free normally.
|
||||
const Embed::Bytes entry = LookupEmbedded(relPath);
|
||||
if (entry.data == nullptr) {
|
||||
// Not in our embedded set either — let the engine try disk
|
||||
// one more time (returns 0 if that also fails). Covers
|
||||
// pathological cases like an empty disk install + embedded
|
||||
// missing some file.
|
||||
return FileRead_o(unused, path, outBuf, outSize, extraBytes,
|
||||
flag1, flag2);
|
||||
}
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAllocFn>(
|
||||
Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
const size_t totalSize = entry.size + extraBytes;
|
||||
void *buf = SMemAlloc(totalSize, __FILE__, __LINE__, 0);
|
||||
if (buf == nullptr) return 0;
|
||||
std::memcpy(buf, entry.data, entry.size);
|
||||
if (extraBytes > 0) {
|
||||
std::memset(static_cast<uint8_t *>(buf) + entry.size, 0, extraBytes);
|
||||
}
|
||||
if (outBuf != nullptr) *outBuf = buf;
|
||||
if (outSize != nullptr) *outSize = entry.size;
|
||||
return 1;
|
||||
// An embedded addon's TOC takes the same per-line directives as an
|
||||
// installed addon's, whichever source serves it.
|
||||
const int result = ReadAddonFile(addon, relPath, unused, path, outBuf, outSize,
|
||||
extraBytes, flag1, flag2);
|
||||
if (result != 0)
|
||||
AddOns::TocRewrite::Transform(path, outBuf, outSize);
|
||||
return result;
|
||||
}
|
||||
|
||||
// The exists-check counterpart of FileRead_h: the same source decision and
|
||||
|
||||
@@ -89,8 +89,8 @@ void PatchLoadWindow() {
|
||||
reinterpret_cast<const void *>(&LoadWindowCloseThunk));
|
||||
}
|
||||
|
||||
int __fastcall Script_UpdateAddOnMemoryUsage(void * /*L*/) {
|
||||
Gc::Tag::UpdateUsage();
|
||||
int __fastcall Script_UpdateAddOnMemoryUsage(void *L) {
|
||||
Gc::Tag::UpdateUsage(L);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -107,30 +107,15 @@ int __fastcall Script_GetAddOnMemoryUsage(void *L) {
|
||||
}
|
||||
|
||||
// `_classicapi_LuaHeapAudit()` → objectBytes, nblocks, taggedObjects.
|
||||
// Walks every collectable object the engine can reach — the main list,
|
||||
// the userdata list and the string table, the three lists `luaC_sweep`
|
||||
// sweeps — summing `Gc::Tag::SizeOf`. `objectBytes` should sit just under
|
||||
// `nblocks` (the engine's own byte count): the gap is what is not a GC
|
||||
// object (string-table buckets, the main thread and its stack, buffers).
|
||||
// A gap that grows with the heap means a size formula is wrong.
|
||||
uint64_t SumList(const uint8_t *object) {
|
||||
uint64_t total = 0;
|
||||
while (object != nullptr) {
|
||||
total += Gc::Tag::SizeOf(object);
|
||||
object = Game::Read<const uint8_t *>(object, Offsets::OFF_LUA_GCHEADER_NEXT);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// Sums `Gc::Tag::SizeOf` over every collectable object the engine can
|
||||
// reach. `objectBytes` should sit just under `nblocks` (the engine's own
|
||||
// byte count): the gap is what is not a GC object (string-table buckets,
|
||||
// the main thread and its stack, buffers). A gap that grows with the heap
|
||||
// means a size formula is wrong.
|
||||
int __fastcall Script_LuaHeapAudit(void *L) {
|
||||
uint64_t bytes = 0;
|
||||
Gc::Tag::ForEachObject(L, [&bytes](const void *object) { bytes += Gc::Tag::SizeOf(object); });
|
||||
const uint8_t *g = Game::Read<const uint8_t *>(L, Offsets::OFF_LUA_STATE_G);
|
||||
uint64_t bytes = SumList(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTGC)) +
|
||||
SumList(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTUDATA));
|
||||
const auto buckets =
|
||||
Game::Read<const uint8_t *const *>(g, Offsets::OFF_LUA_G_STRT_HASH);
|
||||
const int size = Game::Read<int>(g, Offsets::OFF_LUA_G_STRT_SIZE);
|
||||
for (int i = 0; i < size; ++i)
|
||||
bytes += SumList(buckets[i]);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(bytes));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(Game::Read<uint32_t>(g, Offsets::OFF_LUA_G_NBLOCKS)));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(Gc::Tag::TaggedCount()));
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "addons/Toc.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "turtle/Detect.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -139,13 +140,23 @@ bool TryRewriteInterfaceLine(const char *line, size_t len, std::string &out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// True iff the game-type list names a game type this client is: always
|
||||
// `vanilla`, and `turtle` (a ClassicAPI extension, like the `_Turtle.toc`
|
||||
// flavor) on a Turtle client.
|
||||
bool ListContainsGameType(const char *list, size_t listLen) {
|
||||
return ListContains(list, listLen, "vanilla") ||
|
||||
(Turtle::Detected() && ListContains(list, listLen, "turtle"));
|
||||
}
|
||||
|
||||
// Evaluate a `[keyword args]` condition — the return is whether the line should
|
||||
// load. An unrecognized keyword returns false so the caller drops the line (a
|
||||
// condition we cannot confirm must not load — fail safe).
|
||||
bool EvalCondition(const char *head, size_t headLen, const char *args,
|
||||
size_t argsLen) {
|
||||
if (EqCI(head, headLen, "AllowLoadGameType"))
|
||||
return ListContains(args, argsLen, "vanilla");
|
||||
return ListContainsGameType(args, argsLen);
|
||||
if (EqCI(head, headLen, "ExcludeLoadGameType"))
|
||||
return !ListContainsGameType(args, argsLen);
|
||||
if (EqCI(head, headLen, "AllowLoadTextLocale"))
|
||||
return ListContains(args, argsLen, ClientLocale());
|
||||
if (EqCI(head, headLen, "AllowLoad"))
|
||||
|
||||
@@ -42,7 +42,15 @@ namespace AddOns::TocRewrite {
|
||||
// below); metadata-line conditions, added in retail 12.0.7, are out of
|
||||
// scope. What the tokens resolve to on this 1.12 client:
|
||||
//
|
||||
// [AllowLoadGameType ...] keep the line iff the list contains `vanilla`
|
||||
// [AllowLoadGameType ...] keep the line iff the list names a game type
|
||||
// of this client
|
||||
// [ExcludeLoadGameType ...] keep the line iff the list names none
|
||||
// This client is the `vanilla` game type, and
|
||||
// also `turtle` (a ClassicAPI extension, like
|
||||
// the `_Turtle.toc` flavor) when
|
||||
// Turtle::Detected(). So
|
||||
// `[ExcludeLoadGameType turtle]` loads a line
|
||||
// everywhere but Turtle.
|
||||
// [AllowLoadTextLocale ...] keep the line iff the list contains the
|
||||
// client locale (GetLocale() code, e.g. enUS)
|
||||
// [AllowLoad ...] addon files only load in-game, so `game`
|
||||
|
||||
+216
-16
@@ -15,6 +15,17 @@
|
||||
// subclasses (wowdecomp15595 CSimpleAnim.cpp, CSimple*Anim.cpp and their
|
||||
// _Scripts.cpp), on Animation::Object, and the group's CreateAnimation.
|
||||
// Path and its control points are in Path.cpp.
|
||||
//
|
||||
// Later clients' additions, on the same objects:
|
||||
// - the target setters (SetTarget, SetTargetName, SetTargetKey,
|
||||
// SetChildKey, SetTargetParent, GetTarget);
|
||||
// - Alpha's fromAlpha / toAlpha. Blizzard's own code starts a fade from
|
||||
// the region's current alpha with SetFromAlpha(region:GetAlpha()), and
|
||||
// its XML fades in with toAlpha="1" alone, so the alpha is absolute (the
|
||||
// target's alpha becomes from + (to - from) x progress) and an unset
|
||||
// fromAlpha is 0. SetChange goes back to 4.3.4's relative change;
|
||||
// - Scale's SetScaleFrom / SetScaleTo. 4.3.4's scale is the case from = 1,
|
||||
// and SetScale sets the end, so its old results do not change.
|
||||
|
||||
#include "animation/Classes.h"
|
||||
#include "animation/Region.h"
|
||||
@@ -25,7 +36,9 @@
|
||||
#include "baselib/Ascii.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace Animation {
|
||||
|
||||
@@ -79,9 +92,15 @@ Anim::~Anim() {
|
||||
|
||||
// ---- Playback --------------------------------------------------------------------
|
||||
|
||||
bool Anim::Play() {
|
||||
if (targetName[0] != '\0')
|
||||
void Anim::ResolveTarget() {
|
||||
if (!targetKey.empty())
|
||||
target = Region::FindRegionByKey(this, targetKey.c_str());
|
||||
else if (targetName[0] != '\0')
|
||||
target = Region::FindRegionByName(targetName);
|
||||
}
|
||||
|
||||
bool Anim::Play() {
|
||||
ResolveTarget();
|
||||
if (playState != 1) {
|
||||
playState = 1;
|
||||
RunScript(A_ONPLAY, 0);
|
||||
@@ -204,14 +223,21 @@ void Anim::Apply(float p) {
|
||||
return;
|
||||
switch (kind) {
|
||||
case KIND_ALPHA:
|
||||
Region::AddAnimAlpha(region, target, static_cast<int16_t>(change * p));
|
||||
if (alphaFromTo)
|
||||
Region::SetAnimAlpha(region, target, static_cast<uint8_t>((fromAlpha + (toAlpha - fromAlpha) * p) * 255.0f));
|
||||
else
|
||||
Region::AddAnimAlpha(region, target, static_cast<int16_t>(change * p));
|
||||
break;
|
||||
case KIND_TRANSLATION:
|
||||
Region::AddAnimTranslation(region, target, offsetX * p, offsetY * p);
|
||||
break;
|
||||
case KIND_SCALE:
|
||||
Region::AddAnimScale(region, target, originPoint, originX, originY, scaleChangeX * p, scaleChangeY * p);
|
||||
case KIND_SCALE: {
|
||||
// AddAnimScale takes the share of the way to the origin, 1 - scale.
|
||||
const float x = scaleFromX + (scaleToX - scaleFromX) * p;
|
||||
const float y = scaleFromY + (scaleToY - scaleFromY) * p;
|
||||
Region::AddAnimScale(region, target, originPoint, originX, originY, 1.0f - x, 1.0f - y);
|
||||
break;
|
||||
}
|
||||
case KIND_ROTATION:
|
||||
Region::AddAnimRotation(region, target, originPoint, originX, originY, radians * p);
|
||||
break;
|
||||
@@ -473,6 +499,101 @@ int __fastcall Script_GetRegionParent(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- Target (later clients) ----
|
||||
//
|
||||
// An animation with no target animates its group's region. Each setter
|
||||
// replaces the others' target and returns whether it names a region now;
|
||||
// a name or key is looked up again on Play, as 4.3.4 looks up `target`.
|
||||
|
||||
int PushTargetFound(void *L, const Anim *a) {
|
||||
Game::Lua::PushBool(L, a->target != nullptr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void *RegionArg(void *L, int idx) {
|
||||
if (Game::Lua::Type(L, idx) != Game::Lua::TYPE_TABLE)
|
||||
return nullptr;
|
||||
void *object = Game::Lua::ResolveObject(L, idx);
|
||||
if (object == nullptr || IsOurs(object) || !Game::Lua::IsA(object, Offsets::VAR_REGION_LUA_TYPE_ID))
|
||||
return nullptr;
|
||||
return object;
|
||||
}
|
||||
|
||||
const char *StringArg(void *L, Anim *a, const char *usage) {
|
||||
if (Game::Lua::IsString(L, 2))
|
||||
return Game::Lua::ToString(L, 2);
|
||||
Game::Lua::Error(L, usage, a->DisplayName());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetTarget(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
void *region = RegionArg(L, 2);
|
||||
if (region == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
a->targetName[0] = '\0';
|
||||
a->targetKey.clear();
|
||||
a->target = region;
|
||||
return PushTargetFound(L, a);
|
||||
}
|
||||
int __fastcall Script_SetTargetName(void *L) {
|
||||
Anim *a = Self(L);
|
||||
const char *name = a != nullptr ? StringArg(L, a, "Usage: %s:SetTargetName(name)") : nullptr;
|
||||
if (name == nullptr)
|
||||
return 0;
|
||||
std::snprintf(a->targetName, sizeof(a->targetName), "%s", CreateName(name, a).c_str());
|
||||
a->targetKey.clear();
|
||||
a->target = nullptr;
|
||||
a->ResolveTarget();
|
||||
return PushTargetFound(L, a);
|
||||
}
|
||||
int __fastcall Script_SetTargetKey(void *L) {
|
||||
Anim *a = Self(L);
|
||||
const char *key = a != nullptr ? StringArg(L, a, "Usage: %s:SetTargetKey(key)") : nullptr;
|
||||
if (key == nullptr)
|
||||
return 0;
|
||||
a->targetName[0] = '\0';
|
||||
a->targetKey = key;
|
||||
a->target = nullptr;
|
||||
a->ResolveTarget();
|
||||
return PushTargetFound(L, a);
|
||||
}
|
||||
int __fastcall Script_SetChildKey(void *L) {
|
||||
Anim *a = Self(L);
|
||||
const char *key = a != nullptr ? StringArg(L, a, "Usage: %s:SetChildKey(childKey)") : nullptr;
|
||||
if (key == nullptr)
|
||||
return 0;
|
||||
a->targetName[0] = '\0';
|
||||
a->targetKey = std::string("$parent.$parent.") + key;
|
||||
a->target = nullptr;
|
||||
a->ResolveTarget();
|
||||
return PushTargetFound(L, a);
|
||||
}
|
||||
int __fastcall Script_SetTargetParent(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
a->targetName[0] = '\0';
|
||||
a->targetKey.clear();
|
||||
a->target = a->GetRegionParent();
|
||||
return PushTargetFound(L, a);
|
||||
}
|
||||
int __fastcall Script_GetTarget(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
void *region = a->target != nullptr ? a->target : a->GetRegionParent();
|
||||
if (region != nullptr)
|
||||
Game::Lua::PushObject(L, region);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- Alpha ----
|
||||
|
||||
int __fastcall Script_SetChange(void *L) {
|
||||
@@ -483,6 +604,7 @@ int __fastcall Script_SetChange(void *L) {
|
||||
const float change = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
const float value = change < -1.0f ? -1.0f : (change >= 1.0f ? 1.0f : change);
|
||||
a->change = static_cast<int16_t>(value * 255.0f);
|
||||
a->alphaFromTo = false;
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetChange(void *L) {
|
||||
@@ -493,6 +615,39 @@ int __fastcall Script_GetChange(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Later clients' absolute alpha. Setting either end turns it on; the other
|
||||
// end keeps its value, 0 until set.
|
||||
void SetAlphaEnd(void *L, float Anim::*end, const char *usage) {
|
||||
Anim *a = SelfOf(L, Anim::s_alphaClass);
|
||||
if (a == nullptr || !NumberArg(L, 2, a, usage))
|
||||
return;
|
||||
const float value = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
a->*end = value < 0.0f ? 0.0f : (value > 1.0f ? 1.0f : value);
|
||||
a->alphaFromTo = true;
|
||||
}
|
||||
int __fastcall Script_SetFromAlpha(void *L) {
|
||||
SetAlphaEnd(L, &Anim::fromAlpha, "Usage: %s:SetFromAlpha(alpha)");
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetToAlpha(void *L) {
|
||||
SetAlphaEnd(L, &Anim::toAlpha, "Usage: %s:SetToAlpha(alpha)");
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetFromAlpha(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_alphaClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->fromAlpha);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_GetToAlpha(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_alphaClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->toAlpha);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- Translation ----
|
||||
|
||||
int __fastcall Script_SetOffset(void *L) {
|
||||
@@ -572,19 +727,57 @@ int __fastcall Script_SetScale(void *L) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetScale(x, y)", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
// CSimpleScaleAnim::SetScale.
|
||||
// CSimpleScaleAnim::SetScale: the scale it ends on.
|
||||
const float x = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
const float y = static_cast<float>(Game::Lua::ToNumber(L, 3));
|
||||
a->scaleChangeX = 1.0f - (x >= 0.001f ? x : 0.001f);
|
||||
a->scaleChangeY = 1.0f - (y >= 0.001f ? y : 0.001f);
|
||||
a->scaleToX = x >= 0.001f ? x : 0.001f;
|
||||
a->scaleToY = y >= 0.001f ? y : 0.001f;
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetScale(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, 1.0f - a->scaleChangeX);
|
||||
Game::Lua::PushNumber(L, 1.0f - a->scaleChangeY);
|
||||
Game::Lua::PushNumber(L, a->scaleToX);
|
||||
Game::Lua::PushNumber(L, a->scaleToY);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Later clients' SetScaleFrom / SetScaleTo: the scale the animation starts
|
||||
// and ends on, as given.
|
||||
void SetScaleEnd(void *L, float Anim::*x, float Anim::*y, const char *usage) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return;
|
||||
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
|
||||
Game::Lua::Error(L, usage, a->DisplayName());
|
||||
return;
|
||||
}
|
||||
a->*x = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
a->*y = static_cast<float>(Game::Lua::ToNumber(L, 3));
|
||||
}
|
||||
int __fastcall Script_SetScaleFrom(void *L) {
|
||||
SetScaleEnd(L, &Anim::scaleFromX, &Anim::scaleFromY, "Usage: %s:SetScaleFrom(x, y)");
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetScaleTo(void *L) {
|
||||
SetScaleEnd(L, &Anim::scaleToX, &Anim::scaleToY, "Usage: %s:SetScaleTo(x, y)");
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetScaleFrom(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->scaleFromX);
|
||||
Game::Lua::PushNumber(L, a->scaleFromY);
|
||||
return 2;
|
||||
}
|
||||
int __fastcall Script_GetScaleTo(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->scaleToX);
|
||||
Game::Lua::PushNumber(L, a->scaleToY);
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -647,24 +840,31 @@ const Game::Lua::FrameMethodEntry kAnimMethods[] = {
|
||||
{"GetSmoothing", &Script_GetSmoothing},
|
||||
{"SetParent", &Script_SetParent},
|
||||
{"GetRegionParent", &Script_GetRegionParent},
|
||||
{"SetTarget", &Script_SetTarget},
|
||||
{"SetTargetName", &Script_SetTargetName},
|
||||
{"SetTargetKey", &Script_SetTargetKey},
|
||||
{"SetChildKey", &Script_SetChildKey},
|
||||
{"SetTargetParent", &Script_SetTargetParent},
|
||||
{"GetTarget", &Script_GetTarget},
|
||||
{"HasScript", &Script_HasScript},
|
||||
{"GetScript", &Script_GetScript},
|
||||
{"SetScript", &Script_SetScript},
|
||||
{"HookScript", &Script_HookScript},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kAlphaMethods[] = {
|
||||
{"SetChange", &Script_SetChange},
|
||||
{"GetChange", &Script_GetChange},
|
||||
{"SetChange", &Script_SetChange}, {"GetChange", &Script_GetChange},
|
||||
{"SetFromAlpha", &Script_SetFromAlpha}, {"GetFromAlpha", &Script_GetFromAlpha},
|
||||
{"SetToAlpha", &Script_SetToAlpha}, {"GetToAlpha", &Script_GetToAlpha},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kTranslationMethods[] = {
|
||||
{"SetOffset", &Script_SetOffset},
|
||||
{"GetOffset", &Script_GetOffset},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kScaleMethods[] = {
|
||||
{"SetOrigin", &Script_SetOrigin},
|
||||
{"GetOrigin", &Script_GetOrigin},
|
||||
{"SetScale", &Script_SetScale},
|
||||
{"GetScale", &Script_GetScale},
|
||||
{"SetOrigin", &Script_SetOrigin}, {"GetOrigin", &Script_GetOrigin},
|
||||
{"SetScale", &Script_SetScale}, {"GetScale", &Script_GetScale},
|
||||
{"SetScaleFrom", &Script_SetScaleFrom}, {"GetScaleFrom", &Script_GetScaleFrom},
|
||||
{"SetScaleTo", &Script_SetScaleTo}, {"GetScaleTo", &Script_GetScaleTo},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kRotationMethods[] = {
|
||||
{"SetOrigin", &Script_SetOrigin}, {"GetOrigin", &Script_GetOrigin}, {"SetDegrees", &Script_SetDegrees},
|
||||
|
||||
+21
-3
@@ -16,6 +16,7 @@
|
||||
#include "animation/Object.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// 4.3.4's CSimpleAnimGroup and CSimpleAnim (and its Alpha, Translation,
|
||||
@@ -62,6 +63,10 @@ class Group : public Object {
|
||||
bool HasAlphaAnim() const;
|
||||
bool HasPositionAnim() const;
|
||||
|
||||
// Later clients' setToFinalAlpha: when the group finishes, its alpha
|
||||
// animations' targets keep the alpha they end on (KeepAnimAlpha).
|
||||
bool setToFinalAlpha = false;
|
||||
|
||||
void NotifyAnimAdded(Anim *anim);
|
||||
void NotifyOrderChanged(Anim *anim, int oldOrder);
|
||||
void NotifyAnimRemoved(Anim *anim);
|
||||
@@ -141,17 +146,30 @@ class Anim : public Object {
|
||||
uint8_t flags = 0;
|
||||
int playState = 0; // 0 stopped, 1 playing, 2 paused
|
||||
int loopState = STATE_NONE;
|
||||
void *target = nullptr; // m_target, resolved from targetName on Play
|
||||
void *target = nullptr; // m_target, resolved from targetName or targetKey on Play
|
||||
char targetName[128] = {}; // m_targetName
|
||||
// Later clients' targetKey: a key path from the animation itself, where
|
||||
// `$parent` steps to the group, then the group's region, then that
|
||||
// region's parent. childKey "Icon" is "$parent.$parent.Icon".
|
||||
std::string targetKey;
|
||||
bool hasSmoother = false;
|
||||
float smoothIn = 0.0f, smoothOut = 0.0f;
|
||||
|
||||
// Alpha: m_change, -255..255.
|
||||
// Resolves targetName or targetKey into target, when either is set.
|
||||
void ResolveTarget();
|
||||
|
||||
// Alpha: m_change, -255..255, or (alphaFromTo) the absolute fromAlpha /
|
||||
// toAlpha, 0..1, that later clients replaced it with.
|
||||
int16_t change = 0;
|
||||
bool alphaFromTo = false;
|
||||
float fromAlpha = 0.0f, toAlpha = 0.0f;
|
||||
// Translation: the offset; Scale: the scale and origin; Rotation: the
|
||||
// angle and origin. Offsets in layout units, as 4.3.4 keeps them.
|
||||
float offsetX = 0.0f, offsetY = 0.0f;
|
||||
float scaleChangeX = 0.0f, scaleChangeY = 0.0f; // m_scaleChange: 1 - scale
|
||||
// Scale: from and to, 1 by default. 4.3.4's m_scaleChange (1 - scale)
|
||||
// is the case from = 1; later clients added the from scale.
|
||||
float scaleFromX = 1.0f, scaleFromY = 1.0f;
|
||||
float scaleToX = 1.0f, scaleToY = 1.0f;
|
||||
float radians = 0.0f;
|
||||
int originPoint = 4; // FRAMEPOINT_CENTER
|
||||
float originX = 0.0f, originY = 0.0f;
|
||||
|
||||
+24
-1
@@ -12,7 +12,10 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 4.3.4's CSimpleAnimGroup (wowdecomp15595 CSimpleAnimGroup.cpp and
|
||||
// CSimpleAnimGroup_Scripts.cpp), on Animation::Object.
|
||||
// CSimpleAnimGroup_Scripts.cpp), on Animation::Object, with later clients'
|
||||
// SetToFinalAlpha / IsSetToFinalAlpha: the final alpha is kept when the
|
||||
// group finishes (Finish included), and Stop still restores the alpha. No
|
||||
// source on disk says what a later client's Stop does with the flag.
|
||||
|
||||
#include "animation/Classes.h"
|
||||
#include "animation/Region.h"
|
||||
@@ -343,6 +346,12 @@ bool Group::OnUpdate(float elapsedSec) {
|
||||
RunScript(G_ONUPDATE, 1);
|
||||
}
|
||||
if (done) {
|
||||
// setToFinalAlpha: the alpha each alpha animation's target finished on
|
||||
// becomes what the end restores. Stop restores the alpha as before.
|
||||
if (setToFinalAlpha)
|
||||
for (Anim *anim : animations)
|
||||
if (anim->kind == KIND_ALPHA)
|
||||
Region::KeepAnimAlpha(RegionObject(), anim->target);
|
||||
Region::NotifyEnd(RegionObject(), this);
|
||||
const bool requested = (flags & GROUP_PENDING_FINISH) != 0;
|
||||
loopState = STATE_NONE;
|
||||
@@ -632,6 +641,18 @@ int __fastcall Script_GetIgnoreFramerateThrottle(void *L) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_SetToFinalAlpha(void *L) {
|
||||
if (Group *g = Self(L))
|
||||
g->setToFinalAlpha = Game::Lua::ToFlag(L, 2);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_IsSetToFinalAlpha(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, g->setToFinalAlpha);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kMethods[] = {
|
||||
{"GetObjectType", &Script_GetObjectType},
|
||||
@@ -662,6 +683,8 @@ const Game::Lua::FrameMethodEntry kMethods[] = {
|
||||
{"HookScript", &Script_HookScript},
|
||||
{"SetIgnoreFramerateThrottle", &Script_SetIgnoreFramerateThrottle},
|
||||
{"GetIgnoreFramerateThrottle", &Script_GetIgnoreFramerateThrottle},
|
||||
{"SetToFinalAlpha", &Script_SetToFinalAlpha},
|
||||
{"IsSetToFinalAlpha", &Script_IsSetToFinalAlpha},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -64,10 +64,12 @@
|
||||
#include "frame/Alpha.h"
|
||||
#include "frame/Destruction.h"
|
||||
#include "texture/Transform.h"
|
||||
#include "xml/Anchors.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
@@ -162,8 +164,20 @@ uint32_t VertexColor(void *region) {
|
||||
return bgra;
|
||||
}
|
||||
|
||||
// An unchanged alpha writes nothing. The setter already does nothing when the
|
||||
// one entry it would store is the region's colour, but the getter reads a
|
||||
// gradient back as white, and the setter shrinks a gradient's four entries to
|
||||
// that one (benilla-re ui/texture, "Setting a vertex colour"). So restoring
|
||||
// the stored alpha (PreOnAnimUpdate, AnimDeactivated) on a gradient under an
|
||||
// ancestor's alpha group flattened it to white. 3.3.5's and 4.3.4's
|
||||
// SetVertexColor drop the gradient there too; here only a region that an
|
||||
// animation itself fades loses it. The ancestor's fade still reaches the
|
||||
// gradient: a frame's alpha change refolds every region's entries (0x77FAC0).
|
||||
void SetVertexAlpha(void *region, uint8_t alpha) {
|
||||
const uint32_t bgra = (VertexColor(region) & 0x00FFFFFF) | (static_cast<uint32_t>(alpha) << 24);
|
||||
const uint32_t color = VertexColor(region);
|
||||
if ((color >> 24) == alpha)
|
||||
return;
|
||||
const uint32_t bgra = (color & 0x00FFFFFF) | (static_cast<uint32_t>(alpha) << 24);
|
||||
reinterpret_cast<SetColor_t>(Offsets::FUN_FONTSTRING_SET_COLOR)(region, nullptr, &bgra);
|
||||
}
|
||||
|
||||
@@ -671,6 +685,28 @@ void AddAnimAlpha(void *region, void *target, int16_t alpha) {
|
||||
SetVertexAlpha(region, static_cast<uint8_t>(value > 255 ? 255 : (value < 0 ? 0 : value)));
|
||||
}
|
||||
|
||||
void SetAnimAlpha(void *region, void *target, uint8_t alpha) {
|
||||
if (IsFrame(region)) {
|
||||
if (target != nullptr && target != region) {
|
||||
ForEachChild(region, [target, alpha](void *child) { SetAnimAlpha(child, target, alpha); });
|
||||
return;
|
||||
}
|
||||
if (alpha != OwnAlpha(region))
|
||||
SetOwnAlpha(region, alpha);
|
||||
return;
|
||||
}
|
||||
if (target != nullptr && target != region)
|
||||
return;
|
||||
SetVertexAlpha(region, alpha);
|
||||
}
|
||||
|
||||
void KeepAnimAlpha(void *region, void *target) {
|
||||
void *shown = target != nullptr ? target : region;
|
||||
AnimData *data = DataOf(shown);
|
||||
if (data != nullptr && data->hasAlpha)
|
||||
data->alpha = IsFrame(shown) ? OwnAlpha(shown) : static_cast<uint8_t>(VertexColor(shown) >> 24);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// The corners (TL, BL, TR, BR) as 2D points.
|
||||
@@ -830,4 +866,29 @@ void *FindRegionByName(const char *name) {
|
||||
return object;
|
||||
}
|
||||
|
||||
// The leading `$parent` steps that stay among our objects (an animation to
|
||||
// its group, a group to its region) are taken here; the rest of the path,
|
||||
// from that region, is the one Xml::Anchors resolves for relativeKey.
|
||||
void *FindRegionByKey(Object *from, const char *path) {
|
||||
if (from == nullptr || path == nullptr)
|
||||
return nullptr;
|
||||
void *current = from;
|
||||
const char *p = path;
|
||||
while (current != nullptr && IsOurs(current)) {
|
||||
const char *dot = std::strchr(p, '.');
|
||||
const std::string part = dot != nullptr ? std::string(p, dot) : std::string(p);
|
||||
if (Game::StrCmpI(part.c_str(), "$parent") != 0)
|
||||
return nullptr; // a key on a group or animation names no region
|
||||
current = static_cast<Object *>(current)->parent;
|
||||
p = dot != nullptr ? dot + 1 : p + part.size();
|
||||
}
|
||||
if (current == nullptr)
|
||||
return nullptr;
|
||||
if (*p != '\0')
|
||||
current = ::Xml::Anchors::ResolveKeyPath(current, p, false);
|
||||
if (current == nullptr || IsOurs(current) || !Game::Lua::IsA(current, Offsets::VAR_REGION_LUA_TYPE_ID))
|
||||
return nullptr;
|
||||
return current;
|
||||
}
|
||||
|
||||
} // namespace Animation::Region
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
// frames, textures and font strings, which have no room for them.
|
||||
namespace Animation {
|
||||
class Group;
|
||||
class Object;
|
||||
}
|
||||
|
||||
namespace Animation::Region {
|
||||
@@ -35,6 +36,12 @@ void NotifyEnd(void *region, Group *group);
|
||||
void PreOnAnimUpdate(void *region);
|
||||
// CScriptRegion::AddAnimAlpha(target, alpha) on the group's region.
|
||||
void AddAnimAlpha(void *region, void *target, int16_t alpha);
|
||||
// Later clients' absolute alpha (Alpha fromAlpha / toAlpha): the same target
|
||||
// rule as AddAnimAlpha, but the alpha becomes `alpha` instead of growing.
|
||||
void SetAnimAlpha(void *region, void *target, uint8_t alpha);
|
||||
// AnimationGroup setToFinalAlpha: the alpha that `target` (the group's
|
||||
// `region` when null) shows now becomes the one the group's end restores.
|
||||
void KeepAnimAlpha(void *region, void *target);
|
||||
// CScriptRegion::AddAnimTranslation(target, translation), in layout units:
|
||||
// moves the drawn textures and font strings, never a rect.
|
||||
void AddAnimTranslation(void *region, void *target, float x, float y);
|
||||
@@ -58,5 +65,9 @@ bool StringAnimated(void *fontString);
|
||||
// CScriptObject::GetScriptObjectByName for a region (Animation:Play's
|
||||
// target lookup): the region named `name`, or nullptr.
|
||||
void *FindRegionByName(const char *name);
|
||||
// The region a later-client targetKey path names, read from `from`: each
|
||||
// dot-separated part is `$parent` (the object's parent) or a key on the
|
||||
// current object's table. Nullptr unless the path ends on a region.
|
||||
void *FindRegionByKey(Object *from, const char *path);
|
||||
|
||||
} // namespace Animation::Region
|
||||
|
||||
+52
-2
@@ -16,6 +16,11 @@
|
||||
// LoadXML_AnimOrigin; the attributes BinaryXML.cpp parses), on 1.12 nodes.
|
||||
// A script body compiles through the engine's own SetScriptObjectScript, so
|
||||
// it is named "<object>:<script>" and handled like a frame's.
|
||||
//
|
||||
// Later clients' attributes go to the same fields their methods set:
|
||||
// setToFinalAlpha and parentArray on a group; targetKey and childKey on any animation;
|
||||
// fromAlpha / toAlpha on Alpha; fromScaleX / fromScaleY / toScaleX /
|
||||
// toScaleY on Scale. Each applies only when present and non-empty.
|
||||
|
||||
#include "animation/Xml.h"
|
||||
|
||||
@@ -70,6 +75,22 @@ void BindParentKey(void *object, Object *child, const char *key) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
// Later clients' parentArray on a group: appended to object[key], as a
|
||||
// frame or region is.
|
||||
void BindParentArray(void *object, Object *child, const char *key) {
|
||||
if (key == nullptr || *key == '\0')
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (::Xml::PushWrapper(L, object)) {
|
||||
Push(L, child);
|
||||
::Xml::AppendToArray(L, top + 1, key);
|
||||
}
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
// <Scripts>: each element's body, its `function` global, or its `method`
|
||||
// field on the object.
|
||||
void LoadScripts(Object *object, const void *node) {
|
||||
@@ -205,6 +226,7 @@ void LoadGroupNode(Group *group, const void *node, void *status) {
|
||||
LoadGroupNode(group, base, status);
|
||||
}
|
||||
BindParentKey(group->RegionObject(), group, ::Xml::Attr(node, "parentKey"));
|
||||
BindParentArray(group->RegionObject(), group, ::Xml::Attr(node, "parentArray"));
|
||||
if (const char *looping = ::Xml::Attr(node, "looping"); looping != nullptr && *looping != '\0') {
|
||||
LoopType type;
|
||||
if (StringToLoopType(looping, type))
|
||||
@@ -212,6 +234,8 @@ void LoadGroupNode(Group *group, const void *node, void *status) {
|
||||
else
|
||||
::Xml::Warn(status, "AnimationGroup %s: Invalid looping value: %s", group->DisplayName(), looping);
|
||||
}
|
||||
if (const char *keep = ::Xml::Attr(node, "setToFinalAlpha"); keep != nullptr && *keep != '\0')
|
||||
group->setToFinalAlpha = Game::StringToFlag(keep);
|
||||
float x = 0.0f, y = 0.0f;
|
||||
FloatAttr(node, "initialOffsetX", x);
|
||||
FloatAttr(node, "initialOffsetY", y);
|
||||
@@ -262,8 +286,13 @@ void LoadAnim(Anim *anim, const void *node, void *status) {
|
||||
if (FloatAttr(node, "scaleY", sy) && sy < 0.001f)
|
||||
::Xml::Warn(status, "%s: Invalid scaleY value: %s. Value must be at least 0.001.", anim->DisplayName(),
|
||||
::Xml::Attr(node, "scaleY"));
|
||||
anim->scaleChangeX = 1.0f - (sx >= 0.001f ? sx : 0.001f);
|
||||
anim->scaleChangeY = 1.0f - (sy >= 0.001f ? sy : 0.001f);
|
||||
anim->scaleToX = sx >= 0.001f ? sx : 0.001f;
|
||||
anim->scaleToY = sy >= 0.001f ? sy : 0.001f;
|
||||
// Later clients' from and to scale, each axis when given.
|
||||
FloatAttr(node, "fromScaleX", anim->scaleFromX);
|
||||
FloatAttr(node, "fromScaleY", anim->scaleFromY);
|
||||
FloatAttr(node, "toScaleX", anim->scaleToX);
|
||||
FloatAttr(node, "toScaleY", anim->scaleToY);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -285,6 +314,16 @@ void LoadAnim(Anim *anim, const void *node, void *status) {
|
||||
if (name.size() + 1 > sizeof(anim->targetName))
|
||||
::Xml::Warn(status, "target key too long: %s", name.c_str());
|
||||
std::snprintf(anim->targetName, sizeof(anim->targetName), "%s", name.c_str());
|
||||
anim->targetKey.clear();
|
||||
}
|
||||
// Later clients' key paths, looked up on Play like `target`.
|
||||
if (const char *key = ::Xml::Attr(node, "targetKey"); key != nullptr && *key != '\0') {
|
||||
anim->targetName[0] = '\0';
|
||||
anim->targetKey = key;
|
||||
}
|
||||
if (const char *key = ::Xml::Attr(node, "childKey"); key != nullptr && *key != '\0') {
|
||||
anim->targetName[0] = '\0';
|
||||
anim->targetKey = std::string("$parent.$parent.") + key;
|
||||
}
|
||||
if (FloatAttr(node, "startDelay", x))
|
||||
anim->startDelay = x < 0.0f ? 0.0f : x;
|
||||
@@ -319,6 +358,17 @@ void LoadAnim(Anim *anim, const void *node, void *status) {
|
||||
x = 0.0f;
|
||||
}
|
||||
anim->change = static_cast<int16_t>((x < -1.0f ? -1.0f : (x >= 1.0f ? 1.0f : x)) * 255.0f);
|
||||
if (::Xml::Attr(node, "change") != nullptr)
|
||||
anim->alphaFromTo = false;
|
||||
// Later clients' absolute alpha: either end turns it on.
|
||||
if (FloatAttr(node, "fromAlpha", x)) {
|
||||
anim->fromAlpha = x < 0.0f ? 0.0f : (x > 1.0f ? 1.0f : x);
|
||||
anim->alphaFromTo = true;
|
||||
}
|
||||
if (FloatAttr(node, "toAlpha", x)) {
|
||||
anim->toAlpha = x < 0.0f ? 0.0f : (x > 1.0f ? 1.0f : x);
|
||||
anim->alphaFromTo = true;
|
||||
}
|
||||
}
|
||||
if (anim->kind == KIND_SCALE || anim->kind == KIND_ROTATION)
|
||||
LoadOrigin(anim, node);
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
// listed by the coverage function
|
||||
// - a frame method -> Blizzard's widget system for its registry
|
||||
// ("SimpleTextureAPI", Type "ScriptObject")
|
||||
// - an enum / structure /
|
||||
// event -> the System named in its declaration
|
||||
// - an enum / constants /
|
||||
// structure / event -> the System named in its declaration
|
||||
// A system's `Environment` is "All" when its functions were recorded in both
|
||||
// Lua states, else "Game" or "Glue"; a function whose own environment is
|
||||
// narrower than its system's gets a Documentation line saying so.
|
||||
@@ -79,13 +79,20 @@ enum Kind : uint8_t {
|
||||
KIND_TABLE, // RegisterTableFunction
|
||||
KIND_FRAME_METHOD, // RegisterFrameMethods, one per entry
|
||||
KIND_ENUM, // RegisterIntegerEnum
|
||||
KIND_CONSTANTS, // RegisterConstants
|
||||
KIND_ORPHAN_METHOD_DOC // a Doc::Method naming no entry in its table
|
||||
};
|
||||
|
||||
// The value-table kinds, which share the enum* fields of Entry.
|
||||
bool IsValueTable(Kind kind) {
|
||||
return kind == KIND_ENUM || kind == KIND_CONSTANTS;
|
||||
}
|
||||
|
||||
struct Entry {
|
||||
Kind kind;
|
||||
unsigned env; // Env bits the registration was recorded in
|
||||
const char *table; // KIND_TABLE: the namespace; KIND_ENUM: the parent ("Enum")
|
||||
const char *table; // KIND_TABLE: the namespace; KIND_ENUM: the parent ("Enum");
|
||||
// KIND_CONSTANTS: "Constants"
|
||||
const char *name; // registered name / method name / enum sub-name
|
||||
const void *registry; // frame methods only
|
||||
const Function *doc; // nullptr = undocumented
|
||||
@@ -195,6 +202,7 @@ const char *SystemOf(const Entry &e) {
|
||||
case KIND_FRAME_METHOD:
|
||||
return WidgetSystemFor(e.registry);
|
||||
case KIND_ENUM:
|
||||
case KIND_CONSTANTS:
|
||||
return e.enumSystem;
|
||||
default:
|
||||
return nullptr;
|
||||
@@ -240,6 +248,7 @@ std::vector<SystemInfo> CollectSystems() {
|
||||
AddSystem(systems, SystemOf(e), nullptr, "ScriptObject", e.env);
|
||||
break;
|
||||
case KIND_ENUM:
|
||||
case KIND_CONSTANTS:
|
||||
AddSystem(systems, e.enumSystem, nullptr, nullptr, 0);
|
||||
break;
|
||||
default:
|
||||
@@ -394,6 +403,25 @@ void PushEnumeration(void *L, const Entry &e) {
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Constants",
|
||||
// Values = { { Name, Type = "number", Value } } }.
|
||||
void PushConstants(void *L, const Entry &e) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.name);
|
||||
Lua::SetFieldString(L, "Type", "Constants");
|
||||
Lua::PushString(L, "Values");
|
||||
Lua::NewTable(L);
|
||||
for (int i = 0; i < e.enumCount; ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.enumEntries[i].key);
|
||||
Lua::SetFieldString(L, "Type", "number");
|
||||
Lua::SetFieldNumber(L, "Value", e.enumEntries[i].value);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Structure" [, Documentation], Fields = { … } }.
|
||||
void PushStructure(void *L, const Structure &s) {
|
||||
Lua::NewTable(L);
|
||||
@@ -479,10 +507,13 @@ void PushSystem(void *L, const SystemInfo &sys) {
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
for (const Entry &e : g_entries) {
|
||||
if (e.kind != KIND_ENUM || !SameTextNoCase(e.enumSystem, sys.name))
|
||||
if (!IsValueTable(e.kind) || !SameTextNoCase(e.enumSystem, sys.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushEnumeration(L, e);
|
||||
if (e.kind == KIND_ENUM)
|
||||
PushEnumeration(L, e);
|
||||
else
|
||||
PushConstants(L, e);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
@@ -527,6 +558,7 @@ std::vector<std::string> Undocumented() {
|
||||
break;
|
||||
}
|
||||
case KIND_ENUM:
|
||||
case KIND_CONSTANTS:
|
||||
if (e.enumSystem == nullptr)
|
||||
out.push_back(std::string(e.table) + "." + e.name);
|
||||
break;
|
||||
@@ -662,18 +694,21 @@ void NameWidgetSystem(const void *registry, const char *system) {
|
||||
g_ownWidgetSystems.push_back({reinterpret_cast<uintptr_t>(registry), system});
|
||||
}
|
||||
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system) {
|
||||
namespace {
|
||||
|
||||
void RecordValueTable(Kind kind, const char *parent, const char *sub,
|
||||
const Game::Lua::EnumIntegerEntry *entries, int count,
|
||||
const char *system) {
|
||||
if (g_pass == ENV_NONE || sub == nullptr)
|
||||
return;
|
||||
if (Entry *e = Find(KIND_ENUM, parent, sub, nullptr)) {
|
||||
if (Entry *e = Find(kind, parent, sub, nullptr)) {
|
||||
e->env |= g_pass;
|
||||
if (e->enumSystem == nullptr)
|
||||
e->enumSystem = system;
|
||||
return;
|
||||
}
|
||||
Entry e{};
|
||||
e.kind = KIND_ENUM;
|
||||
e.kind = kind;
|
||||
e.env = g_pass;
|
||||
e.table = parent;
|
||||
e.name = sub;
|
||||
@@ -683,6 +718,18 @@ void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntege
|
||||
g_entries.push_back(e);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system) {
|
||||
RecordValueTable(KIND_ENUM, parent, sub, entries, count, system);
|
||||
}
|
||||
|
||||
void RecordConstants(const char *sub, const Game::Lua::EnumIntegerEntry *entries, int count,
|
||||
const char *system) {
|
||||
RecordValueTable(KIND_CONSTANTS, "Constants", sub, entries, count, system);
|
||||
}
|
||||
|
||||
} // namespace Api::Documentation
|
||||
|
||||
// The Structure constructor lives here so the list head is private to this
|
||||
|
||||
@@ -39,5 +39,7 @@ void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry
|
||||
void NameWidgetSystem(const void *registry, const char *system);
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system);
|
||||
void RecordConstants(const char *sub, const Game::Lua::EnumIntegerEntry *entries, int count,
|
||||
const char *system);
|
||||
|
||||
} // namespace Api::Documentation
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
// 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_AutoComplete: the player names an edit box can complete.
|
||||
//
|
||||
// 4.3.4's CGAutoCompleteName keeps its own list of names, each with flag bits
|
||||
// that the party, raid, guild, friend list and chat code add and remove as
|
||||
// those lists change. 1.12 has no such list, but it keeps every list the bits
|
||||
// come from, so a query reads them where the engine keeps them:
|
||||
// - party: the four VAR_PARTY_GUIDS, the name from the name cache, online
|
||||
// from the member stats block (Group::MemberStats) - InGroup;
|
||||
// - raid: the VAR_RAID_MEMBER_PTRS slots and their online bit - InGroup;
|
||||
// - guild roster: each GuildMemberInfo's GUID, name and online word, read
|
||||
// only while the player is in a guild, the test Script_IsInGuild
|
||||
// (0x00516DE0) makes, because the engine keeps the old roster after a
|
||||
// /gquit - InGuild;
|
||||
// - friends list: the social singleton's slots (FriendList.h) - Friend.
|
||||
// The one list the engine does not keep is ours: the players this character
|
||||
// whispered or was whispered by (Chat::Dispatch reports each line), in the
|
||||
// order they were last heard from - InteractedWith. Logging out clears it,
|
||||
// as 4.3.4's ShutdownGame clears its list.
|
||||
//
|
||||
// A player on several lists is one entry with every list's bit. A list that
|
||||
// holds the player decides Online; a player known only from a whisper is
|
||||
// Online, as 4.3.4 sets Online together with InteractedWith.
|
||||
//
|
||||
// Matching is 4.3.4's NodeMatchesParams (the same in 3.3.5's binary,
|
||||
// 0x0057ACA0), with two differences:
|
||||
// - a cursorPosition outside 1..length means the end of the text. Retail's
|
||||
// tell-target extraction (ChatFrameEditBox.lua) passes 0 and expects a
|
||||
// whole-text match;
|
||||
// - the edit-from-center search also tries the last place. The engine's
|
||||
// loop stops one place early, so it never finds the text after the
|
||||
// cursor at the end of a name.
|
||||
// Order is 3.3.5's SortNameNode (0x0057B130): with
|
||||
// autoCompleteResortNamesOnRecency on, the most recent whisper first, then by
|
||||
// name. Retail's client cannot be read, so each result's priority is the
|
||||
// highest Enum.AutoCompletePriority its bits reach, in the enum's own order:
|
||||
// Friend, Guild, InGroup, Interacted, else Other.
|
||||
|
||||
#include "autocomplete/Names.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "friendlist/FriendList.h"
|
||||
#include "group/MemberStats.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "text/Utf8.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace AutoComplete::Names {
|
||||
|
||||
namespace {
|
||||
|
||||
// Enum.AutoCompleteEntryFlag. 1.12 has nothing to fill Bnet, InAOI,
|
||||
// AccountCharacter or RecentPlayer from.
|
||||
enum EntryFlag : uint32_t {
|
||||
FLAG_IN_GROUP = 0x1,
|
||||
FLAG_IN_GUILD = 0x2,
|
||||
FLAG_FRIEND = 0x4,
|
||||
FLAG_BNET = 0x8,
|
||||
FLAG_INTERACTED_WITH = 0x10,
|
||||
FLAG_ONLINE = 0x20,
|
||||
FLAG_IN_AOI = 0x40,
|
||||
FLAG_ACCOUNT_CHARACTER = 0x80,
|
||||
FLAG_RECENT_PLAYER = 0x100,
|
||||
};
|
||||
|
||||
// Enum.AutoCompletePriority.
|
||||
enum Priority {
|
||||
PRIORITY_OTHER = 0,
|
||||
PRIORITY_INTERACTED = 1,
|
||||
PRIORITY_IN_GROUP = 2,
|
||||
PRIORITY_GUILD = 3,
|
||||
PRIORITY_FRIEND = 4,
|
||||
PRIORITY_ACCOUNT_CHARACTER = 5,
|
||||
PRIORITY_ACCOUNT_CHARACTER_SAME_REALM = 6,
|
||||
};
|
||||
|
||||
constexpr int CHAT_MSG_WHISPER = 6;
|
||||
constexpr int CHAT_MSG_WHISPER_INFORM = 7;
|
||||
constexpr int LANGUAGE_ADDON = -1; // the dispatch's addon-message arm
|
||||
|
||||
CVar::Factory::Handle g_resortOnRecency = nullptr;
|
||||
CVar::Factory::Handle g_editFromCenter = nullptr;
|
||||
CVar::Factory::Handle g_useContext = nullptr;
|
||||
|
||||
bool CVarOn(CVar::Factory::Handle cvar) { return CVar::Factory::GetInt(cvar, 1) != 0; }
|
||||
|
||||
// A whisper partner. `order` grows with every whisper, so a larger one was
|
||||
// heard from more recently.
|
||||
struct Interaction {
|
||||
uint64_t guid;
|
||||
std::string name;
|
||||
uint32_t order;
|
||||
};
|
||||
|
||||
std::vector<Interaction> g_interactions;
|
||||
uint32_t g_lastOrder = 0;
|
||||
|
||||
// One player as one list knows them, or, after Merge, as all lists do.
|
||||
struct Entry {
|
||||
uint64_t guid;
|
||||
const char *name; // the list's own storage, valid until Lua runs
|
||||
uint32_t flags; // EntryFlag bits, Online excluded
|
||||
bool listed; // a group, guild or friends list holds the player
|
||||
bool online; // what those lists say
|
||||
uint32_t order; // the Interaction's order, 0 for none
|
||||
};
|
||||
|
||||
bool InGuild() {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
const auto *info = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO);
|
||||
return info != nullptr && Game::Read<uint32_t>(info, Offsets::OFF_PLAYER_INFO_GUILD_KEY) != 0;
|
||||
}
|
||||
|
||||
// Every player whose name `match` accepts, one entry per list that has them.
|
||||
template <typename Match>
|
||||
std::vector<Entry> Collect(Match match) {
|
||||
std::vector<Entry> found;
|
||||
const uint64_t self = Unit::Identity::PlayerGuid();
|
||||
const auto add = [&](uint64_t guid, const char *name, uint32_t flag, bool listed, bool online,
|
||||
uint32_t order) {
|
||||
if (guid == 0 || guid == self || name == nullptr || *name == '\0' || !match(name))
|
||||
return;
|
||||
found.push_back({guid, name, flag, listed, online, order});
|
||||
};
|
||||
|
||||
const auto *party =
|
||||
reinterpret_cast<const uint64_t *>(static_cast<uintptr_t>(Offsets::VAR_PARTY_GUIDS));
|
||||
for (int i = 0; i < Offsets::PARTY_MAX_SLOTS; ++i) {
|
||||
const uint64_t guid = party[i];
|
||||
if (guid != 0)
|
||||
add(guid, Unit::Identity::NameForGuid(guid), FLAG_IN_GROUP, true,
|
||||
Group::MemberStats::Lookup(guid).online, 0);
|
||||
}
|
||||
|
||||
int raidCount = Game::Read<int>(Offsets::VAR_RAID_MEMBER_COUNT);
|
||||
if (raidCount > Offsets::RAID_MAX_SLOTS)
|
||||
raidCount = Offsets::RAID_MAX_SLOTS;
|
||||
const auto *slots = reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_RAID_MEMBER_PTRS));
|
||||
for (int i = 0; i < raidCount; ++i) {
|
||||
if (slots[i] == nullptr)
|
||||
continue;
|
||||
const uint64_t guid = Game::Read<uint64_t>(slots[i], 0);
|
||||
const uint32_t flags = Game::Read<uint32_t>(slots[i], Offsets::OFF_RAID_SLOT_FLAGS);
|
||||
add(guid, Unit::Identity::NameForGuid(guid), FLAG_IN_GROUP, true,
|
||||
(flags & Offsets::RAID_SLOT_FLAG_ONLINE) != 0, 0);
|
||||
}
|
||||
|
||||
if (InGuild()) {
|
||||
auto **roster = Game::Read<const uint8_t **>(Offsets::VAR_GUILD_ROSTER_PTR);
|
||||
const int total = Game::Read<int>(Offsets::VAR_GUILD_ROSTER_TOTAL_COUNT);
|
||||
for (int i = 0; roster != nullptr && i < total; ++i) {
|
||||
const uint8_t *member = roster[i];
|
||||
if (member == nullptr)
|
||||
continue;
|
||||
add(Game::Read<uint64_t>(member, Offsets::OFF_GUILD_MEMBER_GUID),
|
||||
reinterpret_cast<const char *>(member + Offsets::OFF_GUILD_MEMBER_NAME),
|
||||
FLAG_IN_GUILD, true,
|
||||
Game::Read<uint32_t>(member, Offsets::OFF_GUILD_MEMBER_ONLINE) != 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
const int friends = FriendList::Count();
|
||||
for (int i = 0; i < friends; ++i) {
|
||||
const uint8_t *entry = FriendList::EntryByIndex(i);
|
||||
if (entry == nullptr)
|
||||
break;
|
||||
add(Game::Read<uint64_t>(entry, FriendList::OFF_FRIEND_GUID),
|
||||
Game::Read<const char *>(entry, FriendList::OFF_FRIEND_NAME), FLAG_FRIEND, true,
|
||||
Game::Read<uint8_t>(entry, FriendList::OFF_FRIEND_CONNECTED) != 0, 0);
|
||||
}
|
||||
|
||||
for (const Interaction &i : g_interactions)
|
||||
add(i.guid, i.name.c_str(), FLAG_INTERACTED_WITH, false, false, i.order);
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
// One entry per player, with every list's bits and Online decided; then the
|
||||
// include / exclude test, which autoCompleteUseContext turns off.
|
||||
std::vector<Entry> Merge(std::vector<Entry> found, uint32_t include, uint32_t exclude) {
|
||||
std::sort(found.begin(), found.end(),
|
||||
[](const Entry &a, const Entry &b) { return a.guid < b.guid; });
|
||||
std::vector<Entry> merged;
|
||||
for (const Entry &e : found) {
|
||||
if (!merged.empty() && merged.back().guid == e.guid) {
|
||||
Entry &m = merged.back();
|
||||
m.flags |= e.flags;
|
||||
m.listed = m.listed || e.listed;
|
||||
m.online = m.online || e.online;
|
||||
m.order = std::max(m.order, e.order);
|
||||
} else {
|
||||
merged.push_back(e);
|
||||
}
|
||||
}
|
||||
|
||||
const bool useContext = CVarOn(g_useContext);
|
||||
std::vector<Entry> kept;
|
||||
for (Entry &e : merged) {
|
||||
if (e.listed ? e.online : (e.flags & FLAG_INTERACTED_WITH) != 0)
|
||||
e.flags |= FLAG_ONLINE;
|
||||
if (!useContext || ((e.flags & include) != 0 && (e.flags & exclude) == 0))
|
||||
kept.push_back(e);
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
|
||||
int PriorityOf(uint32_t flags) {
|
||||
if (flags & FLAG_FRIEND)
|
||||
return PRIORITY_FRIEND;
|
||||
if (flags & FLAG_IN_GUILD)
|
||||
return PRIORITY_GUILD;
|
||||
if (flags & FLAG_IN_GROUP)
|
||||
return PRIORITY_IN_GROUP;
|
||||
if (flags & FLAG_INTERACTED_WITH)
|
||||
return PRIORITY_INTERACTED;
|
||||
return PRIORITY_OTHER;
|
||||
}
|
||||
|
||||
// Past `chars` code points of `s`, continuation bytes included, stopping at
|
||||
// the terminator. (Storm's SStrAdvanceCharsUTF8 stops one byte into a
|
||||
// multi-byte character.)
|
||||
const char *Advance(const char *s, size_t chars) {
|
||||
for (; *s != '\0' && chars != 0; --chars) {
|
||||
++s;
|
||||
while ((static_cast<uint8_t>(*s) & 0xC0) == 0x80)
|
||||
++s;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// What the edit box holds: the text and the cursor in it, in code points.
|
||||
struct Typed {
|
||||
const char *text;
|
||||
size_t length;
|
||||
size_t cursor;
|
||||
bool allowFullMatch;
|
||||
bool editFromCenter;
|
||||
};
|
||||
|
||||
bool Completes(const Typed &t, const char *name) {
|
||||
if (!t.allowFullMatch && Text::Utf8::CompareI(t.text, name) == 0)
|
||||
return false;
|
||||
if (!t.editFromCenter || t.cursor == t.length)
|
||||
return Text::Utf8::CompareI(t.text, name, t.length) == 0;
|
||||
|
||||
// The text before the cursor starts the name, and the text after it
|
||||
// follows somewhere later in the name.
|
||||
if (Text::Utf8::CompareI(t.text, name, t.cursor) != 0)
|
||||
return false;
|
||||
const size_t nameLength = Text::Utf8::Length(name);
|
||||
if (nameLength < t.length)
|
||||
return false;
|
||||
const char *tail = Advance(t.text, t.cursor);
|
||||
const size_t tailLength = t.length - t.cursor;
|
||||
const char *at = Advance(name, t.cursor);
|
||||
for (size_t places = nameLength - t.length + 1; places != 0; --places) {
|
||||
if (Text::Utf8::CompareI(at, tail, tailLength) == 0)
|
||||
return true;
|
||||
at = Advance(at, 1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A flag mask from Lua: bit.bor results above 0x7FFFFFFF arrive negative.
|
||||
uint32_t FlagArg(void *L, int index) {
|
||||
return static_cast<uint32_t>(static_cast<int64_t>(Game::Lua::ToNumber(L, index)));
|
||||
}
|
||||
|
||||
// C_AutoComplete.GetAutoCompleteResults(name, numResults, cursorPosition,
|
||||
// allowFullMatch, includeFlags, excludeFlags) -> results
|
||||
// A numResults of 0 or less returns every match, as the engine's count test
|
||||
// (`++count == numReturns`) does.
|
||||
int __fastcall Script_GetAutoCompleteResults(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3) ||
|
||||
!Game::Lua::IsNumber(L, 5) || !Game::Lua::IsNumber(L, 6)) {
|
||||
Game::Lua::Error(L, "Usage: C_AutoComplete.GetAutoCompleteResults(name, numResults, "
|
||||
"cursorPosition, allowFullMatch, includeFlags, excludeFlags)");
|
||||
return 0;
|
||||
}
|
||||
Typed typed;
|
||||
typed.text = Game::Lua::ToString(L, 1);
|
||||
typed.length = Text::Utf8::Length(typed.text);
|
||||
const double cursor = Game::Lua::ToNumber(L, 3);
|
||||
typed.cursor = cursor >= 1 && cursor <= static_cast<double>(typed.length)
|
||||
? static_cast<size_t>(cursor)
|
||||
: typed.length;
|
||||
typed.allowFullMatch = Game::Lua::ToBoolean(L, 4) != 0;
|
||||
typed.editFromCenter = CVarOn(g_editFromCenter);
|
||||
const int numResults = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
|
||||
std::vector<Entry> matches =
|
||||
Merge(Collect([&](const char *name) { return Completes(typed, name); }), FlagArg(L, 5),
|
||||
FlagArg(L, 6));
|
||||
const bool recency = CVarOn(g_resortOnRecency);
|
||||
std::sort(matches.begin(), matches.end(), [recency](const Entry &a, const Entry &b) {
|
||||
if (recency && a.order != b.order)
|
||||
return a.order > b.order;
|
||||
return Text::Utf8::CompareI(a.name, b.name) < 0;
|
||||
});
|
||||
if (numResults > 0 && matches.size() > static_cast<size_t>(numResults))
|
||||
matches.resize(numResults);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
for (size_t i = 0; i < matches.size(); ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "name", matches[i].name);
|
||||
Game::Lua::SetFieldNumber(L, "priority", PriorityOf(matches[i].flags));
|
||||
Game::Lua::SetFieldNumber(L, "bnetID", 0);
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// C_AutoComplete.IsRecognizedName(name, includeFlags, excludeFlags) -> bool
|
||||
int __fastcall Script_IsRecognizedName(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_AutoComplete.IsRecognizedName(name, includeFlags, excludeFlags)");
|
||||
return 0;
|
||||
}
|
||||
const char *text = Game::Lua::ToString(L, 1);
|
||||
const std::vector<Entry> matches =
|
||||
Merge(Collect([text](const char *name) { return Text::Utf8::CompareI(text, name) == 0; }),
|
||||
FlagArg(L, 2), FlagArg(L, 3));
|
||||
Game::Lua::PushBool(L, !matches.empty());
|
||||
return 1;
|
||||
}
|
||||
|
||||
void ClearOnLogout() {
|
||||
g_interactions.clear();
|
||||
g_lastOrder = 0;
|
||||
}
|
||||
|
||||
const Game::GlueModuleAutoRegister _clearOnLogout{&ClearOnLogout};
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kResultFields[] = {
|
||||
Game::Doc::Req("name", "string"),
|
||||
Game::Doc::Req("priority", "AutoCompletePriority",
|
||||
"The closest tie to the player: friend, guild, group, whisper, or none."),
|
||||
Game::Doc::Req("bnetID", "number", "Always 0."),
|
||||
};
|
||||
const Game::Doc::Structure kResult{"AutoCompleteResult", "AutoComplete", kResultFields};
|
||||
|
||||
const Game::Doc::Field kResultsArgs[] = {
|
||||
Game::Doc::Req("name", "string", "The text typed so far."),
|
||||
Game::Doc::Req("numResults", "number", "The most results to return; 0 returns all."),
|
||||
Game::Doc::Req("cursorPosition", "number", "The cursor's place in the text, in characters."),
|
||||
Game::Doc::Req("allowFullMatch", "bool", "Whether a name equal to the text can be a result."),
|
||||
Game::Doc::Req("includeFlags", "number",
|
||||
"Enum.AutoCompleteEntryFlag bits; a result has at least one."),
|
||||
Game::Doc::Req("excludeFlags", "number",
|
||||
"Enum.AutoCompleteEntryFlag bits; a result has none."),
|
||||
};
|
||||
const Game::Doc::Field kResultsRets[] = {
|
||||
Game::Doc::Req("results", "table", "Array of AutoCompleteResult."),
|
||||
};
|
||||
const Game::Doc::Function kGetAutoCompleteResults{
|
||||
"The known players whose names complete the typed text.", kResultsArgs, kResultsRets};
|
||||
|
||||
const Game::Doc::Field kRecognizedArgs[] = {
|
||||
Game::Doc::Req("name", "string"),
|
||||
Game::Doc::Req("includeFlags", "number",
|
||||
"Enum.AutoCompleteEntryFlag bits; the player has at least one."),
|
||||
Game::Doc::Req("excludeFlags", "number",
|
||||
"Enum.AutoCompleteEntryFlag bits; the player has none."),
|
||||
};
|
||||
const Game::Doc::Field kRecognizedRets[] = {
|
||||
Game::Doc::Req("isRecognizedName", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kIsRecognizedName{"Whether a known player has exactly this name.",
|
||||
kRecognizedArgs, kRecognizedRets};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kEntryFlagEntries[] = {
|
||||
{"InGroup", FLAG_IN_GROUP},
|
||||
{"InGuild", FLAG_IN_GUILD},
|
||||
{"Friend", FLAG_FRIEND},
|
||||
{"Bnet", FLAG_BNET},
|
||||
{"InteractedWith", FLAG_INTERACTED_WITH},
|
||||
{"Online", FLAG_ONLINE},
|
||||
{"InAOI", FLAG_IN_AOI},
|
||||
{"AccountCharacter", FLAG_ACCOUNT_CHARACTER},
|
||||
{"RecentPlayer", FLAG_RECENT_PLAYER},
|
||||
};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kPriorityEntries[] = {
|
||||
{"Other", PRIORITY_OTHER},
|
||||
{"Interacted", PRIORITY_INTERACTED},
|
||||
{"InGroup", PRIORITY_IN_GROUP},
|
||||
{"Guild", PRIORITY_GUILD},
|
||||
{"Friend", PRIORITY_FRIEND},
|
||||
{"AccountCharacter", PRIORITY_ACCOUNT_CHARACTER},
|
||||
{"AccountCharacterSameRealm", PRIORITY_ACCOUNT_CHARACTER_SAME_REALM},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void OnChat(int chatType, int languageID, const char *name, uint64_t guid) {
|
||||
if (chatType != CHAT_MSG_WHISPER && chatType != CHAT_MSG_WHISPER_INFORM)
|
||||
return;
|
||||
if (languageID == LANGUAGE_ADDON || guid == 0 || !Guid::IsPlayer(guid) || name == nullptr ||
|
||||
*name == '\0')
|
||||
return;
|
||||
++g_lastOrder;
|
||||
for (Interaction &i : g_interactions) {
|
||||
if (i.guid == guid) {
|
||||
i.name = name;
|
||||
i.order = g_lastOrder;
|
||||
return;
|
||||
}
|
||||
}
|
||||
g_interactions.push_back({guid, name, g_lastOrder});
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
// Retail's help lines and category (Ketho's CVars.lua dump).
|
||||
g_resortOnRecency = CVar::Factory::Register(
|
||||
"autoCompleteResortNamesOnRecency",
|
||||
"Shows people you recently spoke with higher up on the AutoComplete list.",
|
||||
Offsets::CVAR_CATEGORY_GAME, "1");
|
||||
g_editFromCenter = CVar::Factory::Register(
|
||||
"autoCompleteWhenEditingFromCenter",
|
||||
"If you edit a name by inserting characters into the center, a smarter auto-complete "
|
||||
"will occur.",
|
||||
Offsets::CVAR_CATEGORY_GAME, "1");
|
||||
g_useContext = CVar::Factory::Register(
|
||||
"autoCompleteUseContext",
|
||||
"The system will, for example, only show people in your guild when you are typing "
|
||||
"/gpromote. Names will also never be removed.",
|
||||
Offsets::CVAR_CATEGORY_GAME, "1");
|
||||
|
||||
Game::Lua::RegisterTableFunction("C_AutoComplete", "GetAutoCompleteResults",
|
||||
&Script_GetAutoCompleteResults, &kGetAutoCompleteResults);
|
||||
Game::Lua::RegisterTableFunction("C_AutoComplete", "IsRecognizedName", &Script_IsRecognizedName,
|
||||
&kIsRecognizedName);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "AutoCompleteEntryFlag", kEntryFlagEntries,
|
||||
sizeof(kEntryFlagEntries) / sizeof(kEntryFlagEntries[0]),
|
||||
"AutoComplete");
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "AutoCompletePriority", kPriorityEntries,
|
||||
sizeof(kPriorityEntries) / sizeof(kPriorityEntries[0]),
|
||||
"AutoComplete");
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace AutoComplete::Names
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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>
|
||||
|
||||
namespace AutoComplete::Names {
|
||||
|
||||
// Chat::Dispatch calls this for every dispatched chat line, before the engine
|
||||
// fires its CHAT_MSG_* event. A whisper to or from a player (`guid` is the
|
||||
// other player: Chat::Dispatch's sender) makes that player an InteractedWith
|
||||
// name, most recent first — 4.3.4's ChatFrame does the same in its message
|
||||
// handler. Every other line is ignored.
|
||||
void OnChat(int chatType, int languageID, const char *name, uint64_t guid);
|
||||
|
||||
} // namespace AutoComplete::Names
|
||||
@@ -19,8 +19,9 @@
|
||||
// the `BAG_UPDATE_DELAYED` event. Subscribe here to run C++ work exactly once
|
||||
// per frame in which any bag actually changed, instead of polling every frame.
|
||||
//
|
||||
// Callbacks fire at the tail of the world tick, after every BAG_UPDATE for the
|
||||
// frame has already fired (so bag contents are settled). No firing on frames
|
||||
// Callbacks fire from the world tick's subscription, after the frame's UI draw
|
||||
// and after every BAG_UPDATE for the frame has already fired (so bag contents
|
||||
// are settled). No firing on frames
|
||||
// where nothing changed. Order across subscribers is unspecified.
|
||||
//
|
||||
// Module pattern, matching `Tick::WorldTick::AutoSubscribe`: declare a
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// 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/>.
|
||||
|
||||
// `GetCallstackHeight()`, `GetErrorCallstackHeight()` and
|
||||
// `SetErrorCallstackHeight([height])` — retail's call stack heights. Its
|
||||
// error handler turns them into the debugstack level of the code that
|
||||
// failed, however many functions sit between that code and the handler:
|
||||
// debugstack(GetCallstackHeight() - (GetErrorCallstackHeight() - 1))
|
||||
// A height counts call levels as lua_getstack does, and so as debugstack
|
||||
// does: C functions and lost tail calls count, and level 1 is the caller of
|
||||
// the C function that asks.
|
||||
//
|
||||
// 1.12 records no error height, and needs none. Every error report runs the
|
||||
// engine's error function (FUN_FRAMESCRIPT_CALL_ERROR_HANDLER) as a C
|
||||
// closure: each script pcall passes it as the errfunc, so Lua calls it at
|
||||
// the point of the error before the stack unwinds, and a chunk that fails
|
||||
// to compile is reported by a pcall of it (FUN_LUA_RUN_STRING). It calls the
|
||||
// seterrorhandler function with lua_call. So while any handler runs, that
|
||||
// closure's level is on the stack directly above the code that failed, and
|
||||
// GetErrorCallstackHeight answers the height below the innermost one.
|
||||
// Nothing is recorded, so a handler that fails part way leaves nothing
|
||||
// behind, and a nested error finds its own level.
|
||||
//
|
||||
// SetErrorCallstackHeight is for a handler called directly, with no error
|
||||
// raised (retail's CallErrorHandler and assertsafe): with no engine level on
|
||||
// the stack, GetErrorCallstackHeight answers the value set. An engine level
|
||||
// wins over it, so a value a failed handler left set does not move the
|
||||
// stack of a later error. Retail's order between the two is not known; no
|
||||
// retail code sets the value around a raised error.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace BaseLib::ErrorCallstack {
|
||||
|
||||
namespace {
|
||||
|
||||
std::optional<int> g_setHeight;
|
||||
|
||||
// The number of call levels below the running C function.
|
||||
int Height(void *L) {
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
int height = 0;
|
||||
while (Game::Lua::GetStack(L, height + 1, ar))
|
||||
++height;
|
||||
return height;
|
||||
}
|
||||
|
||||
// The innermost level whose function is the engine's error function, or 0
|
||||
// when no error is being reported.
|
||||
int ErrorFunctionLevel(void *L) {
|
||||
const auto errorFunction = reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_FRAMESCRIPT_CALL_ERROR_HANDLER);
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
for (int level = 1; Game::Lua::GetStack(L, level, ar); ++level) {
|
||||
Game::Lua::GetInfo(L, "f", ar); // the level's function; nil for a lost tail call
|
||||
const bool found = Game::Lua::ToCFunction(L, -1) == errorFunction;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (found)
|
||||
return level;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetCallstackHeight(void *L) {
|
||||
Game::Lua::PushNumber(L, Height(L));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetErrorCallstackHeight(void *L) {
|
||||
const int level = ErrorFunctionLevel(L);
|
||||
if (level > 0)
|
||||
Game::Lua::PushNumber(L, Height(L) - level);
|
||||
else if (g_setHeight)
|
||||
Game::Lua::PushNumber(L, *g_setHeight);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetErrorCallstackHeight(void *L) {
|
||||
if (Game::Lua::Type(L, 1) <= Game::Lua::TYPE_NIL)
|
||||
g_setHeight.reset();
|
||||
else if (Game::Lua::IsNumber(L, 1))
|
||||
g_setHeight = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
else
|
||||
Game::Lua::Error(L, "Usage: SetErrorCallstackHeight([height])");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kHeightRets[] = {
|
||||
Game::Doc::Req("height", "number", "The calling function's level and every level below it."),
|
||||
};
|
||||
const Game::Doc::Field kErrorHeightRets[] = {
|
||||
Game::Doc::Opt("height", "number", nullptr, "The stack height of the code that caused the error."),
|
||||
};
|
||||
const Game::Doc::Field kSetArgs[] = {
|
||||
Game::Doc::Opt("height", "number", nullptr, "The height to return, or nil to clear it."),
|
||||
};
|
||||
const Game::Doc::Function kGetCallstackHeight{
|
||||
"Returns the number of call levels on the stack, counting the calling function as level 1.",
|
||||
{}, kHeightRets, "FrameScriptGlobals"};
|
||||
const Game::Doc::Function kGetErrorCallstackHeight{
|
||||
"Returns the call stack height of the code that caused the error being handled, or nil when no error is being handled.",
|
||||
{}, kErrorHeightRets, "FrameScriptGlobals"};
|
||||
const Game::Doc::Function kSetErrorCallstackHeight{
|
||||
"Sets the height that GetErrorCallstackHeight returns for an error handler called with no error raised.",
|
||||
kSetArgs, {}, "FrameScriptGlobals"};
|
||||
|
||||
// A new Lua state starts with no height set.
|
||||
void RegisterInGame() {
|
||||
g_setHeight.reset();
|
||||
Game::Lua::RegisterGlobalFunction("GetCallstackHeight", &Script_GetCallstackHeight, &kGetCallstackHeight);
|
||||
Game::Lua::RegisterGlobalFunction("GetErrorCallstackHeight", &Script_GetErrorCallstackHeight,
|
||||
&kGetErrorCallstackHeight);
|
||||
Game::Lua::RegisterGlobalFunction("SetErrorCallstackHeight", &Script_SetErrorCallstackHeight,
|
||||
&kSetErrorCallstackHeight);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
g_setHeight.reset();
|
||||
Game::Lua::RegisterGlueFunction("GetCallstackHeight", &Script_GetCallstackHeight, &kGetCallstackHeight);
|
||||
Game::Lua::RegisterGlueFunction("GetErrorCallstackHeight", &Script_GetErrorCallstackHeight,
|
||||
&kGetErrorCallstackHeight);
|
||||
Game::Lua::RegisterGlueFunction("SetErrorCallstackHeight", &Script_SetErrorCallstackHeight,
|
||||
&kSetErrorCallstackHeight);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::ErrorCallstack
|
||||
@@ -11,7 +11,8 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `securecall(func or name, ...)`, `securecallfunction(func, ...)` and
|
||||
// `securecall(func or name, ...)`, `securecallfunction(func, ...)`,
|
||||
// `securecallmethod(object, method, ...)` and
|
||||
// `secureexecuterange(tbl, func, ...)` — the later clients' protected calls,
|
||||
// less taint (1.12 has none). Retail, probed:
|
||||
// - securecall returns the function's results; a string names a global
|
||||
@@ -22,6 +23,11 @@
|
||||
// - secureexecuterange calls func(key, value, ...) for each entry of tbl in
|
||||
// `next` order; an error in one goes to the error handler and the walk
|
||||
// goes on. It returns nothing.
|
||||
// securecallmethod is from retail's API documentation, not a probe: it calls
|
||||
// object[method](object, ...) with a raw lookup (no metatable), and an error
|
||||
// in the lookup or the call goes to the error handler with no results. Here
|
||||
// a lookup that finds no function, or an object that is not a table, errors
|
||||
// the way calling that value does.
|
||||
// The error handler is the one every engine script run passes
|
||||
// (VAR_FRAMESCRIPT_ERROR_HANDLER_REF), as HookScript's wrapper uses.
|
||||
//
|
||||
@@ -70,6 +76,20 @@ int __fastcall Script_securecall(void *L) {
|
||||
|
||||
int __fastcall Script_securecallfunction(void *L) { return CallProtected(L); }
|
||||
|
||||
int __fastcall Script_securecallmethod(void *L) {
|
||||
if (Game::Lua::GetTop(L) < 2)
|
||||
Game::Lua::SetTop(L, 2);
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::PushValue(L, 2);
|
||||
Game::Lua::RawGet(L, 1);
|
||||
} else {
|
||||
Game::Lua::PushNil(L);
|
||||
}
|
||||
Game::Lua::Remove(L, 2); // [object, args..., func]
|
||||
Game::Lua::Insert(L, 1); // [func, object, args...]
|
||||
return CallProtected(L);
|
||||
}
|
||||
|
||||
int __fastcall Script_secureexecuterange(void *L) {
|
||||
const int n = Game::Lua::GetTop(L); // tbl, func, extra args 3..n
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE)
|
||||
@@ -97,6 +117,11 @@ const Game::Doc::Field kCallArgs[] = {
|
||||
const Game::Doc::Field kCallRets[] = {
|
||||
Game::Doc::Opt("...", "any", nullptr, "The function's results, or nothing after an error."),
|
||||
};
|
||||
const Game::Doc::Field kMethodArgs[] = {
|
||||
Game::Doc::Req("object", "table", "The table that holds the method. The lookup ignores its metatable."),
|
||||
Game::Doc::Req("method", "string", "The name of the method."),
|
||||
Game::Doc::Opt("...", "any", nullptr, "The arguments after object."),
|
||||
};
|
||||
const Game::Doc::Field kRangeArgs[] = {
|
||||
Game::Doc::Req("tbl", "table", "The table whose entries are visited."),
|
||||
Game::Doc::Req("func", "function", "Called as func(key, value, ...) for each entry."),
|
||||
@@ -108,6 +133,9 @@ const Game::Doc::Function kSecureCall{
|
||||
const Game::Doc::Function kSecureCallFunction{
|
||||
"Calls a function and returns its results. An error goes to the error handler and the call returns nothing.",
|
||||
kCallArgs, kCallRets, "LuaGlobals"};
|
||||
const Game::Doc::Function kSecureCallMethod{
|
||||
"Calls object[method](object, ...) and returns its results. An error goes to the error handler and the call returns nothing.",
|
||||
kMethodArgs, kCallRets, "LuaGlobals"};
|
||||
const Game::Doc::Function kSecureExecuteRange{
|
||||
"Calls a function for each entry of a table. An error goes to the error handler and the other entries still run.",
|
||||
kRangeArgs, {}, "LuaGlobals"};
|
||||
@@ -115,12 +143,14 @@ const Game::Doc::Function kSecureExecuteRange{
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("securecall", &Script_securecall, &kSecureCall);
|
||||
Game::Lua::RegisterGlobalFunction("securecallfunction", &Script_securecallfunction, &kSecureCallFunction);
|
||||
Game::Lua::RegisterGlobalFunction("securecallmethod", &Script_securecallmethod, &kSecureCallMethod);
|
||||
Game::Lua::RegisterGlobalFunction("secureexecuterange", &Script_secureexecuterange, &kSecureExecuteRange);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("securecall", &Script_securecall, &kSecureCall);
|
||||
Game::Lua::RegisterGlueFunction("securecallfunction", &Script_securecallfunction, &kSecureCallFunction);
|
||||
Game::Lua::RegisterGlueFunction("securecallmethod", &Script_securecallmethod, &kSecureCallMethod);
|
||||
Game::Lua::RegisterGlueFunction("secureexecuterange", &Script_secureexecuterange, &kSecureExecuteRange);
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ bool ExtractHeaderAttr(const char *tagStart, const char *tagEnd,
|
||||
|
||||
EmbeddedConfig ResolveEmbeddedConfig() {
|
||||
EmbeddedConfig out = {};
|
||||
const Embed::Bytes xml = Embed::Resource(Embed::Ids::kBindingsXml);
|
||||
const Embed::Bytes xml = Embed::File(Embed::Ids::kBindingsXml, 0);
|
||||
if (xml.data == nullptr) return out;
|
||||
const char *content = reinterpret_cast<const char *>(xml.data);
|
||||
const char *end = content + xml.size;
|
||||
|
||||
@@ -61,10 +61,7 @@ const char *UnitName(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
if (void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0)) {
|
||||
using GetName_t = const char *(__thiscall *)(void *obj, int *outFlags);
|
||||
const char *name =
|
||||
reinterpret_cast<GetName_t>(static_cast<uintptr_t>(Offsets::FUN_OBJECT_GET_NAME))(
|
||||
unit, nullptr);
|
||||
const char *name = Unit::Identity::NameForUnit(unit);
|
||||
if (name != nullptr && *name != '\0')
|
||||
return name;
|
||||
}
|
||||
|
||||
+10
-2
@@ -29,6 +29,8 @@
|
||||
// marker icons but still can't inject an arbitrary `|T` path.
|
||||
// - Chat::EventArgs::SenderScope — name the sender for the CHAT_MSG_* event
|
||||
// the original fires synchronously: its arg12 and `GetCurrentChatGUID()`.
|
||||
// - AutoComplete::Names::OnChat — a whisper partner becomes an
|
||||
// InteractedWith name for C_AutoComplete.
|
||||
//
|
||||
// Signature — `__fastcall`, `RET 0x28` (stack args at entry):
|
||||
// ECX = the MESSAGE text (`MOV ECX,[EBP-0x10]` in FUN_0049D560; copied into
|
||||
@@ -68,6 +70,7 @@
|
||||
#include "RaidMarkers.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "autocomplete/Names.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "net/PacketReader.h"
|
||||
@@ -158,10 +161,15 @@ void __fastcall ChatDispatch_h(
|
||||
// escapes are added.
|
||||
Chat::RaidMarkers::EventScope markers;
|
||||
|
||||
const uint64_t senderGuid = Sender(_ReturnAddress(), chatType, channel, flags, guidLo, guidHi);
|
||||
|
||||
// A whisper partner becomes an autocomplete name before the event fires,
|
||||
// so a CHAT_MSG_WHISPER handler already finds them.
|
||||
AutoComplete::Names::OnChat(chatType, languageID, senderName, senderGuid);
|
||||
|
||||
// The sender for the CHAT_MSG_* event the original fires synchronously;
|
||||
// restored when this scope ends (after the original returns).
|
||||
Chat::EventArgs::SenderScope sender(
|
||||
Sender(_ReturnAddress(), chatType, channel, flags, guidLo, guidHi), flags, true);
|
||||
Chat::EventArgs::SenderScope sender(senderGuid, flags, true);
|
||||
|
||||
g_original(msg, chatType, senderName, languageID, channel, targetName, flags,
|
||||
speakerGuidLo, speakerGuidHi, rank, guidLo, guidHi);
|
||||
|
||||
+17
-53
@@ -25,7 +25,8 @@
|
||||
// - a cast-time spell is cast -> SMSG_SPELL_GO, InterruptFlags & AUTOATTACK
|
||||
// - autorepeat (Auto Shot / Shoot) fires -> SMSG_SPELL_GO, AttributesEx2 & AUTOREPEAT
|
||||
// - starting a fresh melee attack -> SMSG_ATTACKSTART (off-hand only)
|
||||
// - a weapon is swapped mid-combat -> no packet; polled off the invMgr GUIDs
|
||||
// - a weapon is swapped mid-combat -> no packet; the engine's inventory-slot
|
||||
// observer (via Player::Equipment)
|
||||
// - the LOCAL PLAYER parries an attack -> SMSG_ATTACKERSTATEUPDATE, victim == us,
|
||||
// targetState == PARRY (no separate packet —
|
||||
// the server just clips OUR remaining time)
|
||||
@@ -431,64 +432,29 @@ void OnPacket(uint32_t opcode, CDataStore *packet) {
|
||||
|
||||
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
|
||||
|
||||
// ---- Weapon swap mid-combat (polled -- no packet, no observer needed) ---
|
||||
} // namespace
|
||||
|
||||
// ---- Weapon swap mid-combat (inventory-slot observer) -------------------
|
||||
//
|
||||
// A weapon equipped while `IsInCombat()` resets that hand's timer
|
||||
// server-side (`Player::_ApplyWeaponDependentAuraMods` -> `SetAttackTime`
|
||||
// with `resetTimer = IsInCombat()`). There's no dedicated packet for this;
|
||||
// `Player::Equipment` already sees the same invMgr GUIDs change via the
|
||||
// engine's inventory-slot observer for `PLAYER_EQUIPMENT_CHANGED`, but
|
||||
// that's a one-way Lua event fire with nothing to subscribe to in C++. Only
|
||||
// 3 slots matter here, so a plain 3-GUID compare on the existing WorldTick
|
||||
// pass is simpler than a second hook on the same observer.
|
||||
|
||||
uint64_t g_lastMainGuid = 0;
|
||||
uint64_t g_lastOffGuid = 0;
|
||||
uint64_t g_lastRangedGuid = 0;
|
||||
bool g_weaponSnapValid = false;
|
||||
|
||||
void PollWeaponSwap() {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr) {
|
||||
g_weaponSnapValid = false;
|
||||
return;
|
||||
}
|
||||
const uint8_t *invMgr = player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
|
||||
const uint32_t count =
|
||||
*reinterpret_cast<const uint32_t *>(invMgr + Offsets::OFF_INVMGR_SLOT_COUNT);
|
||||
if (count < 18) {
|
||||
g_weaponSnapValid = false;
|
||||
return;
|
||||
}
|
||||
const uint64_t *guids = *reinterpret_cast<const uint64_t *const *>(
|
||||
invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (guids == nullptr) {
|
||||
g_weaponSnapValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// `Player::Equipment`'s co-hook on the engine's inventory-slot observer calls
|
||||
// in here when a slot's GUID changes. That runs inside SMSG_UPDATE_OBJECT
|
||||
// after the whole values block is written, so `AttackTimeMs` already reads
|
||||
// the new weapon's speed when it arrives in the same block.
|
||||
void OnWeaponSlotChanged(int slot0) {
|
||||
// 0-based invMgr slots: 15 = main hand (Lua 16), 16 = off hand (17),
|
||||
// 17 = ranged (18) -- same `slot0 = luaSlot - 1` convention documented
|
||||
// in Player::Equipment.
|
||||
const uint64_t mainGuid = guids[15];
|
||||
const uint64_t offGuid = guids[16];
|
||||
const uint64_t rangedGuid = guids[17];
|
||||
|
||||
if (g_weaponSnapValid && PlayerInCombat()) {
|
||||
if (mainGuid != g_lastMainGuid)
|
||||
MarkReset(MAIN_HAND, AttackTimeMs(MAIN_HAND));
|
||||
if (offGuid != g_lastOffGuid)
|
||||
MarkReset(OFF_HAND, AttackTimeMs(OFF_HAND));
|
||||
if (rangedGuid != g_lastRangedGuid)
|
||||
MarkReset(RANGED, AttackTimeMs(RANGED));
|
||||
}
|
||||
|
||||
g_lastMainGuid = mainGuid;
|
||||
g_lastOffGuid = offGuid;
|
||||
g_lastRangedGuid = rangedGuid;
|
||||
g_weaponSnapValid = true;
|
||||
// in Player::Equipment, and in SwingType order.
|
||||
if (slot0 < 15 || slot0 > 17 || !PlayerInCombat())
|
||||
return;
|
||||
const auto type = static_cast<SwingType>(slot0 - 15);
|
||||
MarkReset(type, AttackTimeMs(type));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// ---- PLAYER_SWING fire (WorldTick, at most once per type per frame) -----
|
||||
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
@@ -504,8 +470,6 @@ const Game::Doc::Event kPlayerSwingDoc{
|
||||
const Event::Custom::AutoReserve _evt{"PLAYER_SWING", &kPlayerSwingDoc};
|
||||
|
||||
void OnWorldTick() {
|
||||
PollWeaponSwap();
|
||||
|
||||
const int slot = _evt.Slot();
|
||||
const uint32_t now = NowMs();
|
||||
for (int t = 0; t < 3; ++t) {
|
||||
|
||||
@@ -32,6 +32,12 @@ enum SwingType : int { MAIN_HAND = 0, OFF_HAND = 1, RANGED = 2 };
|
||||
// callers treat 0 as "this swing type doesn't apply right now".
|
||||
uint32_t AttackTimeMs(SwingType type);
|
||||
|
||||
// Called by `Player::Equipment` from the engine's inventory-slot observer
|
||||
// when the item GUID in 0-based equipment slot `slot0` changes. A weapon
|
||||
// slot (15 / 16 / 17) changed in combat resets that hand's swing timer; any
|
||||
// other slot is ignored.
|
||||
void OnWeaponSlotChanged(int slot0);
|
||||
|
||||
// True if `player` and `target` (raw CGUnit* object pointers, e.g. from
|
||||
// `Unit::Position::ResolveToken` or `Object::ByGuid`) are within pure 2D
|
||||
// (X/Y) melee reach of each other -- the server's own auto-attack distance
|
||||
|
||||
+21
-12
@@ -35,6 +35,7 @@
|
||||
#include "combatlog/Entry.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "combatlog/File.h"
|
||||
#include "combatlog/History.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
@@ -298,7 +299,6 @@ using CacheRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t id, co
|
||||
using NameCache_t = const uint8_t *(__thiscall *)(void *cache, uint32_t lo, uint32_t hi,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, char dedup);
|
||||
using ObjectName_t = const char *(__thiscall *)(void *object, int *outFlags);
|
||||
using GameObjectName_t = const char *(__fastcall *)(void *object);
|
||||
|
||||
void *QueryDone() { return reinterpret_cast<void *>(Offsets::FUN_COMBAT_LOG_NAME_QUERY_DONE); }
|
||||
@@ -322,7 +322,7 @@ const char *ObjectName(uint64_t guid) {
|
||||
const uint32_t type =
|
||||
fields != nullptr ? Game::Read<uint32_t>(fields, Offsets::OFF_OBJECT_FIELD_TYPE) : 0;
|
||||
if (type & Offsets::TYPEMASK_UNIT)
|
||||
return reinterpret_cast<ObjectName_t>(Offsets::FUN_OBJECT_GET_NAME)(object, nullptr);
|
||||
return Unit::Identity::NameForUnit(object);
|
||||
if (type & Offsets::TYPEMASK_GAMEOBJECT)
|
||||
return reinterpret_cast<GameObjectName_t>(Offsets::FUN_GAMEOBJECT_GET_NAME)(object);
|
||||
if (type & Offsets::TYPEMASK_ITEM)
|
||||
@@ -406,9 +406,7 @@ int Push(void *L, const Entry &e) {
|
||||
PushString(L, name);
|
||||
else
|
||||
PushNil(L);
|
||||
PushNumber(L, spell != nullptr
|
||||
? SchoolMask(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_SCHOOL))
|
||||
: e.spellSchool);
|
||||
PushNumber(L, spell != nullptr ? SpellSchoolMask(spell) : e.spellSchool);
|
||||
count += 3;
|
||||
}
|
||||
if (e.info & INFO_COUNT) {
|
||||
@@ -420,14 +418,12 @@ int Push(void *L, const Entry &e) {
|
||||
++count;
|
||||
}
|
||||
if (e.info & INFO_MISS) {
|
||||
const int miss = e.v[0];
|
||||
PushString(L, (miss >= 0 && miss < MISS_COUNT) ? kMissNames[miss] : kMissNames[0]);
|
||||
PushString(L, MissName(e.v[0]));
|
||||
PushFlag(L, e.offHand);
|
||||
count += 2;
|
||||
}
|
||||
if (e.info & INFO_ENVIRONMENTAL) {
|
||||
const int type = e.v[0];
|
||||
PushString(L, (type >= 0 && type < 6) ? kEnvironmentalNames[type] : kEnvironmentalNames[0]);
|
||||
PushString(L, EnvironmentalName(e.v[0]));
|
||||
++count;
|
||||
}
|
||||
if (e.info & INFO_DAMAGE) {
|
||||
@@ -489,9 +485,7 @@ int Push(void *L, const Entry &e) {
|
||||
PushString(L, name);
|
||||
else
|
||||
PushNil(L);
|
||||
PushNumber(L, extra != nullptr ? SchoolMask(Game::Read<uint32_t>(
|
||||
extra, Offsets::OFF_SPELL_RECORD_SCHOOL))
|
||||
: 0);
|
||||
PushNumber(L, SpellSchoolMask(extra));
|
||||
count += 3;
|
||||
}
|
||||
if (e.info & INFO_ITEM) {
|
||||
@@ -582,6 +576,14 @@ const char *Name(int event) {
|
||||
return (event >= 0 && event < EVENT_COUNT) ? kEventNames[event] : "";
|
||||
}
|
||||
|
||||
const char *MissName(int missType) {
|
||||
return (missType >= 0 && missType < MISS_COUNT) ? kMissNames[missType] : kMissNames[0];
|
||||
}
|
||||
|
||||
const char *EnvironmentalName(int type) {
|
||||
return (type >= 0 && type < 6) ? kEnvironmentalNames[type] : kEnvironmentalNames[0];
|
||||
}
|
||||
|
||||
int PushEvent(void *L, const Entry &entry) { return Push(L, entry); }
|
||||
|
||||
const uint8_t *SpellRecord(int spellId) { return Spell::Lookup::RecordForID(spellId); }
|
||||
@@ -609,6 +611,12 @@ bool Ranged(const uint8_t *spell) {
|
||||
|
||||
uint32_t SchoolMask(uint32_t school) { return school < kSchoolCount ? 1u << school : 0; }
|
||||
|
||||
uint32_t SpellSchoolMask(const uint8_t *spell) {
|
||||
return spell != nullptr
|
||||
? SchoolMask(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_SCHOOL))
|
||||
: 0;
|
||||
}
|
||||
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName) {
|
||||
if (spell == nullptr)
|
||||
return false;
|
||||
@@ -701,6 +709,7 @@ void Queue(Entry &e) {
|
||||
g_startTimeMs = e.timeMs;
|
||||
}
|
||||
History::Record(e);
|
||||
File::Write(e);
|
||||
Send(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,12 @@ enum Subevent : int {
|
||||
|
||||
const char *Name(int event);
|
||||
|
||||
// The INFO_MISS value as 4.3.4's g_spellMissStrings names it.
|
||||
const char *MissName(int missType);
|
||||
|
||||
// The INFO_ENVIRONMENTAL value as 4.3.4's s_environmentalDamageTypes names it.
|
||||
const char *EnvironmentalName(int type);
|
||||
|
||||
// 4.3.4's m_infoFlags: which suffix blocks the event carries, pushed in
|
||||
// this order after the spell prefix.
|
||||
enum Info : uint32_t {
|
||||
@@ -158,6 +164,9 @@ 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);
|
||||
|
||||
// The spell's school as a mask (4.3.4's SpellRec m_schoolMask), 0 for nullptr.
|
||||
uint32_t SpellSchoolMask(const uint8_t *spell);
|
||||
|
||||
// Whether one of the spell's three effects applies the aura `auraName`
|
||||
// (EffectApplyAuraName).
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName);
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The combat log file — 4.3.4's CombatLogEntry::PackEvent line, written as
|
||||
// SendCombatLogEvents writes it while LoggingCombat is on.
|
||||
//
|
||||
// The engine pieces:
|
||||
// - the switch is LoggingCombat's own flag, so /combatlog and
|
||||
// LoggingCombat() turn both files on and off, and logging out (the chat
|
||||
// teardown in CGGameUI::ShutdownGame, 0x00498700, clears the flag) stops
|
||||
// both;
|
||||
// - each time the flag turns on, a new file starts, named as retail names
|
||||
// it: Logs\WoWCombatLog-MMDDYY_HHMMSS.txt, the local time logging started.
|
||||
// 1.12's own Logs\WoWCombatLog.txt keeps its chat-text lines, which 1.12
|
||||
// log readers parse;
|
||||
// - the file is a Storm log, opened as LoggingCombat opens its own (append)
|
||||
// and written with SLogWrite (which stamps "M/D HH:MM:SS.mmm " on each
|
||||
// line, as in 4.3.4's file). When the flag turns off it is closed, which
|
||||
// writes its buffered lines. Storm closes what is still open at exit;
|
||||
// - the line is PackEvent's: no hideCaster, flags as hex, the spell school
|
||||
// as a hex mask (the extra spell's in decimal), damage values as numbers,
|
||||
// flags as 1 or nil, no isOffHand and no alternate power type.
|
||||
//
|
||||
// The flag is read once a frame (FrameTick, which also runs on the glue
|
||||
// screen, so a logout's cleared flag is seen there) and before each line.
|
||||
// LoggingCombat itself is not hooked: other DLLs add their own logs to it.
|
||||
|
||||
#include "combatlog/File.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "tick/FrameTick.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
namespace CombatLog::File {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kAppend = 4; // SLogCreate: open at the end of the file
|
||||
|
||||
uint32_t g_log = 0;
|
||||
|
||||
bool Logging() { return Game::Read<int>(Offsets::VAR_COMBAT_LOGGING) != 0; }
|
||||
|
||||
// A new log named for the local time now. True when it is open.
|
||||
bool Open() {
|
||||
const std::time_t now = std::time(nullptr);
|
||||
std::tm local{};
|
||||
if (localtime_s(&local, &now) != 0)
|
||||
return false;
|
||||
char path[64];
|
||||
std::strftime(path, sizeof path, "Logs\\WoWCombatLog-%m%d%y_%H%M%S.txt", &local);
|
||||
using Create_t = int(__stdcall *)(const char *path, uint32_t flags, uint32_t *log);
|
||||
const auto create = reinterpret_cast<Create_t>(static_cast<uintptr_t>(Offsets::FUN_SLOG_CREATE));
|
||||
if (create(path, kAppend, &g_log) == 0)
|
||||
g_log = 0;
|
||||
return g_log != 0;
|
||||
}
|
||||
|
||||
void Close() {
|
||||
using Close_t = void(__stdcall *)(uint32_t log);
|
||||
reinterpret_cast<Close_t>(static_cast<uintptr_t>(Offsets::FUN_SLOG_CLOSE))(g_log);
|
||||
g_log = 0;
|
||||
}
|
||||
|
||||
// Follows the flag: a new file when it turns on, the file closed when it
|
||||
// turns off.
|
||||
void OnFrame() {
|
||||
const bool logging = Logging();
|
||||
if (logging && g_log == 0)
|
||||
Open();
|
||||
else if (!logging && g_log != 0)
|
||||
Close();
|
||||
}
|
||||
|
||||
const Tick::FrameTick::AutoSubscribe _frame{&OnFrame};
|
||||
|
||||
// PackString
|
||||
void Plain(std::string &line, const char *text) {
|
||||
line += ',';
|
||||
line += text;
|
||||
}
|
||||
|
||||
// PackQuotedString, or PackString "nil" when there is no text.
|
||||
void Quoted(std::string &line, const char *text) {
|
||||
if (text == nullptr) {
|
||||
Plain(line, "nil");
|
||||
return;
|
||||
}
|
||||
line += ",\"";
|
||||
for (const char *c = text; *c != '\0'; ++c) {
|
||||
if (*c == '"' || *c == '\\')
|
||||
line += '\\';
|
||||
line += *c;
|
||||
}
|
||||
line += '"';
|
||||
}
|
||||
|
||||
// PackHex
|
||||
void Hex(std::string &line, uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof text, ",0x%x", value);
|
||||
line += text;
|
||||
}
|
||||
|
||||
void Number(std::string &line, int value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof text, ",%d", value);
|
||||
line += text;
|
||||
}
|
||||
|
||||
// PackBool
|
||||
void Flag(std::string &line, bool value) { line += value ? ",1" : ",nil"; }
|
||||
|
||||
void Unit(std::string &line, uint64_t guid, bool named, const std::string &name, uint32_t flags,
|
||||
uint32_t raidFlags) {
|
||||
char text[Guid::STRING_SIZE];
|
||||
line += ',';
|
||||
line += Guid::FormatAsString(guid, text, sizeof text);
|
||||
Quoted(line, named ? name.c_str() : nullptr);
|
||||
Hex(line, flags);
|
||||
Hex(line, raidFlags);
|
||||
}
|
||||
|
||||
// CombatLogEntry::PackEvent, the blocks 1.12 fills.
|
||||
std::string Pack(const Entry &e) {
|
||||
std::string line = Name(e.event);
|
||||
Unit(line, e.source, e.sourceNamed, e.sourceName, e.sourceFlags, e.sourceRaidFlags);
|
||||
Unit(line, e.dest, e.destNamed, e.destName, e.destFlags, e.destRaidFlags);
|
||||
if (e.spellPrefix) {
|
||||
const uint8_t *spell = SpellRecord(e.spellId);
|
||||
Number(line, e.spellId);
|
||||
Quoted(line, SpellName(spell));
|
||||
Hex(line, spell != nullptr ? SpellSchoolMask(spell) : e.spellSchool);
|
||||
}
|
||||
if (e.info & INFO_COUNT)
|
||||
Number(line, e.v[0]);
|
||||
if (e.info & INFO_TEXT)
|
||||
Quoted(line, e.text.c_str());
|
||||
if (e.info & INFO_MISS)
|
||||
Plain(line, MissName(e.v[0]));
|
||||
if (e.info & INFO_ENVIRONMENTAL)
|
||||
Plain(line, EnvironmentalName(e.v[0]));
|
||||
if (e.info & INFO_DAMAGE) {
|
||||
for (const int slot : {1, 2, 3, 5, 6, 4})
|
||||
Number(line, e.v[slot]);
|
||||
Flag(line, e.critical);
|
||||
Flag(line, e.glancing);
|
||||
Flag(line, e.crushing);
|
||||
}
|
||||
if (e.info & INFO_MISS_ABSORBED)
|
||||
Number(line, e.v[4]);
|
||||
if (e.info & INFO_MISS_RESISTED)
|
||||
Number(line, e.v[5]);
|
||||
if (e.info & INFO_MISS_BLOCKED)
|
||||
Number(line, e.v[6]);
|
||||
if (e.info & INFO_HEAL) {
|
||||
for (int i = 0; i < 3; ++i)
|
||||
Number(line, e.v[i]);
|
||||
Flag(line, e.critical);
|
||||
}
|
||||
if (e.info & INFO_ENERGIZE) {
|
||||
for (int i = 0; i < 2; ++i)
|
||||
Number(line, e.v[i]);
|
||||
}
|
||||
if (e.info & INFO_DRAIN) {
|
||||
for (int i = 0; i < 3; ++i)
|
||||
Number(line, e.v[i]);
|
||||
}
|
||||
if (e.info & INFO_EXTRA_SPELL) {
|
||||
const uint8_t *extra = SpellRecord(e.v[0]);
|
||||
Number(line, e.v[0]);
|
||||
Quoted(line, SpellName(extra));
|
||||
Number(line, static_cast<int>(SpellSchoolMask(extra)));
|
||||
}
|
||||
if (e.info & INFO_ITEM) {
|
||||
Number(line, e.v[0]);
|
||||
Quoted(line, e.itemNamed ? e.itemName.c_str() : nullptr);
|
||||
}
|
||||
if (e.info & INFO_AURA_TYPE)
|
||||
Plain(line, e.harmful ? "DEBUFF" : "BUFF");
|
||||
if (e.info & INFO_AURA_AMOUNT)
|
||||
Number(line, e.v[2]);
|
||||
return line;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Write(const Entry &entry) {
|
||||
// A line in the frame logging turned on comes before the frame's check.
|
||||
if (!Logging() || (g_log == 0 && !Open()))
|
||||
return;
|
||||
using Write_t = void(__cdecl *)(uint32_t log, const char *format, ...);
|
||||
reinterpret_cast<Write_t>(static_cast<uintptr_t>(Offsets::FUN_SLOG_WRITE))(
|
||||
g_log, "%s", Pack(entry).c_str());
|
||||
}
|
||||
|
||||
} // namespace CombatLog::File
|
||||
@@ -0,0 +1,29 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// The combat log file — 4.3.4's WoWCombatLog.txt line, written while
|
||||
// LoggingCombat is on to a file per logging session named as retail names it
|
||||
// (Logs\WoWCombatLog-MMDDYY_HHMMSS.txt). 1.12's own WoWCombatLog.txt keeps
|
||||
// its chat-text lines.
|
||||
|
||||
#include "combatlog/Entry.h"
|
||||
|
||||
namespace CombatLog::File {
|
||||
|
||||
// SendCombatLogEvents' write: CombatLogEntry::PackEvent's line, through
|
||||
// CGChat::AddCombatMessage's SLogWrite, while combat logging is on.
|
||||
void Write(const Entry &entry);
|
||||
|
||||
} // namespace CombatLog::File
|
||||
@@ -0,0 +1,83 @@
|
||||
// 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 "compat/WowTurbo.h"
|
||||
|
||||
#include "Common.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <windows.h>
|
||||
|
||||
namespace Compat::WowTurbo {
|
||||
|
||||
namespace {
|
||||
|
||||
// The prologue patch wow_turbo's MinHook writes on x86: `jmp rel32` over the
|
||||
// first five bytes. 1.12's entries open with `push ebp; mov ebp,esp`, never
|
||||
// the `mov edi,edi` that would make MinHook hot-patch instead.
|
||||
constexpr uint8_t kJmp = 0xE9;
|
||||
constexpr size_t kJmpLen = 5;
|
||||
|
||||
// The `kJmpLen` bytes at `va` as WoW.exe holds them on disk, located through
|
||||
// the loaded image's own section table.
|
||||
bool ReadStock(uintptr_t va, uint8_t (&out)[kJmpLen]) {
|
||||
const auto base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
|
||||
const auto *dos = reinterpret_cast<const IMAGE_DOS_HEADER *>(base);
|
||||
const auto *nt = reinterpret_cast<const IMAGE_NT_HEADERS *>(base + dos->e_lfanew);
|
||||
const IMAGE_SECTION_HEADER *section = IMAGE_FIRST_SECTION(nt);
|
||||
const uintptr_t rva = va - base;
|
||||
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++section) {
|
||||
if (rva < section->VirtualAddress ||
|
||||
rva + kJmpLen > section->VirtualAddress + section->SizeOfRawData)
|
||||
continue;
|
||||
wchar_t path[MAX_PATH];
|
||||
const DWORD len = GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
if (len == 0 || len == MAX_PATH)
|
||||
return false;
|
||||
const HANDLE file = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, nullptr,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE)
|
||||
return false;
|
||||
OVERLAPPED at{};
|
||||
at.Offset = static_cast<DWORD>(rva - section->VirtualAddress + section->PointerToRawData);
|
||||
DWORD read = 0;
|
||||
const bool ok = ReadFile(file, out, kJmpLen, &read, &at) != 0 && read == kJmpLen;
|
||||
CloseHandle(file);
|
||||
return ok;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Reclaim(uintptr_t function) {
|
||||
const HMODULE turbo = GetModuleHandleW(L"wow_turbo.dll");
|
||||
if (turbo == nullptr)
|
||||
return;
|
||||
const auto *entry = reinterpret_cast<const uint8_t *>(function);
|
||||
if (entry[0] != kJmp)
|
||||
return;
|
||||
int32_t rel = 0;
|
||||
std::memcpy(&rel, entry + 1, sizeof rel);
|
||||
HMODULE owner = nullptr;
|
||||
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(function + kJmpLen + rel), &owner) ||
|
||||
owner != turbo)
|
||||
return;
|
||||
uint8_t stock[kJmpLen];
|
||||
if (ReadStock(function, stock) && stock[0] != kJmp)
|
||||
Common::PatchBytes(reinterpret_cast<void *>(function), stock, kJmpLen);
|
||||
}
|
||||
|
||||
} // namespace Compat::WowTurbo
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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>
|
||||
|
||||
// wow_turbo (github.com/athei/wow-mods) detours ~380 engine functions to Rust
|
||||
// reimplementations and yields any function another mod has already hooked:
|
||||
// its installer refuses a prologue that fails its signature. It installs from
|
||||
// DllMain, but ClassicAPI installs from `Load`, which VanillaFixes calls after
|
||||
// every DllMain, so wow_turbo never sees our hooks and always installs first.
|
||||
// MinHook then copies wow_turbo's jump into our trampoline, and our hook calls
|
||||
// the Rust body instead of the engine's. That loses everything that lives in
|
||||
// the stock body: Frame::ScriptArgs' pcall redirects in both script runners,
|
||||
// and InlineTexture / IndentedWordWrap in the text builder, whose Rust copy
|
||||
// calls its own emitter.
|
||||
//
|
||||
// `Reclaim` gives the stock prologue back before we hook, the state wow_turbo
|
||||
// itself produces when it loads after a mod that hooked first. wow_turbo keeps
|
||||
// its original in place, logs "prologue overwritten ... by ClassicAPI.dll"
|
||||
// and does not re-patch these entries.
|
||||
//
|
||||
// The DllMain core hooks need no reclaim: the only one wow_turbo also
|
||||
// replaces is the integrity scan, which both make the same no-op.
|
||||
|
||||
namespace Compat::WowTurbo {
|
||||
|
||||
// Restores the engine's own first instruction at `function` when it is a
|
||||
// `jmp` into wow_turbo.dll. No-op when wow_turbo is not loaded or the
|
||||
// prologue is anything else.
|
||||
void Reclaim(uintptr_t function);
|
||||
|
||||
} // namespace Compat::WowTurbo
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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/>.
|
||||
|
||||
// `Enum.BagIndex` — retail's BagIndexConstants enumeration, with Classic
|
||||
// Era's names for the containers this client has and this client's own
|
||||
// indices for them. Era numbers its bank bags from 6 because its reagent bag
|
||||
// takes 5; here they start right after the equipped bags, as the stock bank
|
||||
// frame numbers them. The reagent bag, reagent bank and account bank tabs
|
||||
// have no entry.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Container::BagIndex {
|
||||
|
||||
namespace {
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kEntries[] = {
|
||||
{"Keyring", Offsets::BAG_INDEX_KEYRING},
|
||||
{"Bank", Offsets::BAG_INDEX_BANK},
|
||||
{"Backpack", Offsets::BAG_INDEX_BACKPACK},
|
||||
{"Bag_1", Offsets::BAG_INDEX_BAG_FIRST},
|
||||
{"Bag_2", Offsets::BAG_INDEX_BAG_FIRST + 1},
|
||||
{"Bag_3", Offsets::BAG_INDEX_BAG_FIRST + 2},
|
||||
{"Bag_4", Offsets::BAG_INDEX_BAG_LAST},
|
||||
{"BankBag_1", Offsets::BAG_INDEX_BANK_BAG_FIRST},
|
||||
{"BankBag_2", Offsets::BAG_INDEX_BANK_BAG_FIRST + 1},
|
||||
{"BankBag_3", Offsets::BAG_INDEX_BANK_BAG_FIRST + 2},
|
||||
{"BankBag_4", Offsets::BAG_INDEX_BANK_BAG_FIRST + 3},
|
||||
{"BankBag_5", Offsets::BAG_INDEX_BANK_BAG_FIRST + 4},
|
||||
{"BankBag_6", Offsets::BAG_INDEX_BANK_BAG_LAST},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "BagIndex", kEntries,
|
||||
sizeof(kEntries) / sizeof(kEntries[0]), "BagIndexConstants");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::BagIndex
|
||||
@@ -208,13 +208,6 @@ bool IsItemFiltered(const uint8_t *cgItem) {
|
||||
|
||||
namespace {
|
||||
|
||||
// Lua's BACKPACK_CONTAINER, BANK_CONTAINER, KEYRING_CONTAINER and the last
|
||||
// bank bag (NUM_BAG_SLOTS + NUM_BANKBAGSLOTS).
|
||||
constexpr int kBackpack = 0;
|
||||
constexpr int kBank = -1;
|
||||
constexpr int kKeyring = -2;
|
||||
constexpr int kLastBankBag = 10;
|
||||
|
||||
const Game::Doc::Event kSearchUpdateDoc{
|
||||
"Container", "Fires when the bag search text changes.", {}};
|
||||
const Event::Custom::AutoReserve _evt{"INVENTORY_SEARCH_UPDATE", &kSearchUpdateDoc};
|
||||
@@ -225,11 +218,12 @@ const Event::Custom::AutoReserve _evt{"INVENTORY_SEARCH_UPDATE", &kSearchUpdateD
|
||||
// get their fixed sizes. The keyring is the inventory's last range, so the
|
||||
// whole-inventory bound is its own. Any other index is no container.
|
||||
int SlotsToWalk(int bagID) {
|
||||
if (bagID == kBackpack)
|
||||
if (bagID == Offsets::BAG_INDEX_BACKPACK)
|
||||
return Offsets::BACKPACK_NUM_SLOTS;
|
||||
if (bagID == kBank)
|
||||
if (bagID == Offsets::BAG_INDEX_BANK)
|
||||
return Offsets::INVMGR_BANK_MAIN_LAST_SLOT - Offsets::INVMGR_BANK_MAIN_FIRST_SLOT + 1;
|
||||
if (bagID == kKeyring || (bagID >= 1 && bagID <= kLastBankBag))
|
||||
if (bagID == Offsets::BAG_INDEX_KEYRING ||
|
||||
(bagID >= Offsets::BAG_INDEX_BAG_FIRST && bagID <= Offsets::BAG_INDEX_BANK_BAG_LAST))
|
||||
return INT_MAX;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
// reproduction of Blizzard's: hearthstone, gear, consumables, reagents,
|
||||
// trade goods, quest items, everything else by quality, junk last. Within
|
||||
// a category, by item class, subclass, name, then fuller stacks first.
|
||||
// Gear adds two keys so like pieces sit together: the equip slot right
|
||||
// after the class (kSlotOrder), and quality, best first, before the name.
|
||||
// Gear adds two keys so like pieces sit together: the equip slot ahead of
|
||||
// the class (kSlotOrder), and quality, best first, before the name.
|
||||
//
|
||||
// An item whose data has NOT arrived, or that is locked (on the cursor,
|
||||
// in the trade window, mid-move), is pinned: left where it is, with its
|
||||
@@ -172,7 +172,10 @@ uint8_t CategoryFor(uint32_t itemID, uint32_t itemClass, uint32_t quality) {
|
||||
// shoulder. Not the equipment slot either, which lumps shields, off-hand
|
||||
// weapons and held items into one. The order is Baganator's (its
|
||||
// Sorting/ItemFields.lua): weapons by hand, then ranged and ammo, then
|
||||
// armor head to foot, then jewelry, shirt and tabard.
|
||||
// armor head to foot, then jewelry, shirt and tabard. Unlike Baganator,
|
||||
// this key runs before the item class: shields, held-in-off-hand items
|
||||
// and relics are armor class, and ranking class first would pull them
|
||||
// out of the hand group to the head of the armor block.
|
||||
constexpr uint8_t kSlotOrder[] = {
|
||||
17, 13, 21, 14, 23, 26, 22, 15, 25, 24, 27, 28, // weapons, ranged, relic
|
||||
1, 3, 16, 5, 20, 9, 10, 6, 7, 8, // head .. feet
|
||||
@@ -238,11 +241,11 @@ int CompareNames(const char *a, const char *b) {
|
||||
bool Precedes(const Entry &a, const Entry &b) {
|
||||
if (a.category != b.category)
|
||||
return a.category < b.category;
|
||||
if (a.itemClass != b.itemClass)
|
||||
return a.itemClass < b.itemClass;
|
||||
const bool gear = a.category == kCatGear;
|
||||
if (gear && a.slotRank != b.slotRank)
|
||||
return a.slotRank < b.slotRank;
|
||||
if (a.itemClass != b.itemClass)
|
||||
return a.itemClass < b.itemClass;
|
||||
if (a.subClass != b.subClass)
|
||||
return a.subClass < b.subClass;
|
||||
if (gear && a.quality != b.quality)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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/>.
|
||||
|
||||
// `Constants.MoneyFormattingConstants`, retail's MoneyConstants system
|
||||
// (Blizzard_APIDocumentationGenerated/MoneyConstantsDocumentation.lua):
|
||||
// GOLD_REWARD_THRESHOLD_TO_HIDE_COPPER = 10, the gold amount above which
|
||||
// FormattingUtil's GetMoneyString(..., checkGoldThreshold) drops the copper.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace Currency::MoneyConstants {
|
||||
|
||||
namespace {
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kMoneyFormattingConstants[] = {
|
||||
{"GOLD_REWARD_THRESHOLD_TO_HIDE_COPPER", 10},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterConstants("MoneyFormattingConstants", kMoneyFormattingConstants,
|
||||
sizeof(kMoneyFormattingConstants) /
|
||||
sizeof(kMoneyFormattingConstants[0]),
|
||||
"MoneyConstants");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Currency::MoneyConstants
|
||||
@@ -0,0 +1,530 @@
|
||||
// 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_DurationUtil.CreateDuration()` and `Enum.DurationTimeModifier`
|
||||
// (DurationUtilDocumentation.lua, LuaDurationObjectAPIDocumentation.lua). The
|
||||
// object is C in retail, so what follows is retail's behavior as probed in a
|
||||
// 12.1.5 client (2026-11-01; the log is in docs/notes/duration-timer.md):
|
||||
//
|
||||
// - The object is a userdata, tostring "LuaDurationObject: <address>",
|
||||
// getmetatable false. A span is a start time, a total and a mod rate.
|
||||
// - Elapsed is now - start clamped to [0, total]; remaining is total -
|
||||
// elapsed, so before the start it is the whole total. The percents are
|
||||
// 0 to 1; a zero span is 100% elapsed, 0% remaining.
|
||||
// - HasStarted is now >= start, HasExpired now >= end, IsActive between
|
||||
// them. A fresh or Reset object (start 0, total 0) has started and
|
||||
// expired.
|
||||
// - SetTimeSpan with the end before the start keeps the start and a zero
|
||||
// total. SetTimeFromEnd sets the start to end - duration.
|
||||
// - modRate leaves every real-time answer alone. In base time the total is
|
||||
// total / modRate from the same start: 1 s into a 4 s span at mod rate 2
|
||||
// has 1 s left of a 2 s total.
|
||||
// - Copy is independent of the original; Reset does not reach a copy.
|
||||
//
|
||||
// Times are GetTime() seconds and the clock is GetTime()'s, so a span
|
||||
// straddling the 2^32 ms tick wrap jumps exactly as GetTime() does.
|
||||
//
|
||||
// The Format* methods hand the value to a Text::NumericFormatter. Not ported:
|
||||
// the Evaluate* methods (retail curve objects) and SetClock /
|
||||
// C_DurationUtil.CreateManualClock. 1.12 has no secret values, so
|
||||
// HasSecretValues is always false.
|
||||
|
||||
#include "duration/Object.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "text/NumericFormatter.h"
|
||||
#include "time/Clock.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <iterator>
|
||||
|
||||
namespace Duration::Object {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
struct Span {
|
||||
int refs = 0;
|
||||
double start = 0.0;
|
||||
double total = 0.0;
|
||||
double modRate = 1.0;
|
||||
};
|
||||
|
||||
void Retain(Span *s) { ++s->refs; }
|
||||
|
||||
void Release(Span *s) {
|
||||
if (s != nullptr && --s->refs == 0)
|
||||
delete s;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Enum.DurationTimeModifier.
|
||||
enum Modifier : int {
|
||||
MODIFIER_REAL_TIME = 0,
|
||||
MODIFIER_BASE_TIME = 1,
|
||||
};
|
||||
|
||||
constexpr const char *kMetatableKey = "ClassicAPI.LuaDurationObject";
|
||||
|
||||
void PushMetatable(void *L) {
|
||||
Lua::PushString(L, kMetatableKey);
|
||||
Lua::RawGet(L, Lua::REGISTRY_INDEX);
|
||||
}
|
||||
|
||||
int AbsIndex(void *L, int idx) { return idx > 0 ? idx : Lua::GetTop(L) + idx + 1; }
|
||||
|
||||
double Total(const Span &s, int m) { return m == MODIFIER_BASE_TIME ? s.total / s.modRate : s.total; }
|
||||
|
||||
double Elapsed(const Span &s, int m, double now) {
|
||||
const double e = now - s.start;
|
||||
const double total = Total(s, m);
|
||||
return e < 0.0 ? 0.0 : (e > total ? total : e);
|
||||
}
|
||||
|
||||
double ElapsedPct(const Span &s, int m, double now) {
|
||||
const double total = Total(s, m);
|
||||
return total > 0.0 ? Elapsed(s, m, now) / total : 1.0;
|
||||
}
|
||||
|
||||
double RemainingPct(const Span &s, int m, double now) {
|
||||
const double total = Total(s, m);
|
||||
return total > 0.0 ? (total - Elapsed(s, m, now)) / total : 0.0;
|
||||
}
|
||||
|
||||
void Set(Span &s, double start, double total, double modRate) {
|
||||
s.start = start;
|
||||
s.total = total > 0.0 ? total : 0.0;
|
||||
s.modRate = modRate;
|
||||
}
|
||||
|
||||
// ---- arguments ------------------------------------------------------------------
|
||||
|
||||
void Usage(void *L, int arg, const char *usage) { Lua::Error(L, "bad argument #%d to '?' (Usage: %s)", arg, usage); }
|
||||
|
||||
Span *Self(void *L, const char *usage) {
|
||||
Span *s = ToSpan(L, 1);
|
||||
if (s == nullptr)
|
||||
Usage(L, 1, usage);
|
||||
return s;
|
||||
}
|
||||
|
||||
// A finite number at `idx`; raises the usage error otherwise.
|
||||
bool Number(void *L, int idx, const char *usage, double &out) {
|
||||
out = Lua::Type(L, idx) == Lua::TYPE_NUMBER ? Lua::ToNumber(L, idx) : NAN;
|
||||
if (std::isfinite(out))
|
||||
return true;
|
||||
Usage(L, idx, usage);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The optional mod rate at `idx`: 1 when nil, else a positive number.
|
||||
bool ModRate(void *L, int idx, const char *usage, double &out) {
|
||||
out = 1.0;
|
||||
if (Lua::Type(L, idx) <= Lua::TYPE_NIL)
|
||||
return true;
|
||||
if (Number(L, idx, usage, out) && out > 0.0)
|
||||
return true;
|
||||
Usage(L, idx, usage);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The optional DurationTimeModifier at `idx`: RealTime when nil.
|
||||
bool ModifierArg(void *L, int idx, const char *usage, int &out) {
|
||||
if (ToModifier(L, idx, out))
|
||||
return true;
|
||||
Usage(L, idx, usage);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- the methods ----------------------------------------------------------------
|
||||
|
||||
// The getters that take a modifier: `self:Get...([modifier])`.
|
||||
template <typename Answer> int Query(void *L, const char *usage, Answer answer) {
|
||||
Span *s = Self(L, usage);
|
||||
int m;
|
||||
if (s == nullptr || !ModifierArg(L, 2, usage, m))
|
||||
return 0;
|
||||
answer(*s, m, Time::Clock::NowSeconds());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetStartTime(void *L) {
|
||||
return Query(L, "local startTime = self:GetStartTime([modifier])",
|
||||
[L](const Span &s, int, double) { Lua::PushNumber(L, s.start); });
|
||||
}
|
||||
|
||||
int __fastcall Script_GetEndTime(void *L) {
|
||||
return Query(L, "local endTime = self:GetEndTime([modifier])",
|
||||
[L](const Span &s, int m, double) { Lua::PushNumber(L, s.start + Total(s, m)); });
|
||||
}
|
||||
|
||||
int __fastcall Script_GetTotalDuration(void *L) {
|
||||
return Query(L, "local totalDuration = self:GetTotalDuration([modifier])",
|
||||
[L](const Span &s, int m, double) { Lua::PushNumber(L, Total(s, m)); });
|
||||
}
|
||||
|
||||
int __fastcall Script_GetElapsedDuration(void *L) {
|
||||
return Query(L, "local elapsedDuration = self:GetElapsedDuration([modifier])",
|
||||
[L](const Span &s, int m, double now) { Lua::PushNumber(L, Elapsed(s, m, now)); });
|
||||
}
|
||||
|
||||
int __fastcall Script_GetRemainingDuration(void *L) {
|
||||
return Query(L, "local remainingDuration = self:GetRemainingDuration([modifier])",
|
||||
[L](const Span &s, int m, double now) { Lua::PushNumber(L, Total(s, m) - Elapsed(s, m, now)); });
|
||||
}
|
||||
|
||||
int __fastcall Script_GetElapsedPercent(void *L) {
|
||||
return Query(L, "local elapsedPercent = self:GetElapsedPercent([modifier])",
|
||||
[L](const Span &s, int m, double now) { Lua::PushNumber(L, ElapsedPct(s, m, now)); });
|
||||
}
|
||||
|
||||
int __fastcall Script_GetRemainingPercent(void *L) {
|
||||
return Query(L, "local remainingPercent = self:GetRemainingPercent([modifier])",
|
||||
[L](const Span &s, int m, double now) { Lua::PushNumber(L, RemainingPct(s, m, now)); });
|
||||
}
|
||||
|
||||
int __fastcall Script_HasStarted(void *L) {
|
||||
return Query(L, "local hasStarted = self:HasStarted([modifier])",
|
||||
[L](const Span &s, int, double now) { Lua::PushBool(L, now >= s.start); });
|
||||
}
|
||||
|
||||
int __fastcall Script_HasExpired(void *L) {
|
||||
return Query(L, "local hasExpired = self:HasExpired([modifier])",
|
||||
[L](const Span &s, int m, double now) { Lua::PushBool(L, now >= s.start + Total(s, m)); });
|
||||
}
|
||||
|
||||
int __fastcall Script_IsActive(void *L) {
|
||||
return Query(L, "local isActive = self:IsActive([modifier])", [L](const Span &s, int m, double now) {
|
||||
Lua::PushBool(L, now >= s.start && now < s.start + Total(s, m));
|
||||
});
|
||||
}
|
||||
|
||||
// `self:Format...Duration(formatter [, modifier])`: the formatter's text for
|
||||
// the value, or its format error, raised as FormatNumber raises it.
|
||||
int Format(void *L, const char *usage, const char *method, int property) {
|
||||
Span *s = Self(L, usage);
|
||||
if (s == nullptr)
|
||||
return 0;
|
||||
Text::NumericFormatter::Formatter *f = Text::NumericFormatter::ToFormatter(L, 2);
|
||||
int m;
|
||||
if (f == nullptr) {
|
||||
Usage(L, 2, usage);
|
||||
return 0;
|
||||
}
|
||||
if (!ModifierArg(L, 3, usage, m))
|
||||
return 0;
|
||||
if (Text::NumericFormatter::Format(L, f, Sample(s, property, m)) == nullptr) {
|
||||
const char *msg = Lua::ToString(L, -1);
|
||||
Lua::Error(L, "LuaDurationObject:%s(): %s", method, msg != nullptr ? msg : "?");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_FormatElapsedDuration(void *L) {
|
||||
return Format(L, "local formatted = self:FormatElapsedDuration(formatter [, modifier])",
|
||||
"FormatElapsedDuration", PROPERTY_ELAPSED_DURATION);
|
||||
}
|
||||
|
||||
int __fastcall Script_FormatRemainingDuration(void *L) {
|
||||
return Format(L, "local formatted = self:FormatRemainingDuration(formatter [, modifier])",
|
||||
"FormatRemainingDuration", PROPERTY_REMAINING_DURATION);
|
||||
}
|
||||
|
||||
int __fastcall Script_FormatTotalDuration(void *L) {
|
||||
return Format(L, "local formatted = self:FormatTotalDuration(formatter [, modifier])",
|
||||
"FormatTotalDuration", PROPERTY_TOTAL_DURATION);
|
||||
}
|
||||
|
||||
int __fastcall Script_IsZero(void *L) {
|
||||
Span *s = Self(L, "local isZero = self:IsZero()");
|
||||
if (s == nullptr)
|
||||
return 0;
|
||||
Lua::PushBool(L, s->total == 0.0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetModRate(void *L) {
|
||||
Span *s = Self(L, "local modRate = self:GetModRate()");
|
||||
if (s == nullptr)
|
||||
return 0;
|
||||
Lua::PushNumber(L, s->modRate);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Nil: the default clock, GetTime().
|
||||
int __fastcall Script_GetClock(void *L) {
|
||||
if (Self(L, "local clock = self:GetClock()") == nullptr)
|
||||
return 0;
|
||||
Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetClockTime(void *L) {
|
||||
if (Self(L, "local clockTime = self:GetClockTime()") == nullptr)
|
||||
return 0;
|
||||
Lua::PushNumber(L, Time::Clock::NowSeconds());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_HasSecretValues(void *L) {
|
||||
if (Self(L, "local hasSecretValues = self:HasSecretValues()") == nullptr)
|
||||
return 0;
|
||||
Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetTimeFromStart(void *L) {
|
||||
constexpr const char *kUsage = "self:SetTimeFromStart(startTime, duration [, modRate])";
|
||||
Span *s = Self(L, kUsage);
|
||||
double start, duration, modRate;
|
||||
if (s == nullptr || !Number(L, 2, kUsage, start) || !Number(L, 3, kUsage, duration) ||
|
||||
!ModRate(L, 4, kUsage, modRate))
|
||||
return 0;
|
||||
Set(*s, start, duration, modRate);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetTimeFromEnd(void *L) {
|
||||
constexpr const char *kUsage = "self:SetTimeFromEnd(endTime, duration [, modRate])";
|
||||
Span *s = Self(L, kUsage);
|
||||
double end, duration, modRate;
|
||||
if (s == nullptr || !Number(L, 2, kUsage, end) || !Number(L, 3, kUsage, duration) ||
|
||||
!ModRate(L, 4, kUsage, modRate))
|
||||
return 0;
|
||||
if (duration < 0.0)
|
||||
duration = 0.0;
|
||||
Set(*s, end - duration, duration, modRate);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Takes no mod rate, so the span has none (not probed).
|
||||
int __fastcall Script_SetTimeSpan(void *L) {
|
||||
constexpr const char *kUsage = "self:SetTimeSpan(startTime, endTime)";
|
||||
Span *s = Self(L, kUsage);
|
||||
double start, end;
|
||||
if (s == nullptr || !Number(L, 2, kUsage, start) || !Number(L, 3, kUsage, end))
|
||||
return 0;
|
||||
Set(*s, start, end - start, 1.0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_Reset(void *L) {
|
||||
if (Span *s = Self(L, "self:Reset()"))
|
||||
Set(*s, 0.0, 0.0, 1.0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Reset plus the secret flag and the clock, neither of which 1.12 has.
|
||||
int __fastcall Script_SetToDefaults(void *L) {
|
||||
if (Span *s = Self(L, "self:SetToDefaults()"))
|
||||
Set(*s, 0.0, 0.0, 1.0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_Assign(void *L) {
|
||||
constexpr const char *kUsage = "self:Assign(other)";
|
||||
Span *s = Self(L, kUsage);
|
||||
if (s == nullptr)
|
||||
return 0;
|
||||
const Span *other = ToSpan(L, 2);
|
||||
if (other == nullptr) {
|
||||
Usage(L, 2, kUsage);
|
||||
return 0;
|
||||
}
|
||||
Set(*s, other->start, other->total, other->modRate);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_Copy(void *L) {
|
||||
Span *s = Self(L, "local copy = self:Copy()");
|
||||
if (s == nullptr)
|
||||
return 0;
|
||||
Span *copy = new Span;
|
||||
Set(*copy, s->start, s->total, s->modRate);
|
||||
Push(L, copy);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Meta_gc(void *L) {
|
||||
auto **block = static_cast<Span **>(Lua::ToUserdata(L, 1));
|
||||
if (block != nullptr) {
|
||||
Release(*block);
|
||||
*block = nullptr;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Meta_tostring(void *L) {
|
||||
char text[48];
|
||||
std::snprintf(text, sizeof(text), "LuaDurationObject: %p", static_cast<void *>(ToSpan(L, 1)));
|
||||
Lua::PushString(L, text);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_CreateDuration(void *L) {
|
||||
Push(L, new Span);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct Method {
|
||||
const char *name;
|
||||
Lua::CFunction fn;
|
||||
};
|
||||
const Method kMethods[] = {
|
||||
{"Assign", &Script_Assign},
|
||||
{"Copy", &Script_Copy},
|
||||
{"FormatElapsedDuration", &Script_FormatElapsedDuration},
|
||||
{"FormatRemainingDuration", &Script_FormatRemainingDuration},
|
||||
{"FormatTotalDuration", &Script_FormatTotalDuration},
|
||||
{"GetClock", &Script_GetClock},
|
||||
{"GetClockTime", &Script_GetClockTime},
|
||||
{"GetElapsedDuration", &Script_GetElapsedDuration},
|
||||
{"GetElapsedPercent", &Script_GetElapsedPercent},
|
||||
{"GetEndTime", &Script_GetEndTime},
|
||||
{"GetModRate", &Script_GetModRate},
|
||||
{"GetRemainingDuration", &Script_GetRemainingDuration},
|
||||
{"GetRemainingPercent", &Script_GetRemainingPercent},
|
||||
{"GetStartTime", &Script_GetStartTime},
|
||||
{"GetTotalDuration", &Script_GetTotalDuration},
|
||||
{"HasExpired", &Script_HasExpired},
|
||||
{"HasSecretValues", &Script_HasSecretValues},
|
||||
{"HasStarted", &Script_HasStarted},
|
||||
{"IsActive", &Script_IsActive},
|
||||
{"IsZero", &Script_IsZero},
|
||||
{"Reset", &Script_Reset},
|
||||
{"SetTimeFromEnd", &Script_SetTimeFromEnd},
|
||||
{"SetTimeFromStart", &Script_SetTimeFromStart},
|
||||
{"SetTimeSpan", &Script_SetTimeSpan},
|
||||
{"SetToDefaults", &Script_SetToDefaults},
|
||||
};
|
||||
|
||||
void SetFunction(void *L, const char *key, Lua::CFunction fn) {
|
||||
Lua::PushString(L, key);
|
||||
Lua::PushCClosure(L, fn, 0);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// registry[kMetatableKey] = {__index = methods, __gc, __tostring, __metatable = false}
|
||||
void CreateMetatable(void *L) {
|
||||
PushMetatable(L);
|
||||
const bool exists = Lua::Type(L, -1) == Lua::TYPE_TABLE;
|
||||
Lua::SetTop(L, -2);
|
||||
if (exists)
|
||||
return;
|
||||
Lua::PushString(L, kMetatableKey);
|
||||
Lua::NewTable(L);
|
||||
Lua::PushString(L, "__index");
|
||||
Lua::NewTable(L);
|
||||
for (const Method &m : kMethods)
|
||||
SetFunction(L, m.name, m.fn);
|
||||
Lua::SetTable(L, -3);
|
||||
SetFunction(L, "__gc", &Meta_gc);
|
||||
SetFunction(L, "__tostring", &Meta_tostring);
|
||||
Lua::PushString(L, "__metatable");
|
||||
Lua::PushBool(L, false);
|
||||
Lua::SetTable(L, -3);
|
||||
Lua::RawSet(L, Lua::REGISTRY_INDEX);
|
||||
}
|
||||
|
||||
const Lua::EnumIntegerEntry kModifiers[] = {
|
||||
{"RealTime", MODIFIER_REAL_TIME},
|
||||
{"BaseTime", MODIFIER_BASE_TIME},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kCreateRets[] = {
|
||||
Game::Doc::Req("duration", "LuaDurationObject"),
|
||||
};
|
||||
const Game::Doc::Function kCreateDuration{"Creates a zero duration object that can hold a time span.", {},
|
||||
kCreateRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
CreateMetatable(Lua::State());
|
||||
Lua::RegisterTableFunction("C_DurationUtil", "CreateDuration", &Script_CreateDuration, &kCreateDuration);
|
||||
Lua::RegisterIntegerEnum("Enum", "DurationTimeModifier", kModifiers, static_cast<int>(std::size(kModifiers)),
|
||||
"LuaDurationObjectShared");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
Span *ToSpan(void *L, int idx) {
|
||||
if (Lua::Type(L, idx) != Lua::TYPE_USERDATA)
|
||||
return nullptr;
|
||||
idx = AbsIndex(L, idx);
|
||||
if (!Lua::GetMetatable(L, idx))
|
||||
return nullptr;
|
||||
PushMetatable(L);
|
||||
const bool ours = Lua::ToPointer(L, -1) == Lua::ToPointer(L, -2);
|
||||
Lua::SetTop(L, -3);
|
||||
return ours ? *static_cast<Span **>(Lua::ToUserdata(L, idx)) : nullptr;
|
||||
}
|
||||
|
||||
void Push(void *L, Span *s) {
|
||||
auto **block = static_cast<Span **>(Lua::NewUserdata(L, sizeof(Span *)));
|
||||
*block = s;
|
||||
Retain(s);
|
||||
PushMetatable(L);
|
||||
Lua::SetMetatable(L, -2);
|
||||
}
|
||||
|
||||
void PushFromStart(void *L, double startSec, double totalSec) {
|
||||
Span *s = new Span;
|
||||
Set(*s, startSec, totalSec, 1.0);
|
||||
Push(L, s);
|
||||
}
|
||||
|
||||
double ElapsedPercent(const Span *s) { return ElapsedPct(*s, MODIFIER_REAL_TIME, Time::Clock::NowSeconds()); }
|
||||
|
||||
double RemainingPercent(const Span *s) { return RemainingPct(*s, MODIFIER_REAL_TIME, Time::Clock::NowSeconds()); }
|
||||
|
||||
double Sample(const Span *s, int property, int modifier) {
|
||||
const double now = Time::Clock::NowSeconds();
|
||||
switch (property) {
|
||||
case PROPERTY_REMAINING_DURATION:
|
||||
return Total(*s, modifier) - Elapsed(*s, modifier, now);
|
||||
case PROPERTY_REMAINING_PERCENT:
|
||||
return RemainingPct(*s, modifier, now);
|
||||
case PROPERTY_ELAPSED_DURATION:
|
||||
return Elapsed(*s, modifier, now);
|
||||
case PROPERTY_ELAPSED_PERCENT:
|
||||
return ElapsedPct(*s, modifier, now);
|
||||
case PROPERTY_TOTAL_DURATION:
|
||||
return Total(*s, modifier);
|
||||
case PROPERTY_START_TIME:
|
||||
return s->start;
|
||||
default:
|
||||
return s->start + Total(*s, modifier);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsZero(const Span *s) { return s->total == 0.0; }
|
||||
|
||||
bool HasExpired(const Span *s, int modifier) {
|
||||
return Time::Clock::NowSeconds() >= s->start + Total(*s, modifier);
|
||||
}
|
||||
|
||||
bool ToModifier(void *L, int idx, int &out) {
|
||||
out = MODIFIER_REAL_TIME;
|
||||
if (Lua::Type(L, idx) <= Lua::TYPE_NIL)
|
||||
return true;
|
||||
const double n = Lua::Type(L, idx) == Lua::TYPE_NUMBER ? Lua::ToNumber(L, idx) : NAN;
|
||||
if (n != MODIFIER_REAL_TIME && n != MODIFIER_BASE_TIME)
|
||||
return false;
|
||||
out = static_cast<int>(n);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Duration::Object
|
||||
@@ -0,0 +1,66 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// Retail's LuaDurationObject, as the engine-side users take it
|
||||
// (StatusBar:SetTimerDuration, UnitCastingDuration). A span is shared, not
|
||||
// copied: a retail bar follows a change made to the object after it was
|
||||
// handed over, and each getter call returns a new Lua object for it (retail
|
||||
// probe 2026-11-01). So the span is reference-counted, and every Lua object
|
||||
// and every holder owns one reference.
|
||||
namespace Duration::Object {
|
||||
|
||||
struct Span;
|
||||
|
||||
void Retain(Span *s);
|
||||
// Null-safe.
|
||||
void Release(Span *s);
|
||||
|
||||
// The span of the duration object at `idx`, or null when the value is not one.
|
||||
Span *ToSpan(void *L, int idx);
|
||||
|
||||
// Pushes a new Lua object for `s`.
|
||||
void Push(void *L, Span *s);
|
||||
|
||||
// Pushes a new duration object for `totalSec` from `startSec`, both in
|
||||
// GetTime() seconds.
|
||||
void PushFromStart(void *L, double startSec, double totalSec);
|
||||
|
||||
// The share of the span gone, or left, now: GetElapsedPercent /
|
||||
// GetRemainingPercent in real time, 0 to 1.
|
||||
double ElapsedPercent(const Span *s);
|
||||
double RemainingPercent(const Span *s);
|
||||
|
||||
// Enum.DurationTextBindingProperty: the values Sample reads.
|
||||
enum Property : int {
|
||||
PROPERTY_REMAINING_DURATION = 0,
|
||||
PROPERTY_REMAINING_PERCENT = 1,
|
||||
PROPERTY_ELAPSED_DURATION = 2,
|
||||
PROPERTY_ELAPSED_PERCENT = 3,
|
||||
PROPERTY_TOTAL_DURATION = 4,
|
||||
PROPERTY_START_TIME = 5,
|
||||
PROPERTY_END_TIME = 6,
|
||||
};
|
||||
|
||||
// `s`'s `property` now, in time `modifier` (Enum.DurationTimeModifier), as
|
||||
// the matching Get... method answers it.
|
||||
double Sample(const Span *s, int property, int modifier);
|
||||
bool IsZero(const Span *s);
|
||||
bool HasExpired(const Span *s, int modifier);
|
||||
|
||||
// The DurationTimeModifier at `idx`, RealTime for nil; false when the value
|
||||
// is neither.
|
||||
bool ToModifier(void *L, int idx, int &out);
|
||||
|
||||
} // namespace Duration::Object
|
||||
@@ -0,0 +1,791 @@
|
||||
// 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_DurationUtil.CreateDurationTextBinding()` and
|
||||
// `Enum.DurationTextBindingProperty` (DurationTextBindingObjectAPIDocumentation.lua,
|
||||
// DurationTextBindingSharedDocumentation.lua). A binding keeps a font string's
|
||||
// text set from a duration object. Retail's behavior, probed in a 12.1.5
|
||||
// client (2026-11-01; the log is in docs/notes/duration-timer.md):
|
||||
//
|
||||
// - The object is a userdata, tostring "DurationTextBinding: <address>". A
|
||||
// new binding is disabled, updates every tick (interval 0), and has no
|
||||
// expired or zero text; CanFormatText and CanUpdateFontString are false.
|
||||
// - Without a formatter it writes nothing, even enabled: the text stays as
|
||||
// it was and GetFormattedText returns "".
|
||||
// - With SetFormatter, the text is the formatter's text for the remaining
|
||||
// seconds, unrounded (2.949 with "%.1f" -> "2.9"), written at once on
|
||||
// enable and then every tick.
|
||||
// - The binding keeps the duration object, not a copy: a change to it shows
|
||||
// at the next tick. GetDuration returns a new Lua object for it.
|
||||
// - Once the duration has expired, the expired text shows; the binding
|
||||
// stays enabled.
|
||||
//
|
||||
// Not probed, and decided here: a zero (or missing) duration shows the zero
|
||||
// text when one is set, before the expired text; Assign copies everything
|
||||
// but the enabled state; Copy starts disabled; a "{}" in SetTextFormat with
|
||||
// no component writes nothing.
|
||||
//
|
||||
// An enabled binding updates with no Lua reference to it (retail's aura
|
||||
// button keeps one binding because a replaced, still-enabled one would go on
|
||||
// writing), so enabling takes a reference and disabling drops it. Updates
|
||||
// run at the head of each UI frame (Tick::FrameTick::AutoSubscribeBefore),
|
||||
// before its OnUpdate pass and draw, so the text shows in the same frame as a
|
||||
// timer bar's fill from that pass; at the tail it would show a frame late.
|
||||
// They set the text only when it differs from the font string's own. Destroyed font strings are
|
||||
// dropped through Frame::RegionDestruction. Not ported: the color curves
|
||||
// (SetTextColorCurve, GetTextColorCurve, ClearTextColorCurve,
|
||||
// GetFormattedTextColor).
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "duration/Object.h"
|
||||
#include "frame/RegionDestruction.h"
|
||||
#include "text/NumericFormatter.h"
|
||||
#include "tick/FrameTick.h"
|
||||
#include "time/Clock.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Duration::TextBinding {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
namespace Fmt = Text::NumericFormatter;
|
||||
using Duration::Object::Span;
|
||||
|
||||
struct Component {
|
||||
int property = Duration::Object::PROPERTY_REMAINING_DURATION;
|
||||
Fmt::Formatter *formatter = nullptr; // one reference
|
||||
};
|
||||
|
||||
struct Binding {
|
||||
int refs = 0;
|
||||
bool enabled = false; // and so held by g_active
|
||||
void *fontString = nullptr;
|
||||
Span *duration = nullptr; // one reference
|
||||
Fmt::Formatter *formatter = nullptr; // SetFormatter; one reference
|
||||
bool hasTextFormat = false; // SetTextFormat
|
||||
std::string textFormat;
|
||||
std::vector<Component> components;
|
||||
bool hasExpiredText = false;
|
||||
std::string expiredText;
|
||||
bool hasZeroText = false;
|
||||
std::string zeroText;
|
||||
int modifier = 0;
|
||||
double interval = 0.0;
|
||||
double lastUpdate = -INFINITY;
|
||||
};
|
||||
|
||||
// Every live binding, for Frame::RegionDestruction; and the enabled ones,
|
||||
// each holding a reference.
|
||||
std::vector<Binding *> g_all;
|
||||
std::vector<Binding *> g_active;
|
||||
|
||||
void Retain(Binding *b) { ++b->refs; }
|
||||
|
||||
void ClearComponents(Binding &b) {
|
||||
for (Component &c : b.components)
|
||||
Fmt::Release(c.formatter);
|
||||
b.components.clear();
|
||||
}
|
||||
|
||||
// Drops every reference the binding holds and its settings.
|
||||
void Clear(Binding &b) {
|
||||
Duration::Object::Release(b.duration);
|
||||
b.duration = nullptr;
|
||||
Fmt::Release(b.formatter);
|
||||
b.formatter = nullptr;
|
||||
ClearComponents(b);
|
||||
b.hasTextFormat = false;
|
||||
b.textFormat.clear();
|
||||
b.fontString = nullptr;
|
||||
b.hasExpiredText = b.hasZeroText = false;
|
||||
b.expiredText.clear();
|
||||
b.zeroText.clear();
|
||||
b.modifier = 0;
|
||||
b.interval = 0.0;
|
||||
b.lastUpdate = -INFINITY;
|
||||
}
|
||||
|
||||
void Release(Binding *b) {
|
||||
if (b == nullptr || --b->refs != 0)
|
||||
return;
|
||||
Clear(*b);
|
||||
g_all.erase(std::find(g_all.begin(), g_all.end(), b));
|
||||
delete b;
|
||||
}
|
||||
|
||||
Binding *New() {
|
||||
Binding *b = new Binding;
|
||||
g_all.push_back(b);
|
||||
return b;
|
||||
}
|
||||
|
||||
bool CanFormat(const Binding &b) { return b.formatter != nullptr || b.hasTextFormat; }
|
||||
|
||||
bool CanUpdate(const Binding &b) { return CanFormat(b) && b.fontString != nullptr; }
|
||||
|
||||
double Sample(const Binding &b, int property) {
|
||||
return b.duration != nullptr ? Duration::Object::Sample(b.duration, property, b.modifier) : 0.0;
|
||||
}
|
||||
|
||||
// Appends the formatter's text for `value`; false on a format error, whose
|
||||
// message is then at the top of the stack.
|
||||
bool AppendFormatted(void *L, Fmt::Formatter *f, double value, std::string &out) {
|
||||
const char *text = Fmt::Format(L, f, value);
|
||||
if (text == nullptr)
|
||||
return false;
|
||||
out += text;
|
||||
Lua::SetTop(L, -2);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The binding's text. False on a format error, whose message is then pushed.
|
||||
bool Formatted(void *L, const Binding &b, std::string &out) {
|
||||
out.clear();
|
||||
if (!CanFormat(b))
|
||||
return true;
|
||||
const bool zero = b.duration == nullptr || Duration::Object::IsZero(b.duration);
|
||||
if (zero && b.hasZeroText) {
|
||||
out = b.zeroText;
|
||||
return true;
|
||||
}
|
||||
if (b.hasExpiredText && (b.duration == nullptr || Duration::Object::HasExpired(b.duration, b.modifier))) {
|
||||
out = b.expiredText;
|
||||
return true;
|
||||
}
|
||||
if (b.formatter != nullptr)
|
||||
return AppendFormatted(L, b.formatter, Sample(b, Duration::Object::PROPERTY_REMAINING_DURATION), out);
|
||||
size_t next = 0;
|
||||
for (size_t i = 0; i < b.textFormat.size(); ++i) {
|
||||
if (b.textFormat[i] == '{' && i + 1 < b.textFormat.size() && b.textFormat[i + 1] == '}') {
|
||||
if (next < b.components.size()) {
|
||||
const Component &c = b.components[next];
|
||||
if (!AppendFormatted(L, c.formatter, Sample(b, c.property), out))
|
||||
return false;
|
||||
}
|
||||
++next;
|
||||
++i;
|
||||
} else {
|
||||
out += b.textFormat[i];
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Writes the binding's text into its font string when it differs. A format
|
||||
// error writes nothing.
|
||||
void Update(void *L, Binding &b) {
|
||||
if (!CanUpdate(b))
|
||||
return;
|
||||
b.lastUpdate = Time::Clock::NowSeconds();
|
||||
const int top = Lua::GetTop(L);
|
||||
std::string text;
|
||||
const bool ok = Formatted(L, b, text);
|
||||
Lua::SetTop(L, top);
|
||||
if (!ok)
|
||||
return;
|
||||
const char *current = Game::Read<const char *>(b.fontString, Offsets::OFF_FONTSTRING_TEXT);
|
||||
if (current != nullptr && text == current)
|
||||
return;
|
||||
using SetText_t = void(__fastcall *)(void *fs, void *edx, const char *text, int expand);
|
||||
reinterpret_cast<SetText_t>(Offsets::FUN_FONTSTRING_SET_TEXT)(b.fontString, nullptr, text.c_str(), 1);
|
||||
}
|
||||
|
||||
void Disable(Binding *b) {
|
||||
if (!b->enabled)
|
||||
return;
|
||||
b->enabled = false;
|
||||
g_active.erase(std::find(g_active.begin(), g_active.end(), b));
|
||||
Release(b);
|
||||
}
|
||||
|
||||
void SetEnabled(void *L, Binding *b, bool enabled) {
|
||||
if (!enabled) {
|
||||
Disable(b);
|
||||
return;
|
||||
}
|
||||
if (b->enabled)
|
||||
return;
|
||||
b->enabled = true;
|
||||
Retain(b);
|
||||
g_active.push_back(b);
|
||||
Update(L, *b);
|
||||
}
|
||||
|
||||
// An enabled binding's own reference outlives the Lua state; drop it before
|
||||
// the state goes. Copy first: each Disable edits g_active.
|
||||
void PrepareForReload() {
|
||||
const std::vector<Binding *> active = g_active;
|
||||
for (Binding *b : active)
|
||||
Disable(b);
|
||||
}
|
||||
|
||||
const Game::ReloadAutoRegister _reload{&PrepareForReload};
|
||||
|
||||
void OnTick() {
|
||||
if (g_active.empty())
|
||||
return;
|
||||
void *L = Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const double now = Time::Clock::NowSeconds();
|
||||
// By index: a format allocation can run the GC, whose finalizers may
|
||||
// release (never free) an active binding.
|
||||
for (size_t i = 0; i < g_active.size(); ++i) {
|
||||
Binding &b = *g_active[i];
|
||||
if (b.interval > 0.0 && now - b.lastUpdate < b.interval)
|
||||
continue;
|
||||
Update(L, b);
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::FrameTick::AutoSubscribeBefore _tickSub{&OnTick};
|
||||
|
||||
void OnRegionDestroyed(void *region) {
|
||||
for (Binding *b : g_all)
|
||||
if (b->fontString == region)
|
||||
b->fontString = nullptr;
|
||||
}
|
||||
|
||||
const Frame::RegionDestruction::AutoSubscribe _destructionSub{&OnRegionDestroyed};
|
||||
|
||||
// ---- the Lua object -------------------------------------------------------------
|
||||
|
||||
constexpr const char *kMetatableKey = "ClassicAPI.DurationTextBinding";
|
||||
|
||||
void PushMetatable(void *L) {
|
||||
Lua::PushString(L, kMetatableKey);
|
||||
Lua::RawGet(L, Lua::REGISTRY_INDEX);
|
||||
}
|
||||
|
||||
int AbsIndex(void *L, int idx) { return idx > 0 ? idx : Lua::GetTop(L) + idx + 1; }
|
||||
|
||||
Binding *ToBinding(void *L, int idx) {
|
||||
if (Lua::Type(L, idx) != Lua::TYPE_USERDATA)
|
||||
return nullptr;
|
||||
idx = AbsIndex(L, idx);
|
||||
if (!Lua::GetMetatable(L, idx))
|
||||
return nullptr;
|
||||
PushMetatable(L);
|
||||
const bool ours = Lua::ToPointer(L, -1) == Lua::ToPointer(L, -2);
|
||||
Lua::SetTop(L, -3);
|
||||
return ours ? *static_cast<Binding **>(Lua::ToUserdata(L, idx)) : nullptr;
|
||||
}
|
||||
|
||||
void Push(void *L, Binding *b) {
|
||||
auto **block = static_cast<Binding **>(Lua::NewUserdata(L, sizeof(Binding *)));
|
||||
*block = b;
|
||||
Retain(b);
|
||||
PushMetatable(L);
|
||||
Lua::SetMetatable(L, -2);
|
||||
}
|
||||
|
||||
void Usage(void *L, int arg, const char *usage) { Lua::Error(L, "bad argument #%d to '?' (Usage: %s)", arg, usage); }
|
||||
|
||||
Binding *Self(void *L, const char *usage) {
|
||||
Binding *b = ToBinding(L, 1);
|
||||
if (b == nullptr)
|
||||
Usage(L, 1, usage);
|
||||
return b;
|
||||
}
|
||||
|
||||
// An optional string at `idx`: `has` false for nil.
|
||||
bool OptionalText(void *L, int idx, const char *usage, bool &has, std::string &out) {
|
||||
has = false;
|
||||
out.clear();
|
||||
if (Lua::Type(L, idx) <= Lua::TYPE_NIL)
|
||||
return true;
|
||||
if (!Lua::IsString(L, idx)) {
|
||||
Usage(L, idx, usage);
|
||||
return false;
|
||||
}
|
||||
has = true;
|
||||
out.assign(Lua::ToString(L, idx), Lua::StrLen(L, idx));
|
||||
return true;
|
||||
}
|
||||
|
||||
void PushOptionalText(void *L, bool has, const std::string &text) {
|
||||
if (has)
|
||||
Lua::PushLString(L, text.data(), static_cast<unsigned int>(text.size()));
|
||||
else
|
||||
Lua::PushNil(L);
|
||||
}
|
||||
|
||||
// Copies `from`'s settings into `to`, which keeps its enabled state.
|
||||
void CopySettings(Binding &to, const Binding &from) {
|
||||
if (&to == &from)
|
||||
return;
|
||||
Span *duration = from.duration;
|
||||
Fmt::Formatter *formatter = from.formatter;
|
||||
std::vector<Component> components = from.components;
|
||||
if (duration != nullptr)
|
||||
Duration::Object::Retain(duration);
|
||||
if (formatter != nullptr)
|
||||
Fmt::Retain(formatter);
|
||||
for (Component &c : components)
|
||||
Fmt::Retain(c.formatter);
|
||||
Clear(to);
|
||||
to.fontString = from.fontString;
|
||||
to.duration = duration;
|
||||
to.formatter = formatter;
|
||||
to.hasTextFormat = from.hasTextFormat;
|
||||
to.textFormat = from.textFormat;
|
||||
to.components = std::move(components);
|
||||
to.hasExpiredText = from.hasExpiredText;
|
||||
to.expiredText = from.expiredText;
|
||||
to.hasZeroText = from.hasZeroText;
|
||||
to.zeroText = from.zeroText;
|
||||
to.modifier = from.modifier;
|
||||
to.interval = from.interval;
|
||||
}
|
||||
|
||||
// ---- the methods ----------------------------------------------------------------
|
||||
|
||||
int __fastcall Script_Assign(void *L) {
|
||||
constexpr const char *kUsage = "self:Assign(other)";
|
||||
Binding *b = Self(L, kUsage);
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
const Binding *other = ToBinding(L, 2);
|
||||
if (other == nullptr) {
|
||||
Usage(L, 2, kUsage);
|
||||
return 0;
|
||||
}
|
||||
CopySettings(*b, *other);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_Copy(void *L) {
|
||||
Binding *b = Self(L, "local copy = self:Copy()");
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
Binding *copy = New();
|
||||
CopySettings(*copy, *b);
|
||||
Push(L, copy);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_CanFormatText(void *L) {
|
||||
Binding *b = Self(L, "local canFormatText = self:CanFormatText()");
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
Lua::PushBool(L, CanFormat(*b));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_CanUpdateFontString(void *L) {
|
||||
Binding *b = Self(L, "local canUpdateText = self:CanUpdateFontString()");
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
Lua::PushBool(L, CanUpdate(*b));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_Enable(void *L) {
|
||||
if (Binding *b = Self(L, "self:Enable()"))
|
||||
SetEnabled(L, b, true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_Disable(void *L) {
|
||||
if (Binding *b = Self(L, "self:Disable()"))
|
||||
SetEnabled(L, b, false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetEnabled(void *L) {
|
||||
if (Binding *b = Self(L, "self:SetEnabled(enabled)"))
|
||||
SetEnabled(L, b, Lua::ToFlag(L, 2));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsEnabled(void *L) {
|
||||
Binding *b = Self(L, "local enabled = self:IsEnabled()");
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
Lua::PushBool(L, b->enabled);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetDuration(void *L) {
|
||||
constexpr const char *kUsage = "self:SetDuration(duration)";
|
||||
Binding *b = Self(L, kUsage);
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
Span *duration = Duration::Object::ToSpan(L, 2);
|
||||
if (duration == nullptr) {
|
||||
Usage(L, 2, kUsage);
|
||||
return 0;
|
||||
}
|
||||
Duration::Object::Retain(duration);
|
||||
Duration::Object::Release(b->duration);
|
||||
b->duration = duration;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetDuration(void *L) {
|
||||
Binding *b = Self(L, "local duration = self:GetDuration()");
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
if (b->duration != nullptr)
|
||||
Duration::Object::Push(L, b->duration);
|
||||
else
|
||||
Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetFontString(void *L) {
|
||||
constexpr const char *kUsage = "self:SetFontString(fontString)";
|
||||
Binding *b = Self(L, kUsage);
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
void *fontString = Lua::ResolveFontString(L, 2, false);
|
||||
if (fontString == nullptr) {
|
||||
Usage(L, 2, kUsage);
|
||||
return 0;
|
||||
}
|
||||
b->fontString = fontString;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetFontString(void *L) {
|
||||
Binding *b = Self(L, "local fontString = self:GetFontString()");
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
if (b->fontString != nullptr)
|
||||
Lua::PushObject(L, b->fontString);
|
||||
else
|
||||
Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetFormatter(void *L) {
|
||||
constexpr const char *kUsage = "self:SetFormatter(formatter)";
|
||||
Binding *b = Self(L, kUsage);
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
Fmt::Formatter *f = Fmt::ToFormatter(L, 2);
|
||||
if (f == nullptr) {
|
||||
Usage(L, 2, kUsage);
|
||||
return 0;
|
||||
}
|
||||
Fmt::Retain(f);
|
||||
Fmt::Release(b->formatter);
|
||||
b->formatter = f;
|
||||
ClearComponents(*b);
|
||||
b->hasTextFormat = false;
|
||||
b->textFormat.clear();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The component at stack index `entry`: {property =
|
||||
// Enum.DurationTextBindingProperty, formatter = NumericFormatter}. Leaves the
|
||||
// stack as it was; false when the entry is not one.
|
||||
bool ReadComponent(void *L, int entry, Component &c) {
|
||||
if (Lua::Type(L, entry) != Lua::TYPE_TABLE)
|
||||
return false;
|
||||
Lua::PushString(L, "property");
|
||||
Lua::RawGet(L, entry);
|
||||
const double p = Lua::Type(L, -1) == Lua::TYPE_NUMBER ? Lua::ToNumber(L, -1) : NAN;
|
||||
Lua::PushString(L, "formatter");
|
||||
Lua::RawGet(L, entry);
|
||||
c.formatter = Fmt::ToFormatter(L, -1);
|
||||
Lua::SetTop(L, entry);
|
||||
if (!(p >= Duration::Object::PROPERTY_REMAINING_DURATION && p <= Duration::Object::PROPERTY_END_TIME) ||
|
||||
p != static_cast<int>(p) || c.formatter == nullptr)
|
||||
return false;
|
||||
c.property = static_cast<int>(p);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Replaces the binding's text format with `formatString` and the component
|
||||
// list at stack index `t`, all or nothing.
|
||||
bool ApplyTextFormat(void *L, Binding &b, int formatString, int t) {
|
||||
std::vector<Component> components;
|
||||
for (int i = 1;; ++i) {
|
||||
Lua::RawGetI(L, t, i);
|
||||
const int entry = Lua::GetTop(L);
|
||||
Component c;
|
||||
const bool end = Lua::Type(L, entry) == Lua::TYPE_NIL;
|
||||
const bool ok = !end && ReadComponent(L, entry, c);
|
||||
Lua::SetTop(L, entry - 1);
|
||||
if (end)
|
||||
break;
|
||||
if (!ok) {
|
||||
for (Component &read : components)
|
||||
Fmt::Release(read.formatter);
|
||||
return false;
|
||||
}
|
||||
Fmt::Retain(c.formatter);
|
||||
components.push_back(c);
|
||||
}
|
||||
Fmt::Release(b.formatter);
|
||||
b.formatter = nullptr;
|
||||
ClearComponents(b);
|
||||
b.components = std::move(components);
|
||||
b.textFormat.assign(Lua::ToString(L, formatString), Lua::StrLen(L, formatString));
|
||||
b.hasTextFormat = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// `components` holds one entry for each "{}" in order.
|
||||
int __fastcall Script_SetTextFormat(void *L) {
|
||||
constexpr const char *kUsage = "self:SetTextFormat(formatString, components)";
|
||||
Binding *b = Self(L, kUsage);
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
if (!Lua::IsString(L, 2)) {
|
||||
Usage(L, 2, kUsage);
|
||||
return 0;
|
||||
}
|
||||
if (Lua::Type(L, 3) != Lua::TYPE_TABLE || !ApplyTextFormat(L, *b, 2, 3))
|
||||
Usage(L, 3, kUsage);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetExpiredText(void *L) {
|
||||
constexpr const char *kUsage = "self:SetExpiredText([text])";
|
||||
if (Binding *b = Self(L, kUsage))
|
||||
OptionalText(L, 2, kUsage, b->hasExpiredText, b->expiredText);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetExpiredText(void *L) {
|
||||
Binding *b = Self(L, "local text = self:GetExpiredText()");
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
PushOptionalText(L, b->hasExpiredText, b->expiredText);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetZeroDurationText(void *L) {
|
||||
constexpr const char *kUsage = "self:SetZeroDurationText([text])";
|
||||
if (Binding *b = Self(L, kUsage))
|
||||
OptionalText(L, 2, kUsage, b->hasZeroText, b->zeroText);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetZeroDurationText(void *L) {
|
||||
Binding *b = Self(L, "local text = self:GetZeroDurationText()");
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
PushOptionalText(L, b->hasZeroText, b->zeroText);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetTimeModifier(void *L) {
|
||||
constexpr const char *kUsage = "self:SetTimeModifier(modifier)";
|
||||
Binding *b = Self(L, kUsage);
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
int m;
|
||||
if (Lua::Type(L, 2) <= Lua::TYPE_NIL || !Duration::Object::ToModifier(L, 2, m)) {
|
||||
Usage(L, 2, kUsage);
|
||||
return 0;
|
||||
}
|
||||
b->modifier = m;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetTimeModifier(void *L) {
|
||||
Binding *b = Self(L, "local modifier = self:GetTimeModifier()");
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
Lua::PushNumber(L, b->modifier);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetUpdateInterval(void *L) {
|
||||
constexpr const char *kUsage = "self:SetUpdateInterval(updateInterval)";
|
||||
Binding *b = Self(L, kUsage);
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
const double n = Lua::Type(L, 2) == Lua::TYPE_NUMBER ? Lua::ToNumber(L, 2) : NAN;
|
||||
if (!(n >= 0.0) || !std::isfinite(n)) {
|
||||
Usage(L, 2, kUsage);
|
||||
return 0;
|
||||
}
|
||||
b->interval = n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetUpdateInterval(void *L) {
|
||||
Binding *b = Self(L, "local updateInterval = self:GetUpdateInterval()");
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
Lua::PushNumber(L, b->interval);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetFormattedText(void *L) {
|
||||
Binding *b = Self(L, "local text = self:GetFormattedText()");
|
||||
if (b == nullptr)
|
||||
return 0;
|
||||
bool ok;
|
||||
{ // the string is gone before lua_error, which unwinds no C++ frame
|
||||
std::string text;
|
||||
ok = Formatted(L, *b, text);
|
||||
if (ok)
|
||||
Lua::PushLString(L, text.data(), static_cast<unsigned int>(text.size()));
|
||||
}
|
||||
if (!ok) {
|
||||
const char *msg = Lua::ToString(L, -1); // the format error, on the stack
|
||||
Lua::Error(L, "DurationTextBinding:GetFormattedText(): %s", msg != nullptr ? msg : "?");
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_UpdateFontString(void *L) {
|
||||
if (Binding *b = Self(L, "self:UpdateFontString()"))
|
||||
Update(L, *b);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clears everything and disables the binding.
|
||||
int __fastcall Script_SetToDefaults(void *L) {
|
||||
if (Binding *b = Self(L, "self:SetToDefaults()")) {
|
||||
Retain(b); // SetEnabled(false) may drop the last reference but ours
|
||||
SetEnabled(L, b, false);
|
||||
Clear(*b);
|
||||
Release(b);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_HasSecretValues(void *L) {
|
||||
if (Self(L, "local hasSecretValues = self:HasSecretValues()") == nullptr)
|
||||
return 0;
|
||||
Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Meta_gc(void *L) {
|
||||
auto **block = static_cast<Binding **>(Lua::ToUserdata(L, 1));
|
||||
if (block != nullptr) {
|
||||
Release(*block);
|
||||
*block = nullptr;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Meta_tostring(void *L) {
|
||||
char text[48];
|
||||
std::snprintf(text, sizeof(text), "DurationTextBinding: %p", static_cast<void *>(ToBinding(L, 1)));
|
||||
Lua::PushString(L, text);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_CreateDurationTextBinding(void *L) {
|
||||
Push(L, New());
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct Method {
|
||||
const char *name;
|
||||
Lua::CFunction fn;
|
||||
};
|
||||
const Method kMethods[] = {
|
||||
{"Assign", &Script_Assign},
|
||||
{"CanFormatText", &Script_CanFormatText},
|
||||
{"CanUpdateFontString", &Script_CanUpdateFontString},
|
||||
{"Copy", &Script_Copy},
|
||||
{"Disable", &Script_Disable},
|
||||
{"Enable", &Script_Enable},
|
||||
{"GetDuration", &Script_GetDuration},
|
||||
{"GetExpiredText", &Script_GetExpiredText},
|
||||
{"GetFontString", &Script_GetFontString},
|
||||
{"GetFormattedText", &Script_GetFormattedText},
|
||||
{"GetTimeModifier", &Script_GetTimeModifier},
|
||||
{"GetUpdateInterval", &Script_GetUpdateInterval},
|
||||
{"GetZeroDurationText", &Script_GetZeroDurationText},
|
||||
{"HasSecretValues", &Script_HasSecretValues},
|
||||
{"IsEnabled", &Script_IsEnabled},
|
||||
{"SetDuration", &Script_SetDuration},
|
||||
{"SetEnabled", &Script_SetEnabled},
|
||||
{"SetExpiredText", &Script_SetExpiredText},
|
||||
{"SetFontString", &Script_SetFontString},
|
||||
{"SetFormatter", &Script_SetFormatter},
|
||||
{"SetTextFormat", &Script_SetTextFormat},
|
||||
{"SetTimeModifier", &Script_SetTimeModifier},
|
||||
{"SetToDefaults", &Script_SetToDefaults},
|
||||
{"SetUpdateInterval", &Script_SetUpdateInterval},
|
||||
{"SetZeroDurationText", &Script_SetZeroDurationText},
|
||||
{"UpdateFontString", &Script_UpdateFontString},
|
||||
};
|
||||
|
||||
void SetFunction(void *L, const char *key, Lua::CFunction fn) {
|
||||
Lua::PushString(L, key);
|
||||
Lua::PushCClosure(L, fn, 0);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// registry[kMetatableKey] = {__index = methods, __gc, __tostring, __metatable = false}
|
||||
void CreateMetatable(void *L) {
|
||||
PushMetatable(L);
|
||||
const bool exists = Lua::Type(L, -1) == Lua::TYPE_TABLE;
|
||||
Lua::SetTop(L, -2);
|
||||
if (exists)
|
||||
return;
|
||||
Lua::PushString(L, kMetatableKey);
|
||||
Lua::NewTable(L);
|
||||
Lua::PushString(L, "__index");
|
||||
Lua::NewTable(L);
|
||||
for (const Method &m : kMethods)
|
||||
SetFunction(L, m.name, m.fn);
|
||||
Lua::SetTable(L, -3);
|
||||
SetFunction(L, "__gc", &Meta_gc);
|
||||
SetFunction(L, "__tostring", &Meta_tostring);
|
||||
Lua::PushString(L, "__metatable");
|
||||
Lua::PushBool(L, false);
|
||||
Lua::SetTable(L, -3);
|
||||
Lua::RawSet(L, Lua::REGISTRY_INDEX);
|
||||
}
|
||||
|
||||
const Lua::EnumIntegerEntry kProperties[] = {
|
||||
{"RemainingDuration", Duration::Object::PROPERTY_REMAINING_DURATION},
|
||||
{"RemainingPercent", Duration::Object::PROPERTY_REMAINING_PERCENT},
|
||||
{"ElapsedDuration", Duration::Object::PROPERTY_ELAPSED_DURATION},
|
||||
{"ElapsedPercent", Duration::Object::PROPERTY_ELAPSED_PERCENT},
|
||||
{"TotalDuration", Duration::Object::PROPERTY_TOTAL_DURATION},
|
||||
{"StartTime", Duration::Object::PROPERTY_START_TIME},
|
||||
{"EndTime", Duration::Object::PROPERTY_END_TIME},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kCreateRets[] = {
|
||||
Game::Doc::Req("binding", "DurationTextBinding"),
|
||||
};
|
||||
const Game::Doc::Function kCreateDurationTextBinding{
|
||||
"Creates a binding that keeps a font string's text set from a duration object.", {}, kCreateRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
CreateMetatable(Lua::State());
|
||||
Lua::RegisterTableFunction("C_DurationUtil", "CreateDurationTextBinding", &Script_CreateDurationTextBinding,
|
||||
&kCreateDurationTextBinding);
|
||||
Lua::RegisterIntegerEnum("Enum", "DurationTextBindingProperty", kProperties,
|
||||
static_cast<int>(std::size(kProperties)), "DurationTextBindingShared");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Duration::TextBinding
|
||||
@@ -28,11 +28,13 @@
|
||||
// return addresses, get the new count. The cursor stays where it was.
|
||||
//
|
||||
// XML sets it with the EditBox attribute `countInvisibleLetters="true"`, read
|
||||
// after the engine's own EditBox loader and parsed the way it parses
|
||||
// `autoFocus`.
|
||||
// after the engine's own EditBox loader (Xml::TypeLoad) and parsed the way it
|
||||
// parses `autoFocus`.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "xml/FrameLoad.h"
|
||||
#include "xml/TypeLoad.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <intrin.h> // _ReturnAddress
|
||||
@@ -84,29 +86,17 @@ int __fastcall CountChars_h(void *editBox, void * /*edx*/, int start, int count)
|
||||
const Game::HookAutoRegister _countHook{Offsets::FUN_EDITBOX_COUNT_CHARS, reinterpret_cast<void *>(&CountChars_h),
|
||||
reinterpret_cast<void **>(&g_origCountChars)};
|
||||
|
||||
using Load_t = uintptr_t(__thiscall *)(void *self, const void *node, void *status);
|
||||
using GetAttr_t = const char *(__thiscall *)(const void *node, const char *name);
|
||||
Load_t g_origEditBoxLoad = nullptr;
|
||||
|
||||
// `self` is the loader sub-object inside the edit box.
|
||||
uintptr_t __fastcall EditBoxLoad_h(void *self, void * /*edx*/, const void *node, void *status) {
|
||||
const uintptr_t result = g_origEditBoxLoad(self, node, status);
|
||||
if (node == nullptr)
|
||||
return result;
|
||||
const char *value =
|
||||
reinterpret_cast<GetAttr_t>(Offsets::FUN_XML_NODE_GET_ATTRIBUTE)(node, "countInvisibleLetters");
|
||||
void OnEditBoxNode(void *object, const void *node, void * /*status*/) {
|
||||
const char *value = Xml::Attr(node, "countInvisibleLetters");
|
||||
if (value == nullptr || *value == '\0')
|
||||
return result;
|
||||
void *editBox = static_cast<uint8_t *>(self) - Offsets::OFF_XML_LOADER_SUBOBJECT;
|
||||
return;
|
||||
if (Game::StringToFlag(value))
|
||||
g_counting.insert(editBox);
|
||||
g_counting.insert(object);
|
||||
else
|
||||
g_counting.erase(editBox);
|
||||
return result;
|
||||
g_counting.erase(object);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _loadHook{Offsets::FUN_XML_EDITBOX_LOAD, reinterpret_cast<void *>(&EditBoxLoad_h),
|
||||
reinterpret_cast<void **>(&g_origEditBoxLoad)};
|
||||
const Xml::TypeLoad::AutoSubscribe _xmlSub{Xml::TypeLoad::EDIT_BOX, &OnEditBoxNode};
|
||||
|
||||
int __fastcall Script_SetCountInvisibleLetters(void *L) {
|
||||
void *editBox = Game::Lua::ResolveEditBox(L);
|
||||
|
||||
@@ -22,13 +22,18 @@ Util\SoundKitConstants.lua
|
||||
Util\TableUtil.lua
|
||||
Util\EnumUtil.lua
|
||||
Util\StringUtil.lua
|
||||
Util\TypeUtil.lua
|
||||
Util\MathUtil.lua
|
||||
Util\SortUtil.lua
|
||||
Util\Spline.lua
|
||||
Util\PixelUtil.lua
|
||||
Util\FrameUtil.lua
|
||||
Util\InputUtil.lua
|
||||
Util\FormattingUtil.lua
|
||||
Util\TimeUtil.lua
|
||||
Util\Flags.lua
|
||||
Util\TaggableObject.lua
|
||||
Util\ExportUtil.lua
|
||||
Util\ButtonStateBehavior.lua
|
||||
Util\CallbackRegistry.lua
|
||||
Util\GlobalCallbackRegistry.lua
|
||||
@@ -36,22 +41,38 @@ Util\TemplateInfoCache.lua
|
||||
Util\FrameFactory.lua
|
||||
Util\DataProvider.lua
|
||||
Util\TreeListDataProvider.lua
|
||||
Util\IndexRangeDataProvider.lua
|
||||
Util\CircularBuffer.lua
|
||||
Util\ColorUtil.lua
|
||||
Util\SharedColorConstants.lua
|
||||
Util\ItemLocation.lua
|
||||
Util\PlayerLocation.lua
|
||||
Util\EventUtil.lua
|
||||
Util\ContinuableContainer.lua
|
||||
Util\AsyncCallbackSystem.lua
|
||||
Util\Item.lua
|
||||
Util\Spell.lua
|
||||
Util\ObjectCache.lua
|
||||
Util\SpellBookItemLocation.lua
|
||||
Util\ItemUtil.lua
|
||||
Util\ContainerFrame.lua
|
||||
Util\AddOnCompat.lua
|
||||
Util\FunctionUtil.lua
|
||||
Util\MixinUtil.lua
|
||||
Util\ObjectUpdater.lua
|
||||
Util\CacheUtil.lua
|
||||
Util\Pools.lua
|
||||
Util\SecureTypes.lua
|
||||
Util\ProxyUtil.lua
|
||||
Util\Pools.lua
|
||||
Util\TableBuilder.lua
|
||||
Util\FrameWatcher.lua
|
||||
Util\TimedCallback.lua
|
||||
Util\EasingUtil.lua
|
||||
Util\ScriptAnimationUtil.lua
|
||||
Util\Interpolator.lua
|
||||
Util\Sequencer.lua
|
||||
Util\SmoothStatusBar.lua
|
||||
Util\AnimationSystem.lua
|
||||
Util\AnchorUtil.lua
|
||||
Util\RegionUtil.lua
|
||||
Util\GridLayoutUtil.lua
|
||||
@@ -89,6 +110,8 @@ FrameXML\EventEditBox.lua
|
||||
FrameXML\EventEditBox.xml
|
||||
FrameXML\EventScrollFrame.lua
|
||||
FrameXML\EventScrollFrame.xml
|
||||
FrameXML\CallbackRegistrant.lua
|
||||
FrameXML\CallbackRegistrant.xml
|
||||
FrameXML\Scroll\ScrollUtil.lua
|
||||
FrameXML\Scroll\ScrollController.lua
|
||||
FrameXML\Scroll\ScrollBoxPadding.lua
|
||||
@@ -111,9 +134,15 @@ FrameXML\Scroll\TrimScrollBar.lua
|
||||
FrameXML\Scroll\TrimScrollBar.xml
|
||||
FrameXML\LayoutFrame.lua
|
||||
FrameXML\LayoutFrame.xml
|
||||
FrameXML\AnimationTemplates.xml
|
||||
FrameXML\SharedTooltipTemplates.xml
|
||||
FrameXML\SharedUIPanelTemplates.xml
|
||||
FrameXML\InputBoxTemplates.xml
|
||||
FrameXML\SearchBoxTemplate.xml [ExcludeLoadGameType turtle]
|
||||
FrameXML\ColorSwatch.lua
|
||||
FrameXML\ColorSwatch.xml
|
||||
FrameXML\ScriptErrorsFrame.lua
|
||||
FrameXML\ScriptErrorsFrame.xml
|
||||
|
||||
Menu\MenuConstants.lua
|
||||
Menu\Classic\MenuConstants.lua
|
||||
|
||||
@@ -1,3 +1,26 @@
|
||||
if not _G then
|
||||
_G = getfenv(0)
|
||||
end
|
||||
end
|
||||
|
||||
-- Retail names for what this client already has. Retail's Deprecated files
|
||||
-- alias the old globals to the C_ functions; here the old globals are the
|
||||
-- implementations.
|
||||
C_Container.GetContainerNumSlots = GetContainerNumSlots
|
||||
C_Container.PickupContainerItem = PickupContainerItem
|
||||
|
||||
C_ActionBar = C_ActionBar or {}
|
||||
|
||||
-- Retail answers per game type. This client always has a keyring.
|
||||
function C_ActionBar.ShouldShowKeyring()
|
||||
return true
|
||||
end
|
||||
|
||||
-- C_AutoComplete's names come from the DLL. This client has no Battle.net
|
||||
-- friends to give a presence ID for, and no other realm a name can address.
|
||||
function C_AutoComplete.GetAutoCompletePresenceID(name)
|
||||
return nil
|
||||
end
|
||||
|
||||
function C_AutoComplete.GetAutoCompleteRealms()
|
||||
return {}
|
||||
end
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
-- Backport of one mixin from retail's Blizzard_SharedXML/AnimationTemplates.lua.
|
||||
|
||||
-- Anim group whose parent is hidden or shown based on script calls defined in the template xml being used
|
||||
VisibleWhilePlayingAnimGroupMixin = {}
|
||||
|
||||
function VisibleWhilePlayingAnimGroupMixin:Show()
|
||||
self:GetParent():Show();
|
||||
end
|
||||
|
||||
function VisibleWhilePlayingAnimGroupMixin:Hide()
|
||||
self:GetParent():Hide();
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
|
||||
<!-- One template from retail's Blizzard_SharedXML/AnimationTemplates.xml. -->
|
||||
<Script file="AnimationTemplates.lua"/>
|
||||
|
||||
<!-- The anim group's parent is visible while the group is playing, hidden otherwise -->
|
||||
<AnimationGroup name="VisibleWhilePlayingAnimGroupTemplate" mixin="VisibleWhilePlayingAnimGroupMixin" virtual="true">
|
||||
<Scripts>
|
||||
<OnLoad method="Hide"/>
|
||||
<OnPlay method="Show"/>
|
||||
<OnStop method="Hide"/>
|
||||
<OnFinished method="Hide"/>
|
||||
</Scripts>
|
||||
</AnimationGroup>
|
||||
</Ui>
|
||||
@@ -0,0 +1,87 @@
|
||||
-- Backport of retail's Blizzard_SharedXMLBase/CallbackRegistrant.lua.
|
||||
|
||||
CallbackRegistrantMixin = {};
|
||||
|
||||
function CallbackRegistrantMixin:OnShow()
|
||||
for i, eventRegistrationInfo in ipairs(self:GetDynamicCallbackRegistrantHandlers()) do
|
||||
self:RegisterFromRegistrationInfo(eventRegistrationInfo);
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:OnHide()
|
||||
self:UnregisterAllInternal(self:GetDynamicCallbackRegistrantHandlers());
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:AddEventMethodInternal(handlersTable, callbackRegistry, event, handlerMethod)
|
||||
local eventRegistrationInfo = self:CreateEventRegistrationInfo(callbackRegistry, event, handlerMethod)
|
||||
table.insert(handlersTable, eventRegistrationInfo);
|
||||
return eventRegistrationInfo;
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:AddDynamicEventMethod(callbackRegistry, event, handlerMethod)
|
||||
local eventRegistrationInfo = self:AddEventMethodInternal(self:GetDynamicCallbackRegistrantHandlers(), callbackRegistry, event, handlerMethod)
|
||||
|
||||
if self:IsShown() then
|
||||
self:RegisterFromRegistrationInfo(eventRegistrationInfo);
|
||||
end
|
||||
end
|
||||
|
||||
-- Routing calls here allows for future expansion in this template (i.e. checking duplicate registrations).
|
||||
function CallbackRegistrantMixin:AddStaticEventMethod(callbackRegistry, event, handlerMethod)
|
||||
local eventRegistrationInfo = self:AddEventMethodInternal(self:GetStaticCallbackRegistrantHandlers(), callbackRegistry, event, handlerMethod)
|
||||
self:RegisterFromRegistrationInfo(eventRegistrationInfo);
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:RemoveStaticEventMethod(callbackRegistry, event, handlerMethod)
|
||||
local handlers = self:GetStaticCallbackRegistrantHandlers();
|
||||
for i, eventRegistrationInfo in ipairs(handlers) do
|
||||
if (eventRegistrationInfo.callbackRegistry == callbackRegistry) and (eventRegistrationInfo.event == event) then
|
||||
self:UnregisterFromRegistrationInfo(eventRegistrationInfo);
|
||||
table.remove(handlers, i);
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:UnregisterAllEventMethods()
|
||||
self:UnregisterAllInternal(self:GetDynamicCallbackRegistrantHandlers());
|
||||
self:UnregisterAllInternal(self:GetStaticCallbackRegistrantHandlers());
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:CreateEventRegistrationInfo(callbackRegistry, event, handlerMethod)
|
||||
return { callbackRegistry = callbackRegistry, event = event, handlerMethod = handlerMethod, registered = false, };
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:RegisterFromRegistrationInfo(eventRegistrationInfo)
|
||||
if eventRegistrationInfo.registered then
|
||||
return;
|
||||
end
|
||||
|
||||
eventRegistrationInfo.callbackRegistry:RegisterCallback(eventRegistrationInfo.event, eventRegistrationInfo.handlerMethod, self);
|
||||
eventRegistrationInfo.registered = true;
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:UnregisterFromRegistrationInfo(eventRegistrationInfo)
|
||||
if not eventRegistrationInfo.registered then
|
||||
return;
|
||||
end
|
||||
|
||||
eventRegistrationInfo.callbackRegistry:UnregisterCallback(eventRegistrationInfo.event, self);
|
||||
eventRegistrationInfo.registered = false;
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:UnregisterAllInternal(handlersTable)
|
||||
for i, eventRegistrationInfo in ipairs(handlersTable) do
|
||||
self:UnregisterFromRegistrationInfo(eventRegistrationInfo);
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:GetDynamicCallbackRegistrantHandlers()
|
||||
self.callbackRegistrantHandlers = self.callbackRegistrantHandlers or {};
|
||||
return self.callbackRegistrantHandlers;
|
||||
end
|
||||
|
||||
function CallbackRegistrantMixin:GetStaticCallbackRegistrantHandlers()
|
||||
self.staticCallbackRegistrantHandlers = self.staticCallbackRegistrantHandlers or {};
|
||||
return self.staticCallbackRegistrantHandlers;
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\..\..\..\..\WoW\Data\Interface\AddOns\Blizzard_SharedXML\UI.xsd">
|
||||
<Frame name="CallbackRegistrantTemplate" mixin="CallbackRegistrantMixin" virtual="true">
|
||||
<Scripts>
|
||||
<OnShow method="OnShow"/>
|
||||
<OnHide method="OnHide"/>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,115 @@
|
||||
-- Backport of the instructions and search box parts of retail's
|
||||
-- Blizzard_SharedXML/Shared/InputBox/InputBoxTemplates.lua.
|
||||
--
|
||||
-- Implementation differences from the retail source:
|
||||
-- - No narration: ClearButtonMixin has no NarrationGetName.
|
||||
|
||||
function EditBox_ClearFocus(self)
|
||||
self:ClearFocus();
|
||||
end
|
||||
|
||||
function InputBoxInstructions_OnTextChanged(self)
|
||||
self.Instructions:SetShown(self:GetText() == "");
|
||||
end
|
||||
|
||||
function InputBoxInstructions_UpdateColorForEnabledState(self, color)
|
||||
if color then
|
||||
self:SetTextColor(color:GetRGBA());
|
||||
end
|
||||
end
|
||||
|
||||
function InputBoxInstructions_OnDisable(self)
|
||||
InputBoxInstructions_UpdateColorForEnabledState(self, self.disabledColor);
|
||||
end
|
||||
|
||||
function InputBoxInstructions_OnEnable(self)
|
||||
InputBoxInstructions_UpdateColorForEnabledState(self, self.enabledColor);
|
||||
end
|
||||
|
||||
function InputBoxInstructions_OnEnter(self)
|
||||
if self.showTooltipForTruncatedInstructions then
|
||||
InputBoxInstructions_ShowTooltipIfTruncated(self);
|
||||
end
|
||||
end
|
||||
|
||||
function InputBoxInstructions_OnLeave(self)
|
||||
if self.showTooltipForTruncatedInstructions then
|
||||
-- Note: Some inheritors of this template may have a special version of GetAppropriateTooltip (Ex. CatalogShopMixin:GetAppropriateTooltip)
|
||||
-- If this functionality is desired then this probably needs to be overridden
|
||||
GetAppropriateTooltip():Hide();
|
||||
end
|
||||
end
|
||||
|
||||
function InputBoxInstructions_ShowTooltipIfTruncated(self)
|
||||
if not self.Instructions:IsShown() or not self.Instructions:IsTruncated() then
|
||||
return;
|
||||
end
|
||||
|
||||
local tooltip = GetAppropriateTooltip();
|
||||
tooltip:SetOwner(self, "ANCHOR_RIGHT");
|
||||
local text = self.Instructions:GetText();
|
||||
GameTooltip_AddHighlightLine(tooltip, text);
|
||||
tooltip:Show();
|
||||
end
|
||||
|
||||
function SearchBoxTemplate_OnLoad(self)
|
||||
self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
|
||||
self.Instructions:SetText(self.instructionText);
|
||||
end
|
||||
|
||||
function SearchBoxTemplate_OnEditFocusLost(self)
|
||||
if ( self:GetText() == "" ) then
|
||||
self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
|
||||
self.clearButton:Hide();
|
||||
end
|
||||
end
|
||||
|
||||
function SearchBoxTemplate_OnEditFocusGained(self)
|
||||
self.searchIcon:SetVertexColor(1.0, 1.0, 1.0);
|
||||
self.clearButton:Show();
|
||||
end
|
||||
|
||||
function SearchBoxTemplate_OnTextChanged(self)
|
||||
if ( not self:HasFocus() and self:GetText() == "" ) then
|
||||
self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
|
||||
self.clearButton:Hide();
|
||||
else
|
||||
self.searchIcon:SetVertexColor(1.0, 1.0, 1.0);
|
||||
self.clearButton:Show();
|
||||
end
|
||||
InputBoxInstructions_OnTextChanged(self);
|
||||
end
|
||||
|
||||
function SearchBoxTemplate_ClearText(self)
|
||||
self:SetText("");
|
||||
self:ClearFocus();
|
||||
end
|
||||
|
||||
function SearchBoxTemplateClearButton_OnClick(self)
|
||||
PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON);
|
||||
SearchBoxTemplate_ClearText(self:GetParent());
|
||||
end
|
||||
|
||||
ClearButtonMixin = {};
|
||||
|
||||
function ClearButtonMixin:OnEnter()
|
||||
self.Icon:SetAlpha(1.0);
|
||||
end
|
||||
|
||||
function ClearButtonMixin:OnLeave()
|
||||
self.Icon:SetAlpha(0.5);
|
||||
end
|
||||
|
||||
function ClearButtonMixin:OnMouseDown()
|
||||
if self:IsEnabled() then
|
||||
self.Icon:SetPoint("TOPLEFT", self, "TOPLEFT", 4, -4);
|
||||
end
|
||||
end
|
||||
|
||||
function ClearButtonMixin:OnMouseUp()
|
||||
self.Icon:SetPoint("TOPLEFT", self, "TOPLEFT", 3, -3);
|
||||
end
|
||||
|
||||
function ClearButtonMixin:OnClick()
|
||||
SearchBoxTemplateClearButton_OnClick(self);
|
||||
end
|
||||
@@ -0,0 +1,97 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
|
||||
<!--
|
||||
Two templates from retail's Blizzard_SharedXML/Shared/InputBox/InputBoxTemplates.xml.
|
||||
- InputBoxTemplate is this client's own (1.12 FrameXML, same border art and
|
||||
geometry), so it is not redefined.
|
||||
- The search and clear icons are atlases this client does not have. The
|
||||
search icon is the world map's magnifying glass, cropped square. The
|
||||
clear icon is the cancel button art, cropped to the button.
|
||||
- SearchBoxTemplate has no narration mixin.
|
||||
- Turtle's FrameXML has its own SearchBoxTemplate (a different box, which
|
||||
its UI uses), and the client keeps the first template registered under a
|
||||
name. So the retail box is ClassicAPISearchBoxTemplate, which ClassicAPI's
|
||||
own UI inherits, and SearchBoxTemplate.xml names it SearchBoxTemplate on
|
||||
every client but Turtle.
|
||||
-->
|
||||
<Script file="InputBoxTemplates.lua"/>
|
||||
|
||||
<EditBox name="InputBoxInstructionsTemplate" inherits="InputBoxTemplate" virtual="true">
|
||||
<!--KeyValues>
|
||||
Optional
|
||||
<KeyValue key="disabledColor" value="GRAY_FONT_COLOR" type="global"/>
|
||||
<KeyValue key="enabledColor" value="HIGHLIGHT_FONT_COLOR" type="global"/>
|
||||
<KeyValue key="showTooltipForTruncatedInstructions" value="false" type="boolean"/>
|
||||
</KeyValues-->
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<FontString parentKey="Instructions" justifyH="LEFT" justifyV="MIDDLE" inherits="GameFontDisableSmall">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="16" y="0"/>
|
||||
<Anchor point="BOTTOMRIGHT" x="-20" y="0"/>
|
||||
</Anchors>
|
||||
<Color r="0.35" g="0.35" b="0.35"/>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnTextChanged function="InputBoxInstructions_OnTextChanged"/>
|
||||
<OnDisable function="InputBoxInstructions_OnDisable"/>
|
||||
<OnEnable function="InputBoxInstructions_OnEnable"/>
|
||||
<OnEnter function="InputBoxInstructions_OnEnter"/>
|
||||
<OnLeave function="InputBoxInstructions_OnLeave"/>
|
||||
</Scripts>
|
||||
<FontString inherits="GameFontHighlightSmall"/>
|
||||
</EditBox>
|
||||
|
||||
<EditBox name="ClassicAPISearchBoxTemplate" inherits="InputBoxInstructionsTemplate" autoFocus="false" virtual="true">
|
||||
<KeyValues>
|
||||
<KeyValue key="instructionText" value="SEARCH" type="global"/>
|
||||
</KeyValues>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture name="$parentSearchIcon" file="Interface\WorldMap\WorldMap-MagnifyingGlass" parentKey="searchIcon">
|
||||
<Size x="10" y="10"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" x="1" y="-1"/>
|
||||
</Anchors>
|
||||
<TexCoords left="0.09375" right="0.90625" top="0.03125" bottom="0.84375"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="$parentClearButton" parentKey="clearButton" mixin="ClearButtonMixin" hidden="true">
|
||||
<Size x="17" y="17"/>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" x="-3" y="0"/>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<Texture parentKey="Icon" file="Interface\Buttons\CancelButton-Up" alpha="0.5">
|
||||
<Size x="10" y="10"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="3" y="-3"/>
|
||||
</Anchors>
|
||||
<TexCoords left="0.21875" right="0.78125" top="0.21875" bottom="0.78125"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnEnter method="OnEnter" />
|
||||
<OnLeave method="OnLeave" />
|
||||
<OnMouseDown method="OnMouseDown" />
|
||||
<OnMouseUp method="OnMouseUp" />
|
||||
<OnClick method="OnClick"/>
|
||||
</Scripts>
|
||||
</Button>
|
||||
</Frames>
|
||||
<TextInsets left="16" right="20" bottom="0" top="0"/>
|
||||
<Scripts>
|
||||
<OnLoad function="SearchBoxTemplate_OnLoad"/>
|
||||
<OnEscapePressed function="EditBox_ClearFocus"/>
|
||||
<OnEnterPressed function="EditBox_ClearFocus"/>
|
||||
<OnEditFocusLost function="SearchBoxTemplate_OnEditFocusLost"/>
|
||||
<OnEditFocusGained function="SearchBoxTemplate_OnEditFocusGained"/>
|
||||
<OnTextChanged function="SearchBoxTemplate_OnTextChanged"/>
|
||||
</Scripts>
|
||||
</EditBox>
|
||||
</Ui>
|
||||
@@ -364,7 +364,7 @@ GameMenuFrame:HookScript("OnShow", function()
|
||||
menuButton:SetPoint(point, relativeTo, relativePoint, x, y)
|
||||
GameMenuButtonKeybindings:ClearAllPoints()
|
||||
GameMenuButtonKeybindings:SetPoint(point, menuButton, relativePoint, x, y)
|
||||
GameMenuFrame:SetHeight(GameMenuFrame:GetHeight() + menuButton:GetHeight() - y)
|
||||
GameMenuFrame:SetHeight(GameMenuFrame:GetUnscaledHeight() + menuButton:GetUnscaledHeight() - y)
|
||||
end)
|
||||
menuButton:SetScript("OnClick", function()
|
||||
PlaySound("igMainMenuOption")
|
||||
|
||||
@@ -173,7 +173,7 @@ end
|
||||
local sizeRoundingMultiple = 0.1;
|
||||
|
||||
function BaseLayoutMixin:GetChildSize(child, ignoreRect)
|
||||
local childWidth, childHeight = child:GetSize(ignoreRect);
|
||||
local childWidth, childHeight = child:GetUnscaledSize(ignoreRect);
|
||||
if self.usePreciseChildSizes then
|
||||
return childWidth, childHeight;
|
||||
else
|
||||
@@ -182,7 +182,7 @@ function BaseLayoutMixin:GetChildSize(child, ignoreRect)
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:GetChildWidth(child, ignoreRect)
|
||||
local childWidth = child:GetWidth(ignoreRect);
|
||||
local childWidth = child:GetUnscaledWidth(ignoreRect);
|
||||
if self.usePreciseChildSizes then
|
||||
return childWidth;
|
||||
else
|
||||
@@ -191,7 +191,7 @@ function BaseLayoutMixin:GetChildWidth(child, ignoreRect)
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:GetChildHeight(child, ignoreRect)
|
||||
local childHeight = child:GetHeight(ignoreRect);
|
||||
local childHeight = child:GetUnscaledHeight(ignoreRect);
|
||||
if self.usePreciseChildSizes then
|
||||
return childHeight;
|
||||
else
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/PortraitFrame.lua.
|
||||
--
|
||||
-- Implementation differences from the retail source:
|
||||
-- - The border is a backdrop (SharedUIPanelTemplates.xml), the same with
|
||||
-- and without the portrait, so SetBorder has no layout to apply.
|
||||
-- - No SetPortraitToClassIcon or SetPortraitToSpecIcon: this client has no
|
||||
-- class circles art (CLASS_ICON_TCOORDS) and no specializations.
|
||||
-- - No DefaultPanelMixin or PortraitFrameFlatBaseMixin: their templates are
|
||||
-- not ported.
|
||||
|
||||
TitledPanelMixin = {};
|
||||
|
||||
function TitledPanelMixin:GetTitleText()
|
||||
return self.TitleContainer.TitleText;
|
||||
end
|
||||
|
||||
function TitledPanelMixin:SetTitleColor(color)
|
||||
self:GetTitleText():SetTextColor(color:GetRGBA());
|
||||
end
|
||||
|
||||
function TitledPanelMixin:SetTitle(title)
|
||||
self:GetTitleText():SetText(title);
|
||||
end
|
||||
|
||||
function TitledPanelMixin:SetTitleFormatted(fmt, ...)
|
||||
self:GetTitleText():SetFormattedText(fmt, ...);
|
||||
end
|
||||
|
||||
function TitledPanelMixin:SetTitleMaxLinesAndHeight(maxLines, height)
|
||||
self:GetTitleText():SetMaxLines(maxLines);
|
||||
self:GetTitleText():SetHeight(height);
|
||||
end
|
||||
|
||||
function TitledPanelMixin:SetTitleOffsets(leftOffset, rightOffset)
|
||||
self.TitleContainer:SetPoint("TOPLEFT", self, "TOPLEFT", leftOffset or 58, -1);
|
||||
self.TitleContainer:SetPoint("TOPRIGHT", self, "TOPRIGHT", rightOffset or -24, -1);
|
||||
end
|
||||
|
||||
PortraitFrameMixin = CreateFromMixins(TitledPanelMixin);
|
||||
|
||||
function PortraitFrameMixin:GetPortrait()
|
||||
return self.PortraitContainer.portrait;
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:HasPortraitTexture()
|
||||
return self.PortraitContainer.portrait:GetTexture();
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetBorder(layoutName)
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitToAsset(texture)
|
||||
self:GetPortrait():SetTexture(texture);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitToUnit(unit)
|
||||
SetPortraitTexture(self:GetPortrait(), unit);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitToBag(bagID)
|
||||
C_Container.SetBagPortraitTexture(self:GetPortrait(), bagID);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitTextureRaw(texture)
|
||||
self:GetPortrait():SetTexture(texture);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitAtlasRaw(atlas, ...)
|
||||
self:GetPortrait():SetAtlas(atlas, ...);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitTexCoord(...)
|
||||
self:GetPortrait():SetTexCoord(...);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitShown(shown)
|
||||
self:GetPortrait():SetShown(shown);
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetPortraitTextureSizeAndOffset(size, offsetX, offsetY)
|
||||
local portrait = self:GetPortrait();
|
||||
portrait:SetSize(size, size);
|
||||
portrait:SetPoint("TOPLEFT", self, "TOPLEFT", offsetX, offsetY);
|
||||
end
|
||||
|
||||
do
|
||||
local function SetFrameLevelInternal(frame, level)
|
||||
if frame then
|
||||
assertsafe(level < 10000, "Base level needs to be low enough to accomodate the existing layering"); -- TODO: Should become a constant
|
||||
frame:SetFrameLevel(level);
|
||||
end
|
||||
end
|
||||
|
||||
function PortraitFrameMixin:SetFrameLevelsFromBaseLevel(baseLevel)
|
||||
SetFrameLevelInternal(self.NineSlice, baseLevel + 500);
|
||||
SetFrameLevelInternal(self.PortraitContainer, baseLevel + 400);
|
||||
SetFrameLevelInternal(self.TitleContainer, baseLevel + 510);
|
||||
SetFrameLevelInternal(self.CloseButton, baseLevel + 510);
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,147 @@
|
||||
-- Backport of retail's Blizzard_ScriptErrorsFrame to vanilla 1.12 / Lua 5.0:
|
||||
-- the window that shows each Lua error with its time, count and stack, and
|
||||
-- pages through them. Retail always loads it; it lives here, not in the
|
||||
-- load-on-demand ClassicAPI_DebugTools, so that it is there for the first
|
||||
-- error.
|
||||
--
|
||||
-- How errors get here: 1.12's error handler (_ERRORMESSAGE, FrameXML\
|
||||
-- BasicControls.xml) reports each one as
|
||||
-- ScriptErrors_Message:SetText(message); ScriptErrors:Show();
|
||||
-- The frame's OnLoad takes those two slots, the same two !BugGrabber takes,
|
||||
-- so the stock box never shows and this window gets the error instead:
|
||||
-- - An addon that loads later and takes the slots again (!BugGrabber, which
|
||||
-- BugSack reads) replaces this window.
|
||||
-- - An addon that calls seterrorhandler replaces _ERRORMESSAGE, so neither
|
||||
-- shows; one that chains to the old handler (geterrorhandler) ends here.
|
||||
-- The stock box stays hidden, so the handler's IsVisible gate always passes
|
||||
-- and every error arrives. message() calls _ERRORMESSAGE, so its text shows
|
||||
-- here too.
|
||||
--
|
||||
-- Implementation differences from the retail source:
|
||||
-- - Global functions in the 3.3.5 style, not retail's
|
||||
-- ScriptErrorsFrameMixin.
|
||||
-- - The text is a ScrollingEditBoxTemplate with a MinimalScrollBar, the
|
||||
-- ScrollBox pair retail uses for scrolling edit boxes, not retail's
|
||||
-- ScrollFrameTemplate, which ClassicAPI does not port.
|
||||
-- - The stack is retail's GetErrorData without the locals, taken in the
|
||||
-- SetText slot: GetErrorCallstackHeight finds the code that failed
|
||||
-- however many handlers sit between it and the slot. 1.12 has no
|
||||
-- debuglocals.
|
||||
-- - No scriptErrors cvar (1.12 always shows errors), no LUA_WARNING, no
|
||||
-- gamepad legend.
|
||||
-- - Lua 5.0: no `...` expression, no # operator.
|
||||
|
||||
local function GetErrorStack()
|
||||
-- Example of how debug stack level is calculated
|
||||
-- Current stack: [1, 2, 3, 4, 5] (current function is at 1, total current height is 5)
|
||||
-- Stack at time of error: [1, 2] (these are currently now index 4 and 5, but at the time of error the stack height is 2)
|
||||
-- To calcuate the level to debug (4): curentStackHeight - (errorStackHeight - 1) = 5 - (2 - 1) = 4
|
||||
local currentStackHeight = GetCallstackHeight();
|
||||
local errorCallStackHeight = GetErrorCallstackHeight();
|
||||
local errorStackOffset = errorCallStackHeight and (errorCallStackHeight - 1);
|
||||
local debugStackLevel = currentStackHeight - (errorStackOffset or 0);
|
||||
return debugstack(debugStackLevel);
|
||||
end
|
||||
|
||||
local ERROR_FORMAT = "|cffffd200Message:|r|cffffffff %s|r\n"
|
||||
.. "|cffffd200Time:|r|cffffffff %s|r\n"
|
||||
.. "|cffffd200Count:|r|cffffffff %s|r\n"
|
||||
.. "|cffffd200Stack:|r\n"
|
||||
.. "|cffffffff%s|r";
|
||||
|
||||
local INDEX_ORDER_FORMAT = "%d / %d";
|
||||
|
||||
function ScriptErrorsFrame_OnLoad(self)
|
||||
self.index = 0;
|
||||
self.errorData = {};
|
||||
self.seen = {};
|
||||
|
||||
-- No default text: with none set, an empty window would apply nil.
|
||||
self.ScrollingEditBox:SetDefaultTextEnabled(false);
|
||||
self.ScrollingEditBox:RegisterCallback("OnFocusGained", function(_, editBox)
|
||||
editBox:HighlightText(0);
|
||||
end, self);
|
||||
|
||||
-- Retail turns the wheel on with the OnMouseWheel script; 1.12 does not.
|
||||
local scrollBox = self.ScrollingEditBox:GetScrollBox();
|
||||
scrollBox:EnableMouseWheel(true);
|
||||
ScrollUtil.RegisterScrollBoxWithScrollBar(scrollBox, self.ScrollBar);
|
||||
|
||||
ScriptErrors_Message.SetText = function(_, message)
|
||||
ScriptErrorsFrame_OnError(message, GetErrorStack());
|
||||
end
|
||||
ScriptErrors.Show = function() end
|
||||
end
|
||||
|
||||
function ScriptErrorsFrame_OnError(message, stack)
|
||||
local self = ScriptErrorsFrame;
|
||||
message = tostring(message);
|
||||
stack = stack or "";
|
||||
|
||||
local messageKey = message .. "\n" .. stack;
|
||||
local index = self.seen[messageKey];
|
||||
if ( index ) then
|
||||
local errorData = self.errorData[index];
|
||||
errorData.count = errorData.count + 1;
|
||||
else
|
||||
table.insert(self.errorData, { count = 1, message = message, time = date(), stack = stack });
|
||||
index = table.getn(self.errorData);
|
||||
self.seen[messageKey] = index;
|
||||
end
|
||||
|
||||
if ( not self:IsShown() ) then
|
||||
self.index = index;
|
||||
self:Show();
|
||||
else
|
||||
ScriptErrorsFrame_Update();
|
||||
end
|
||||
end
|
||||
|
||||
function ScriptErrorsFrame_Update()
|
||||
local self = ScriptErrorsFrame;
|
||||
if ( not self.errorData[self.index] ) then
|
||||
self.index = table.getn(self.errorData);
|
||||
end
|
||||
|
||||
-- SetText puts the cursor and the scroll back at the top.
|
||||
local errorData = self.errorData[self.index];
|
||||
if ( errorData ) then
|
||||
self.ScrollingEditBox:SetText(string.format(ERROR_FORMAT, errorData.message, errorData.time, errorData.count, errorData.stack));
|
||||
self.ScrollingEditBox:GetEditBox():HighlightText(0);
|
||||
else
|
||||
self.ScrollingEditBox:ClearText();
|
||||
end
|
||||
|
||||
local numErrors = table.getn(self.errorData);
|
||||
ScriptErrorsFramePrevious:SetEnabled(numErrors > 1);
|
||||
ScriptErrorsFrameNext:SetEnabled(numErrors > 1);
|
||||
ScriptErrorsFrameIndexLabel:SetText(string.format(INDEX_ORDER_FORMAT, self.index, numErrors));
|
||||
end
|
||||
|
||||
-- Previous / Next wrap around, as on retail.
|
||||
function ScriptErrorsFrame_ChangeDisplayedIndex(delta)
|
||||
local self = ScriptErrorsFrame;
|
||||
local numErrors = table.getn(self.errorData);
|
||||
local index = self.index + delta;
|
||||
if ( index < 1 ) then
|
||||
index = numErrors;
|
||||
elseif ( index > numErrors ) then
|
||||
index = 1;
|
||||
end
|
||||
self.index = index;
|
||||
ScriptErrorsFrame_Update();
|
||||
end
|
||||
|
||||
function ScriptErrorsFrame_HandleSlashCmd(msg)
|
||||
local self = ScriptErrorsFrame;
|
||||
if ( string.lower(msg or "") == "clear" ) then
|
||||
self.index = 0;
|
||||
self.errorData = {};
|
||||
self.seen = {};
|
||||
ScriptErrorsFrame_Update();
|
||||
elseif ( self:IsShown() ) then
|
||||
self:Hide();
|
||||
else
|
||||
self:Show();
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,139 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\..\..\..\..\WoW\Data\Interface\AddOns\Blizzard_SharedXML\UI.xsd">
|
||||
<Frame name="ScriptErrorsFrame" parent="UIParent" movable="true" hidden="true" toplevel="true" frameStrata="DIALOG" enableMouse="true">
|
||||
<Size x="384" y="260"/>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
|
||||
<BackgroundInsets>
|
||||
<AbsInset left="11" right="12" top="12" bottom="11"/>
|
||||
</BackgroundInsets>
|
||||
<TileSize>
|
||||
<AbsValue val="32"/>
|
||||
</TileSize>
|
||||
<EdgeSize>
|
||||
<AbsValue val="32"/>
|
||||
</EdgeSize>
|
||||
</Backdrop>
|
||||
<Layers>
|
||||
<!-- ARTWORK, not BACKGROUND: the Backdrop above draws its
|
||||
background and border into BACKGROUND, and it is applied
|
||||
after the textures declared here, so a header in that layer
|
||||
ends up behind the window border. Every Blizzard frame that
|
||||
uses UI-DialogBox-Header puts it in ARTWORK for this reason. -->
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="$parentTitleBG" file="Interface\DialogFrame\UI-DialogBox-Header">
|
||||
<Size x="256" y="64"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP">
|
||||
<Offset x="0" y="12"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
<FontString name="$parentTitle" inherits="GameFontNormal" text="LUA_ERROR">
|
||||
<Anchors>
|
||||
<Anchor point="TOP">
|
||||
<Offset x="0" y="-2"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentIndexLabel" inherits="GameFontNormal" justifyH="CENTER">
|
||||
<Size x="70" y="16"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM">
|
||||
<Offset x="0" y="20"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Frame name="$parentTitleButton">
|
||||
<Size x="256" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP"/>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this:RegisterForDrag("LeftButton");
|
||||
</OnLoad>
|
||||
<OnDragStart>ScriptErrorsFrame:StartMoving();</OnDragStart>
|
||||
<OnDragStop>ScriptErrorsFrame:StopMovingOrSizing();</OnDragStop>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<!-- The edit box and its scroll bar fill the interior inside the
|
||||
32px dialog border, 16 to 368, where the Close button ends:
|
||||
the bar is 25 wide and sits 4 to the right of the edit box.
|
||||
Retail's numbers do not carry over here: its ScriptErrorsFrame
|
||||
inherits UIPanelDialogTemplate, a thin GearManager border. -->
|
||||
<Frame parentKey="ScrollingEditBox" inherits="ScrollingEditBoxTemplate">
|
||||
<KeyValues>
|
||||
<KeyValue key="fontName" value="GameFontHighlightSmall" type="string"/>
|
||||
</KeyValues>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="16" y="-32"/>
|
||||
<Anchor point="BOTTOMRIGHT" x="-45" y="58"/>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
<EventFrame parentKey="ScrollBar" inherits="MinimalScrollBar">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeKey="$parent.ScrollingEditBox" relativePoint="TOPRIGHT" x="4" y="0"/>
|
||||
<Anchor point="BOTTOMLEFT" relativeKey="$parent.ScrollingEditBox" relativePoint="BOTTOMRIGHT" x="4" y="0"/>
|
||||
</Anchors>
|
||||
</EventFrame>
|
||||
<Button name="$parentReload" inherits="UIPanelButtonTemplate" text="RELOADUI">
|
||||
<Size x="96" y="24"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMLEFT">
|
||||
<Offset x="16" y="16"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>ReloadUI();</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<Button name="$parentPrevious">
|
||||
<Size x="32" y="32"/>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" relativeTo="$parentIndexLabel" relativePoint="LEFT"/>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>ScriptErrorsFrame_ChangeDisplayedIndex(-1);</OnClick>
|
||||
</Scripts>
|
||||
<NormalTexture file="Interface\Buttons\UI-SpellbookIcon-PrevPage-Up"/>
|
||||
<PushedTexture file="Interface\Buttons\UI-SpellbookIcon-PrevPage-Down"/>
|
||||
<DisabledTexture file="Interface\Buttons\UI-SpellbookIcon-PrevPage-Disabled"/>
|
||||
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
|
||||
</Button>
|
||||
<Button name="$parentNext">
|
||||
<Size x="32" y="32"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentIndexLabel" relativePoint="RIGHT"/>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>ScriptErrorsFrame_ChangeDisplayedIndex(1);</OnClick>
|
||||
</Scripts>
|
||||
<NormalTexture file="Interface\Buttons\UI-SpellbookIcon-NextPage-Up"/>
|
||||
<PushedTexture file="Interface\Buttons\UI-SpellbookIcon-NextPage-Down"/>
|
||||
<DisabledTexture file="Interface\Buttons\UI-SpellbookIcon-NextPage-Disabled"/>
|
||||
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
|
||||
</Button>
|
||||
<Button name="$parentClose" inherits="UIPanelButtonTemplate" text="CLOSE">
|
||||
<Size x="96" y="24"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMRIGHT">
|
||||
<Offset x="-16" y="16"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>ScriptErrorsFrame:Hide();</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>ScriptErrorsFrame_OnLoad(this);</OnLoad>
|
||||
<OnShow>ScriptErrorsFrame_Update();</OnShow>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -15,7 +15,7 @@ function ScrollBoxListGridViewMixin:GetStride()
|
||||
local strideExtent = self:GetStrideExtent();
|
||||
if strideExtent then
|
||||
local scrollTarget = self:GetScrollTarget();
|
||||
local extent = scrollTarget:GetWidth();
|
||||
local extent = scrollTarget:GetUnscaledWidth();
|
||||
local spacing = self:GetHorizontalSpacing();
|
||||
local stride = math.max(1, math.floor(extent / strideExtent));
|
||||
local extentWithSpacing = (stride * strideExtent) + ((stride-1) * spacing);
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollBoxListView.lua.
|
||||
-- Backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollBoxListView.lua.
|
||||
-- The one difference: wipe is ClassicAPI.wipe, so an addon's own global
|
||||
-- wipe cannot replace it.
|
||||
|
||||
local wipe = ClassicAPI.wipe;
|
||||
|
||||
local InvalidationReason = EnumUtil.MakeEnum("DataProviderReassigned", "DataProviderContentsChanged");
|
||||
|
||||
ScrollBoxListViewMixin = CreateFromMixins(ScrollBoxViewMixin, CallbackRegistryMixin);
|
||||
|
||||
@@ -8,7 +8,7 @@ ScrollBoxListSequenceViewMixin = CreateFromMixins(ScrollBoxListBiaxalViewMixin);
|
||||
-- after frame sizes have changed so we're avoiding ScrollTarget because calculating its width in an OnSizeChanged
|
||||
-- handler callstack is not reliable and we can end up getting bad values.
|
||||
function ScrollBoxListSequenceViewMixin:GetVisibleWidth(scrollBox)
|
||||
return scrollBox:GetWidth() - (scrollBox:GetLeftPadding() + scrollBox:GetRightPadding());
|
||||
return scrollBox:GetUnscaledWidth() - (scrollBox:GetLeftPadding() + scrollBox:GetRightPadding());
|
||||
end
|
||||
|
||||
function ScrollBoxListSequenceViewMixin:Layout(scrollBox)
|
||||
|
||||
@@ -38,13 +38,13 @@ function ScrollBoxViewUtil.SetPoint(frame, offset, indent, elementStretchDisable
|
||||
if not elementStretchDisabled then
|
||||
frame:SetPoint("BOTTOMLEFT", scrollTarget, "BOTTOMLEFT", offset, 0);
|
||||
end
|
||||
return frame:GetWidth();
|
||||
return frame:GetUnscaledWidth();
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", scrollTarget, "TOPLEFT", indent, -offset);
|
||||
if not elementStretchDisabled then
|
||||
frame:SetPoint("TOPRIGHT", scrollTarget, "TOPRIGHT", 0, -offset);
|
||||
end
|
||||
return frame:GetHeight();
|
||||
return frame:GetUnscaledHeight();
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ ScrollDirectionMixin = {};
|
||||
ScrollDirectionUtil = {};
|
||||
|
||||
function ScrollDirectionUtil.GetFrameExtent(frame, isHorizontal)
|
||||
local width, height = frame:GetSize();
|
||||
local width, height = frame:GetUnscaledSize();
|
||||
return isHorizontal and width or height;
|
||||
end
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ function ScrollingFontMixin:OnLoad()
|
||||
view:SetPanExtent(fontHeight);
|
||||
scrollBox:Init(view);
|
||||
|
||||
local width = scrollBox:GetWidth();
|
||||
local width = scrollBox:GetUnscaledWidth();
|
||||
local fontStringContainer = self:GetFontStringContainer();
|
||||
fontStringContainer:SetWidth(width);
|
||||
fontString:SetWidth(width);
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollUtil.lua.
|
||||
-- Backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollUtil.lua.
|
||||
-- The one difference: wipe is ClassicAPI.wipe, so an addon's own global
|
||||
-- wipe cannot replace it.
|
||||
|
||||
local wipe = ClassicAPI.wipe;
|
||||
|
||||
local function ContainsCursor(frame, cx, cy)
|
||||
return cy <= frame:GetTop() and cy >= frame:GetBottom()
|
||||
@@ -242,7 +246,7 @@ function ScrollUtil.InitScrollFrameWithScrollBar(scrollFrame, scrollBar)
|
||||
onVerticalScroll(scrollFrame, scrollFrame:GetVerticalScroll());
|
||||
|
||||
local visibleExtentPercentage = 0;
|
||||
local height = scrollFrame:GetHeight();
|
||||
local height = scrollFrame:GetUnscaledHeight();
|
||||
if height > 0 then
|
||||
visibleExtentPercentage = height / (vScrollRange + height);
|
||||
end
|
||||
@@ -648,7 +652,7 @@ ScrollBoxDragBehavior = {};
|
||||
local function GetRelativeCursorRange(frame, cy, dragBehavior)
|
||||
local insetBottom, insetTop = dragBehavior:GetCursorHitInsets();
|
||||
local bottom = frame:GetBottom() + insetBottom;
|
||||
local height = frame:GetHeight() - (insetBottom + insetTop);
|
||||
local height = frame:GetUnscaledHeight() - (insetBottom + insetTop);
|
||||
local relativeCursorY = cy - bottom;
|
||||
return relativeCursorY, height;
|
||||
end
|
||||
@@ -1136,7 +1140,7 @@ function ScrollBoxDragBehavior:Register(onDragStop, onDragUpdate)
|
||||
self.delegate:SetScript("OnUpdate", function(delegateFrame, elapsed)
|
||||
local cx, cy = InputUtil.GetCursorPosition(cursorParent);
|
||||
local x, y = cx - dx, cy - dy;
|
||||
self.cursorFrame:SetPoint("TOPLEFT", x, y - cursorParent:GetHeight());
|
||||
self.cursorFrame:SetPoint("TOPLEFT", x, y - cursorParent:GetUnscaledHeight());
|
||||
|
||||
self:TryVerticalEdgeScroll(elapsed, cx, cy);
|
||||
|
||||
@@ -1319,7 +1323,7 @@ function ScrollUtil.GenerateCursorFactory(scrollBox)
|
||||
local view = scrollBox:GetView();
|
||||
|
||||
local function CursorInitializer(cursorFrame, candidateFrame, elementData)
|
||||
cursorFrame:SetSize(candidateFrame:GetSize());
|
||||
cursorFrame:SetSize(candidateFrame:GetUnscaledSize());
|
||||
|
||||
-- Uses the candidate frame's initializer on the cursor frame.
|
||||
local template, initializer = view:GetFactoryDataFromElementData(elementData);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
|
||||
<!--
|
||||
Retail's SearchBoxTemplate (InputBoxTemplates.xml). The TOC loads this file
|
||||
everywhere but Turtle, whose FrameXML has its own SearchBoxTemplate.
|
||||
-->
|
||||
<EditBox name="SearchBoxTemplate" inherits="ClassicAPISearchBoxTemplate" virtual="true"/>
|
||||
</Ui>
|
||||
@@ -0,0 +1,10 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
|
||||
<!--
|
||||
One template from retail's Blizzard_SharedXML/SharedTooltipTemplates.xml.
|
||||
Retail draws it with a NineSlice border and sets it up in
|
||||
SharedTooltip_OnLoad. This client's tooltips already have their border,
|
||||
strata, clamp and set-up in 1.12's own GameTooltipTemplate, so the
|
||||
template is that one.
|
||||
-->
|
||||
<GameTooltip name="SharedTooltipTemplate" inherits="GameTooltipTemplate" virtual="true"/>
|
||||
</Ui>
|
||||
@@ -1,8 +1,16 @@
|
||||
-- Backport of two templates from retail's
|
||||
-- Backport of templates from retail's
|
||||
-- Blizzard_SharedXML/Mainline/SharedUIPanelTemplates.lua.
|
||||
--
|
||||
-- Difference from the retail source: PanelDragBarMixin:UpdateCursor does
|
||||
-- nothing. 1.12's cursor table has no UI_MOVE_CURSOR.
|
||||
-- Differences from the retail source:
|
||||
-- - 1.12's cursor table has no UI_MOVE_CURSOR or UI_RESIZE_CURSOR, so
|
||||
-- PanelDragBarMixin:UpdateCursor and PanelResizeButtonMixin's
|
||||
-- OnEnter / OnLeave do nothing.
|
||||
-- - The ButtonFrameTemplate border is the same with and without the
|
||||
-- portrait, so ButtonFrameTemplate_HidePortrait / ShowPortrait move only
|
||||
-- the title; the background and inset stay inside the border.
|
||||
-- - 1.12 FrameXML owns UIPanelCloseButton (its own OnClick), so
|
||||
-- UIPanelCloseButtonDefaultAnchors inherits UIPanelCloseButtonNoScripts
|
||||
-- and runs UIPanelCloseButton_OnClick itself.
|
||||
|
||||
function TruncatedTooltipScript_OnEnter(self)
|
||||
local text = self.truncatedTooltipScriptText or self.Text;
|
||||
@@ -112,3 +120,188 @@ end
|
||||
|
||||
function PanelDragBarMixin:UpdateCursor()
|
||||
end
|
||||
|
||||
local function ButtonFrameTemplate_UpdateAnchors(self, isPortraitMode)
|
||||
if self.TitleContainer then
|
||||
self.TitleContainer:SetPoint("TOPLEFT", self, "TOPLEFT", isPortraitMode and 58 or 0, -1);
|
||||
self.TitleContainer:SetPoint("TOPRIGHT", self, "TOPRIGHT", isPortraitMode and -24 or 0, -1);
|
||||
end
|
||||
end
|
||||
|
||||
function ButtonFrameTemplate_HidePortrait(self)
|
||||
self:SetBorder("ButtonFrameTemplateNoPortrait");
|
||||
self:SetPortraitShown(false);
|
||||
|
||||
local isPortraitMode = false;
|
||||
ButtonFrameTemplate_UpdateAnchors(self, isPortraitMode);
|
||||
end
|
||||
|
||||
function ButtonFrameTemplate_ShowPortrait(self)
|
||||
self:SetBorder("PortraitFrameTemplate");
|
||||
self:SetPortraitShown(true);
|
||||
|
||||
local isPortraitMode = true;
|
||||
ButtonFrameTemplate_UpdateAnchors(self, isPortraitMode);
|
||||
end
|
||||
|
||||
UIPanelCloseButtonDefaultAnchorsMixin = {};
|
||||
|
||||
function UIPanelCloseButtonDefaultAnchorsMixin:OnLoad()
|
||||
self:SetSize(24, 24);
|
||||
self:ClearAllPoints();
|
||||
self:SetPoint("TOPRIGHT", 1, 0);
|
||||
end
|
||||
|
||||
-- A bit ugly, we want the talent frame to display a dialog box in certain conditions.
|
||||
function UIPanelCloseButton_OnClick(self)
|
||||
local parent = self:GetParent();
|
||||
if parent then
|
||||
local continueHide = true;
|
||||
if parent.onCloseCallback then
|
||||
continueHide = parent.onCloseCallback(self);
|
||||
end
|
||||
|
||||
if continueHide then
|
||||
HideUIPanel(parent);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
PanelResizeButtonMixin = {};
|
||||
|
||||
function PanelResizeButtonMixin:Init(target, minWidth, minHeight, maxWidth, maxHeight, rotationDegrees)
|
||||
self.target = target;
|
||||
self.minWidth = minWidth;
|
||||
self.minHeight = minHeight;
|
||||
self.maxWidth = maxWidth;
|
||||
self.maxHeight = maxHeight;
|
||||
|
||||
local originalTargetOnSizeChanged = target:GetScript("OnSizeChanged") or nop;
|
||||
target:SetScript("OnSizeChanged", function(target, width, height)
|
||||
originalTargetOnSizeChanged(target, width, height);
|
||||
|
||||
-- Size changes should not be enforced here unless this button caused the change.
|
||||
if not self.isActive then
|
||||
return;
|
||||
end
|
||||
|
||||
local newWidth = width;
|
||||
if width < self.minWidth then
|
||||
newWidth = self.minWidth;
|
||||
target:SetWidth(newWidth);
|
||||
elseif self.maxWidth and width > self.maxWidth then
|
||||
newWidth = self.maxWidth;
|
||||
target:SetWidth(newWidth);
|
||||
end
|
||||
|
||||
local newHeight = height;
|
||||
if height < self.minHeight then
|
||||
newHeight = self.minHeight;
|
||||
target:SetHeight(newHeight);
|
||||
elseif self.maxHeight and height > self.maxHeight then
|
||||
newHeight = self.maxHeight;
|
||||
target:SetHeight(newHeight);
|
||||
end
|
||||
|
||||
if self.resizeCallback then
|
||||
self.resizeCallback(self, newWidth, newHeight, self.isActive);
|
||||
end
|
||||
end);
|
||||
|
||||
if rotationDegrees ~= nil then
|
||||
self:SetRotationDegrees(rotationDegrees);
|
||||
end
|
||||
end
|
||||
|
||||
function PanelResizeButtonMixin:OnEnter()
|
||||
end
|
||||
|
||||
function PanelResizeButtonMixin:OnLeave()
|
||||
end
|
||||
|
||||
function PanelResizeButtonMixin:OnMouseDown()
|
||||
if not self:IsEnabled() then
|
||||
return;
|
||||
end
|
||||
|
||||
self.isActive = true;
|
||||
|
||||
local target = self.target;
|
||||
if not target then
|
||||
return;
|
||||
end
|
||||
|
||||
local continueResizeStart = true;
|
||||
if target.onResizeStartCallback then
|
||||
continueResizeStart = target.onResizeStartCallback(self);
|
||||
end
|
||||
|
||||
if continueResizeStart then
|
||||
local alwaysStartFromMouse = true;
|
||||
self.target:StartSizing("BOTTOMRIGHT", alwaysStartFromMouse);
|
||||
end
|
||||
end
|
||||
|
||||
function PanelResizeButtonMixin:OnMouseUp()
|
||||
self.isActive = false;
|
||||
|
||||
local target = self.target;
|
||||
if not target then
|
||||
return;
|
||||
end
|
||||
|
||||
local continueResizeStop = true;
|
||||
if target.onResizeStopCallback then
|
||||
continueResizeStop = target.onResizeStopCallback(self);
|
||||
end
|
||||
|
||||
if continueResizeStop then
|
||||
target:StopMovingOrSizing();
|
||||
end
|
||||
|
||||
if self.resizeStoppedCallback ~= nil then
|
||||
self.resizeStoppedCallback(self.target);
|
||||
end
|
||||
end
|
||||
|
||||
function PanelResizeButtonMixin:IsActive()
|
||||
return not not self.isActive;
|
||||
end
|
||||
|
||||
function PanelResizeButtonMixin:SetMinWidth(minWidth)
|
||||
self.minWidth = minWidth;
|
||||
end
|
||||
|
||||
function PanelResizeButtonMixin:SetMinHeight(minHeight)
|
||||
self.minHeight = minHeight;
|
||||
end
|
||||
|
||||
function PanelResizeButtonMixin:SetMaxWidth(maxWidth)
|
||||
self.maxWidth = maxWidth;
|
||||
end
|
||||
|
||||
function PanelResizeButtonMixin:SetMaxHeight(maxHeight)
|
||||
self.maxHeight = maxHeight;
|
||||
end
|
||||
|
||||
function PanelResizeButtonMixin:SetRotationDegrees(rotationDegrees)
|
||||
local rotationRadians = (rotationDegrees / 180) * math.pi;
|
||||
self:SetRotationRadians(rotationRadians);
|
||||
end
|
||||
|
||||
function PanelResizeButtonMixin:SetRotationRadians(rotationRadians)
|
||||
local childRegions = { self:GetRegions() };
|
||||
for i, child in ipairs(childRegions) do
|
||||
if child.SetRotation ~= nil then
|
||||
child:SetRotation(rotationRadians);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PanelResizeButtonMixin:SetOnResizeStoppedCallback(resizeStoppedCallback)
|
||||
self.resizeStoppedCallback = resizeStoppedCallback;
|
||||
end
|
||||
|
||||
function PanelResizeButtonMixin:SetOnResizeCallback(resizeCallback)
|
||||
self.resizeCallback = resizeCallback;
|
||||
end
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
|
||||
<!-- Templates from retail's Blizzard_SharedXML/Mainline/SharedUIPanelTemplates.xml. -->
|
||||
<!--
|
||||
Templates from retail's Blizzard_SharedXML/Mainline/SharedUIPanelTemplates.xml.
|
||||
Retail draws the panel frames, the close button and the resize grip with
|
||||
atlases and a NineSlice this client does not have, so:
|
||||
- PortraitFrameBaseTemplate's border is the tooltip border, with a dark
|
||||
title bar under the title, as TableInspector draws its window. The
|
||||
border is the same with and without the portrait.
|
||||
- InsetFrameTemplate is a darker fill without a border: retail's content
|
||||
sits on its thin inset border, which a tooltip border would hide.
|
||||
- The inset sits where retail puts it without the portrait.
|
||||
- The close button is 1.12's own (UIPanelCloseButton's art).
|
||||
- The resize grip is the chat frame's expand arrow, turned to point at
|
||||
the corner.
|
||||
- No FrameGlow, gamepad jump hints, TopTileStreaks or button bar.
|
||||
-->
|
||||
<Script file="PortraitFrame.lua"/>
|
||||
<Script file="SharedUIPanelTemplates.lua"/>
|
||||
|
||||
<Frame name="TruncatedTooltipScriptTemplate" virtual="true">
|
||||
@@ -29,4 +44,144 @@
|
||||
<NormalTexture file="Interface\Buttons\UI-GroupLoot-Pass-Up" setAllPoints="true"/>
|
||||
<HighlightTexture file="Interface\Buttons\UI-GroupLoot-Pass-Highlight" alphaMode="ADD" setAllPoints="true"/>
|
||||
</Button>
|
||||
|
||||
<Button name="UIPanelCloseButtonNoScripts" frameLevel="510" virtual="true">
|
||||
<Size x="24" y="24"/>
|
||||
<DisabledTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Disabled"/>
|
||||
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
|
||||
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
|
||||
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
|
||||
</Button>
|
||||
<Button name="UIPanelCloseButtonDefaultAnchors" mixin="UIPanelCloseButtonDefaultAnchorsMixin" inherits="UIPanelCloseButtonNoScripts" virtual="true">
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad"/>
|
||||
<OnClick function="UIPanelCloseButton_OnClick"/>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<Frame name="InsetFrameTemplate" useParentLevel="true" virtual="true">
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture parentKey="Bg" name="$parentBg" setAllPoints="true">
|
||||
<Color r="0" g="0" b="0" a="0.5"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
|
||||
<!--
|
||||
*******************************************************************************
|
||||
PortraitFrameTemplate
|
||||
This is an empty frame with space for a portrait/icon in the top left corner.
|
||||
*******************************************************************************
|
||||
-->
|
||||
<Frame name="PortraitFrameBaseTemplate" mixin="PortraitFrameMixin" virtual="true">
|
||||
<Size x="338" y="424"/>
|
||||
<Backdrop edgeFile="Interface\Tooltips\UI-Tooltip-Border">
|
||||
<EdgeSize val="16"/>
|
||||
</Backdrop>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="5" y="-5"/>
|
||||
<Anchor point="BOTTOMRIGHT" relativePoint="TOPRIGHT" x="-5" y="-24"/>
|
||||
</Anchors>
|
||||
<Color r="0.1" g="0.1" b="0.1" a="0.95"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Frame parentKey="PortraitContainer" frameLevel="400">
|
||||
<Size x="1" y="1"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"/>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture name="$parentPortrait" parentKey="portrait">
|
||||
<Size x="62" y="62"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="-5" y="7" />
|
||||
</Anchors>
|
||||
</Texture>
|
||||
<MaskTexture parentKey="CircleMask" file="Interface\CharacterFrame\TempPortraitAlphaMask" hWrapMode="CLAMPTOBLACKADDITIVE" vWrapMode="CLAMPTOBLACKADDITIVE">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeKey="$parent.portrait" x="2" y="0"/>
|
||||
<Anchor point="BOTTOMRIGHT" relativeKey="$parent.portrait" x="-2" y="4"/>
|
||||
</Anchors>
|
||||
<MaskedTextures>
|
||||
<MaskedTexture childKey="portrait"/>
|
||||
</MaskedTextures>
|
||||
</MaskTexture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
<Frame parentKey="TitleContainer" frameLevel="510">
|
||||
<Size x="0" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="58" y="-1"/>
|
||||
<Anchor point="TOPRIGHT" x="-24" y="-1"/>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentTitleText" inherits="GameFontNormal" text="" parentKey="TitleText" wordwrap="false">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" x="0" y="-5"/>
|
||||
<Anchor point="LEFT"/>
|
||||
<Anchor point="RIGHT"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
</Frames>
|
||||
</Frame>
|
||||
|
||||
<!--
|
||||
*******************************************************************************
|
||||
ButtonFrameTemplate
|
||||
This is a Portrait Frame that also has space for buttons along the bottom
|
||||
edge, and includes an inset content area.
|
||||
*******************************************************************************
|
||||
-->
|
||||
<Frame name="ButtonFrameBaseTemplate" inherits="PortraitFrameBaseTemplate" virtual="true">
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentBg" parentKey="Bg">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="5" y="-24"/>
|
||||
<Anchor point="BOTTOMRIGHT" x="-5" y="5"/>
|
||||
</Anchors>
|
||||
<Color r="0" g="0" b="0" a="0.75"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="$parentCloseButton" parentKey="CloseButton" inherits="UIPanelCloseButtonDefaultAnchors"/>
|
||||
</Frames>
|
||||
</Frame>
|
||||
|
||||
<Frame name="ButtonFrameTemplate" inherits="ButtonFrameBaseTemplate" virtual="true">
|
||||
<Frames>
|
||||
<Frame name="$parentInset" parentKey="Inset" useParentLevel="true" inherits="InsetFrameTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="9" y="-60" />
|
||||
<Anchor point="BOTTOMRIGHT" x="-6" y="26" />
|
||||
</Anchors>
|
||||
</Frame>
|
||||
</Frames>
|
||||
</Frame>
|
||||
|
||||
<Button name="PanelResizeButtonTemplate" mixin="PanelResizeButtonMixin" virtual="true">
|
||||
<Size x="16" y="16"/>
|
||||
<Scripts>
|
||||
<OnEnter method="OnEnter"/>
|
||||
<OnLeave method="OnLeave"/>
|
||||
<OnMouseDown method="OnMouseDown"/>
|
||||
<OnMouseUp method="OnMouseUp"/>
|
||||
</Scripts>
|
||||
<NormalTexture file="Interface\ChatFrame\ChatFrameExpandArrow" rotation="-45"/>
|
||||
<HighlightTexture file="Interface\ChatFrame\ChatFrameExpandArrow" rotation="-45" alphaMode="ADD"/>
|
||||
</Button>
|
||||
</Ui>
|
||||
|
||||
@@ -195,7 +195,7 @@ function DropdownButtonMixin:RegisterMenu(menuDescription)
|
||||
|
||||
local minimumWidth = menuDescription:GetMinimumWidth() or 0;
|
||||
if minimumWidth == 0 then
|
||||
menuDescription:SetMinimumWidth(self:GetWidth());
|
||||
menuDescription:SetMinimumWidth(self:GetUnscaledWidth());
|
||||
end
|
||||
|
||||
if self.menu then
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
-- elements turn it back on where they set those scripts.
|
||||
-- - Lua 5.0 has no `...` as an expression and no `#` operator; varargs
|
||||
-- forward through unpack(arg) and lengths come from table.getn.
|
||||
-- - wipe is ClassicAPI.wipe, so an addon's own global wipe cannot
|
||||
-- replace it.
|
||||
|
||||
local CreateSecureMap = SecureTypes.CreateSecureMap;
|
||||
local CreateSecureArray = SecureTypes.CreateSecureArray;
|
||||
@@ -32,6 +34,7 @@ local ProxyConvertablePrivateMixin = CreateFromMixins(ProxyConvertableMixin);
|
||||
local CreateFromMixinsPrivate = CreateFromMixins;
|
||||
local assert = assert;
|
||||
local type = type;
|
||||
local wipe = ClassicAPI.wipe;
|
||||
|
||||
local ModifyMenuRegistry = CreateFromMixinsPrivate(CallbackRegistryMixin);
|
||||
ModifyMenuRegistry:SetUndefinedEventsAllowed(true);
|
||||
@@ -1128,7 +1131,7 @@ local function InitializeFrame(frame, menu)
|
||||
]]--
|
||||
frame:ClearAllPoints();
|
||||
frame:SetPoint("TOPLEFT"); -- Workaround to force a valid rect prior to calling initializer.
|
||||
frame:GetHeight(); -- Workaround to force a valid rect prior to calling initializer.
|
||||
frame:GetUnscaledHeight(); -- Workaround to force a valid rect prior to calling initializer.
|
||||
|
||||
CallInitializers(frame, menu);
|
||||
|
||||
@@ -1284,7 +1287,7 @@ function MenuMixin:MeasureFrameExtents()
|
||||
local totalHeight = 0;
|
||||
|
||||
for index, frame in self.frames:Enumerate() do
|
||||
local w, h = frame:GetSize();
|
||||
local w, h = frame:GetUnscaledSize();
|
||||
width = math.max(width, w);
|
||||
height = math.max(height, h);
|
||||
totalHeight = totalHeight + height;
|
||||
@@ -1763,7 +1766,7 @@ function MenuProxyMixin:OnLoad()
|
||||
end);
|
||||
|
||||
view:SetElementExtentCalculator(function(dataIndex, elementData)
|
||||
return elementData:GetHeight();
|
||||
return elementData:GetUnscaledHeight();
|
||||
end);
|
||||
|
||||
view:SetElementInitializer("Frame", function(frame, elementData)
|
||||
@@ -1784,7 +1787,7 @@ end
|
||||
|
||||
function MenuProxyMixin:InitScrollLayout(childWidth, maxScrollExtent)
|
||||
local left, top, right, bottom = securecallfunction(SecureGetInset, self);
|
||||
local scrollBarWidth = self.ScrollBar:GetWidth();
|
||||
local scrollBarWidth = self.ScrollBar:GetUnscaledWidth();
|
||||
self.ScrollBox:SetPoint("TOPLEFT", left, -top);
|
||||
self.ScrollBox:SetPoint("BOTTOMRIGHT", -(right + scrollBarWidth), bottom);
|
||||
|
||||
|
||||
@@ -353,7 +353,7 @@ function MenuTemplates.CreateHighlightRadio(text, isSelected, onSelect, data, on
|
||||
-- about that texture here because it is managed by the compositor.
|
||||
button.Text:Show();
|
||||
button.Text:SetTextToFit(text);
|
||||
button.Text:SetWidth(button.Text:GetWidth() + 10);
|
||||
button.Text:SetWidth(button.Text:GetUnscaledWidth() + 10);
|
||||
|
||||
button.HighlightBGTex:SetAlpha(0);
|
||||
|
||||
|
||||
@@ -1,53 +1,44 @@
|
||||
-- ClassicAPI's own entry in the AddOn Options window
|
||||
-- (FrameXML\InterfaceOptions.lua): a ClassicAPI page with Controls, Combat
|
||||
-- and Text To Speech pages under it.
|
||||
-- (FrameXML\InterfaceOptions.lua): a ClassicAPI page with one page under it
|
||||
-- for each feature. Every cvar that ClassicAPI registers has a control here,
|
||||
-- and so does the player cache.
|
||||
--
|
||||
-- Where retail's Settings shows a setting, the option copies its label,
|
||||
-- tooltip and value mapping (Blizzard_SettingsDefinitions_Frame: Controls,
|
||||
-- Combat, Accessibility; the TTS options use the Text To Speech window's
|
||||
-- labels). Retail has no labels for the player cache, the cast settings or
|
||||
-- friendly soft targeting, so those strings are ClassicAPI's own.
|
||||
-- Each control reads and writes exactly one setting, and writes it only when
|
||||
-- the player uses that control. So the pages have no okay, cancel or default:
|
||||
-- Okay, Cancel and Escape only close the window, and Defaults does nothing
|
||||
-- here. A page reads its settings again each time it is shown.
|
||||
--
|
||||
-- Changes apply at once. Cancel (and Escape) restores what each page held
|
||||
-- when it was first shown since the last Okay or Cancel; Defaults restores
|
||||
-- the shown page's defaults.
|
||||
-- A page's controls scroll under its heading in a ScrollBox, so a page holds
|
||||
-- as many as its feature has. Dropdowns and number boxes sit on the same row
|
||||
-- as their label.
|
||||
--
|
||||
-- Every string is a global from locales\enUS.lua: retail's GlobalStrings key
|
||||
-- where retail has the string (ENABLE_INTERACT_TEXT and the like), a
|
||||
-- CLASSICAPI_ one where it does not.
|
||||
--
|
||||
-- Lua 5.0: no `...` expression or select().
|
||||
|
||||
local L = {
|
||||
ABOUT = "Settings for the features that ClassicAPI adds to the game.",
|
||||
PLAYER_CACHE = "Player Cache",
|
||||
REMEMBER_PLAYERS = "Remember players",
|
||||
REMEMBER_PLAYERS_TOOLTIP = "Saves the name, class, race and sex of the players you meet, so addons can show them in later sessions. All your characters on this realm share the data.",
|
||||
FRIEND_TARGETING = "Enable Friendly Action Targeting",
|
||||
FRIEND_TARGETING_TOOLTIP = "Picks the nearest friendly unit where you are looking. When you have no target, your helpful spells go to it.",
|
||||
AUTO_STAND = "Auto Stand",
|
||||
AUTO_STAND_TOOLTIP = "Stand up when you cast a spell while you sit.",
|
||||
AUTO_DISMOUNT = "Auto Dismount",
|
||||
AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell while you are mounted.",
|
||||
AUTO_UNSHIFT = "Auto Cancel Form",
|
||||
AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it.",
|
||||
local ARC_CHOICES = {
|
||||
{ value = 0, text = CLASSICAPI_DIRECTION_FRONT },
|
||||
{ value = 1, text = CLASSICAPI_DIRECTION_CONE },
|
||||
{ value = 2, text = CLASSICAPI_DIRECTION_ANY },
|
||||
}
|
||||
|
||||
-- Enum.SoftTargetEnableFlags: retail's checkboxes switch between Any (on)
|
||||
-- and Gamepad (off).
|
||||
local SOFT_TARGET_GAMEPAD, SOFT_TARGET_ANY = 1, 3
|
||||
local FORCE_CHOICES = {
|
||||
{ value = 0, text = CLASSICAPI_LOOSE_TARGET_FOLLOWS_NONE },
|
||||
{ value = 1, text = CLASSICAPI_LOOSE_TARGET_FOLLOWS_ENEMY },
|
||||
{ value = 2, text = CLASSICAPI_LOOSE_TARGET_FOLLOWS_FRIEND },
|
||||
}
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Settings: get / set / reset over cvars or the player cache
|
||||
-- Settings: get / set over one cvar or the player cache
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function ResetCVars(cvars)
|
||||
for _, cvar in ipairs(cvars) do
|
||||
SetCVar(cvar, GetCVarDefault(cvar))
|
||||
end
|
||||
end
|
||||
|
||||
-- On for any nonzero number.
|
||||
local function CVarSetting(cvar)
|
||||
return {
|
||||
get = function() return GetCVar(cvar) == "1" end,
|
||||
get = function() return (tonumber(GetCVar(cvar)) or 0) ~= 0 end,
|
||||
set = function(value) SetCVar(cvar, value and "1" or "0") end,
|
||||
reset = function() ResetCVars({ cvar }) end,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -55,52 +46,33 @@ local function NumberCVarSetting(cvar)
|
||||
return {
|
||||
get = function() return tonumber(GetCVar(cvar)) end,
|
||||
set = function(value) SetCVar(cvar, tostring(value)) end,
|
||||
reset = function() ResetCVars({ cvar }) end,
|
||||
}
|
||||
end
|
||||
|
||||
-- Retail's "Enable Interact Key" / "Enable Action Targeting" proxies.
|
||||
-- The soft-target enable cvars: 0 off, 1 gamepad, 2 keyboard and mouse,
|
||||
-- 3 always. 1.12 has no gamepad, so 2 and 3 are on.
|
||||
local function SoftTargetSetting(cvar)
|
||||
return {
|
||||
get = function() return tonumber(GetCVar(cvar)) == SOFT_TARGET_ANY end,
|
||||
set = function(value) SetCVar(cvar, tostring(value and SOFT_TARGET_ANY or SOFT_TARGET_GAMEPAD)) end,
|
||||
reset = function() ResetCVars({ cvar }) end,
|
||||
get = function() return (tonumber(GetCVar(cvar)) or 0) >= 2 end,
|
||||
set = function(value) SetCVar(cvar, value and "3" or "0") end,
|
||||
}
|
||||
end
|
||||
|
||||
-- Retail's "Interact Key Icons" proxy: 1 = NPCs only, 2 = all, 3 = none.
|
||||
local ICON_CVARS = { "SoftTargetIconEnemy", "SoftTargetIconInteract", "SoftTargetIconGameObject", "SoftTargetLowPriorityIcons" }
|
||||
local ICON_PRESETS = {
|
||||
[1] = { "0", "1", "0", "0" },
|
||||
[2] = { "1", "1", "1", "1" },
|
||||
[3] = { "0", "0", "0", "0" },
|
||||
}
|
||||
local interactIconsSetting = {
|
||||
get = function()
|
||||
local on, off = true, true
|
||||
for _, cvar in ipairs(ICON_CVARS) do
|
||||
if GetCVar(cvar) == "1" then off = false else on = false end
|
||||
end
|
||||
if on then return 2 elseif off then return 3 else return 1 end
|
||||
end,
|
||||
set = function(value)
|
||||
for i, cvar in ipairs(ICON_CVARS) do
|
||||
SetCVar(cvar, ICON_PRESETS[value][i])
|
||||
end
|
||||
end,
|
||||
reset = function() ResetCVars(ICON_CVARS) end,
|
||||
}
|
||||
|
||||
local rememberSetting = {
|
||||
get = function() return C_PlayerCache.IsEnabled() and true or false end,
|
||||
set = function(value) C_PlayerCache.SetEnabled(value) end,
|
||||
reset = function() C_PlayerCache.SetEnabled(false) end,
|
||||
}
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Pages
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Row labels line up with the checkbox labels; row controls start at
|
||||
-- CONTROL_X.
|
||||
local LABEL_X = 48
|
||||
local CONTROL_X = 200
|
||||
|
||||
-- pfUI's Interface Options skin finds the controls by these names.
|
||||
local numControls = 0
|
||||
local function NextName()
|
||||
numControls = numControls + 1
|
||||
@@ -115,6 +87,10 @@ local function RefreshControls(page)
|
||||
page.updating = nil
|
||||
end
|
||||
|
||||
-- The controls go on `page.content`, the one frame in the page's ScrollBox,
|
||||
-- `page.y` down from its top. The clipping of the ScrollBox also keeps the
|
||||
-- mouse off the controls that are scrolled out of view.
|
||||
-- `title` is the heading, the category name when nil.
|
||||
local function NewPage(name, parent, title, subtitle)
|
||||
local page = CreateFrame("Frame")
|
||||
page.name = name
|
||||
@@ -123,66 +99,80 @@ local function NewPage(name, parent, title, subtitle)
|
||||
|
||||
local heading = page:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
|
||||
heading:SetPoint("TOPLEFT", page, "TOPLEFT", 16, -16)
|
||||
heading:SetText(title)
|
||||
page.y = -40
|
||||
heading:SetText(title or name)
|
||||
local top = -40
|
||||
if subtitle then
|
||||
local text = page:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
||||
text:SetPoint("TOPLEFT", heading, "BOTTOMLEFT", 0, -8)
|
||||
text:SetPoint("RIGHT", page, "RIGHT", -32, 0)
|
||||
text:SetJustifyH("LEFT")
|
||||
text:SetText(subtitle)
|
||||
page.y = -64
|
||||
top = -64
|
||||
end
|
||||
|
||||
page.refresh = function()
|
||||
if not page.saved then
|
||||
page.saved = {}
|
||||
for i, control in ipairs(page.controls) do
|
||||
page.saved[i] = control.setting.get()
|
||||
end
|
||||
end
|
||||
RefreshControls(page)
|
||||
end
|
||||
page.okay = function()
|
||||
page.saved = nil
|
||||
end
|
||||
page.cancel = function()
|
||||
if not page.saved then return end
|
||||
for i, control in ipairs(page.controls) do
|
||||
if control.setting.get() ~= page.saved[i] then
|
||||
control.setting.set(page.saved[i])
|
||||
end
|
||||
end
|
||||
page.saved = nil
|
||||
end
|
||||
page.default = function()
|
||||
for _, control in ipairs(page.controls) do
|
||||
control.setting.reset()
|
||||
end
|
||||
RefreshControls(page)
|
||||
end
|
||||
local scrollBox = CreateFrame("Frame", nil, page, "WowScrollBox")
|
||||
scrollBox:SetPoint("TOPLEFT", page, "TOPLEFT", 0, top)
|
||||
scrollBox:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", -20, 8)
|
||||
scrollBox:EnableMouseWheel(true)
|
||||
local scrollBar = CreateFrame("EventFrame", nil, page, "MinimalScrollBar")
|
||||
scrollBar:SetPoint("TOPLEFT", scrollBox, "TOPRIGHT", 4, 0)
|
||||
scrollBar:SetPoint("BOTTOMLEFT", scrollBox, "BOTTOMRIGHT", 4, 0)
|
||||
|
||||
local content = CreateFrame("Frame", nil, scrollBox)
|
||||
content:SetHeight(1)
|
||||
content.scrollable = true
|
||||
ScrollUtil.InitScrollBoxWithScrollBar(scrollBox, scrollBar, CreateScrollBoxLinearView())
|
||||
ScrollUtil.AddManagedScrollBarVisibilityBehavior(scrollBox, scrollBar)
|
||||
|
||||
page.scrollBox = scrollBox
|
||||
page.content = content
|
||||
page.y = 0
|
||||
page.refresh = RefreshControls
|
||||
|
||||
InterfaceOptions_AddCategory(page)
|
||||
return page
|
||||
end
|
||||
|
||||
-- Moves `page.y` down past a row and makes the content that tall. The
|
||||
-- ScrollBox measures it again on its next update (a hidden page's waits for
|
||||
-- the page to be shown).
|
||||
local function Grow(page, height)
|
||||
page.y = page.y - height
|
||||
page.content:SetHeight(8 - page.y)
|
||||
page.scrollBox:FullUpdate(ScrollBoxConstants.UpdateQueued)
|
||||
end
|
||||
|
||||
local function AddControl(page, control, setting, height)
|
||||
control.setting = setting
|
||||
table.insert(page.controls, control)
|
||||
page.y = page.y - height
|
||||
Grow(page, height)
|
||||
end
|
||||
|
||||
local function AddHeader(page, text)
|
||||
local header = page:CreateFontString(nil, "ARTWORK", "GameFontNormal")
|
||||
header:SetPoint("TOPLEFT", page, "TOPLEFT", 16, page.y - 8)
|
||||
local header = page.content:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
|
||||
header:SetPoint("TOPLEFT", page.content, "TOPLEFT", 16, page.y - 6)
|
||||
header:SetText(text)
|
||||
page.y = page.y - 28
|
||||
Grow(page, 24)
|
||||
end
|
||||
|
||||
local function AddRowLabel(page, label)
|
||||
local text = page.content:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
|
||||
text:SetPoint("TOPLEFT", page.content, "TOPLEFT", LABEL_X, page.y - 10)
|
||||
text:SetText(label)
|
||||
end
|
||||
|
||||
local function SetTooltip(frame, tooltip)
|
||||
frame:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(tooltip, nil, nil, nil, nil, 1)
|
||||
end)
|
||||
frame:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
end
|
||||
|
||||
-- `enabledIf`, when given, greys the checkbox out while it returns false.
|
||||
local function AddCheckbox(page, label, tooltip, setting, enabledIf)
|
||||
local check = CreateFrame("CheckButton", NextName(), page, "OptionsCheckButtonTemplate")
|
||||
check:SetPoint("TOPLEFT", page, "TOPLEFT", 16, page.y)
|
||||
local check = CreateFrame("CheckButton", NextName(), page.content, "OptionsCheckButtonTemplate")
|
||||
check:SetPoint("TOPLEFT", page.content, "TOPLEFT", 16, page.y)
|
||||
local text = getglobal(check:GetName() .. "Text")
|
||||
text:SetText(label)
|
||||
check.tooltipText = tooltip
|
||||
@@ -201,32 +191,32 @@ local function AddCheckbox(page, label, tooltip, setting, enabledIf)
|
||||
end
|
||||
end
|
||||
AddControl(page, check, setting, 30)
|
||||
return check
|
||||
end
|
||||
|
||||
-- `options()` returns the entries as { value = ..., text = ... }.
|
||||
local function AddDropDown(page, label, tooltip, setting, options)
|
||||
local title = page:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
||||
title:SetPoint("TOPLEFT", page, "TOPLEFT", 20, page.y - 6)
|
||||
title:SetText(label)
|
||||
-- `choices` is a list of { value = ..., text = ... }, or a function that
|
||||
-- returns one. A value with no choice shows as the number itself.
|
||||
local function AddDropDown(page, label, tooltip, setting, choices)
|
||||
AddRowLabel(page, label)
|
||||
|
||||
local dropdown = CreateFrame("Frame", NextName(), page, "UIDropDownMenuTemplate")
|
||||
dropdown:SetPoint("TOPLEFT", page, "TOPLEFT", 0, page.y - 20)
|
||||
UIDropDownMenu_SetWidth(180, dropdown)
|
||||
local function List()
|
||||
if type(choices) == "function" then return choices() end
|
||||
return choices
|
||||
end
|
||||
|
||||
-- The template's art has a 16 pixel margin on the left.
|
||||
local dropdown = CreateFrame("Frame", NextName(), page.content, "UIDropDownMenuTemplate")
|
||||
dropdown:SetPoint("TOPLEFT", page.content, "TOPLEFT", CONTROL_X - 16, page.y)
|
||||
UIDropDownMenu_SetWidth(150, dropdown)
|
||||
dropdown:EnableMouse(true)
|
||||
dropdown:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(tooltip, nil, nil, nil, nil, 1)
|
||||
end)
|
||||
dropdown:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
SetTooltip(dropdown, tooltip)
|
||||
|
||||
local function Initialize()
|
||||
local selected = setting.get()
|
||||
for _, option in ipairs(options()) do
|
||||
for _, choice in ipairs(List()) do
|
||||
local info = {}
|
||||
info.text = option.text
|
||||
info.value = option.value
|
||||
info.checked = option.value == selected and 1 or nil
|
||||
info.text = choice.text
|
||||
info.value = choice.value
|
||||
info.checked = choice.value == selected and 1 or nil
|
||||
info.func = function()
|
||||
UIDropDownMenu_SetSelectedValue(dropdown, this.value)
|
||||
setting.set(this.value)
|
||||
@@ -236,18 +226,55 @@ local function AddDropDown(page, label, tooltip, setting, options)
|
||||
end
|
||||
dropdown.Update = function(self)
|
||||
UIDropDownMenu_Initialize(self, Initialize)
|
||||
UIDropDownMenu_SetSelectedValue(self, setting.get())
|
||||
local selected = setting.get()
|
||||
UIDropDownMenu_SetSelectedValue(self, selected)
|
||||
local text = tostring(selected or "")
|
||||
for _, choice in ipairs(List()) do
|
||||
if choice.value == selected then text = choice.text end
|
||||
end
|
||||
UIDropDownMenu_SetText(text, self)
|
||||
end
|
||||
AddControl(page, dropdown, setting, 56)
|
||||
return dropdown
|
||||
AddControl(page, dropdown, setting, 32)
|
||||
end
|
||||
|
||||
local function AddSlider(page, label, tooltip, setting, minValue, maxValue)
|
||||
local slider = CreateFrame("Slider", NextName(), page, "OptionsSliderTemplate")
|
||||
slider:SetPoint("TOPLEFT", page, "TOPLEFT", 24, page.y - 20)
|
||||
-- Writes when Enter is pressed or the box loses focus; Escape puts the
|
||||
-- current value back. Text that is not a number is not written.
|
||||
local function AddNumberBox(page, label, tooltip, setting)
|
||||
AddRowLabel(page, label)
|
||||
|
||||
local box = CreateFrame("EditBox", NextName(), page.content, "InputBoxTemplate")
|
||||
box:SetPoint("TOPLEFT", page.content, "TOPLEFT", CONTROL_X + 6, page.y - 6)
|
||||
box:SetWidth(70)
|
||||
box:SetHeight(20)
|
||||
box:SetAutoFocus(false)
|
||||
SetTooltip(box, tooltip)
|
||||
|
||||
local function ShowValue(self)
|
||||
self:SetText(tostring(setting.get() or ""))
|
||||
end
|
||||
box:SetScript("OnEnterPressed", function() this:ClearFocus() end)
|
||||
box:SetScript("OnEscapePressed", function()
|
||||
ShowValue(this)
|
||||
this:ClearFocus()
|
||||
end)
|
||||
box:SetScript("OnEditFocusLost", function()
|
||||
this:HighlightText(0, 0)
|
||||
local value = tonumber(this:GetText())
|
||||
if value and value ~= setting.get() then
|
||||
setting.set(value)
|
||||
end
|
||||
ShowValue(this)
|
||||
end)
|
||||
box.Update = ShowValue
|
||||
AddControl(page, box, setting, 32)
|
||||
end
|
||||
|
||||
local function AddSlider(page, label, tooltip, setting, minValue, maxValue, step)
|
||||
local slider = CreateFrame("Slider", NextName(), page.content, "OptionsSliderTemplate")
|
||||
slider:SetPoint("TOPLEFT", page.content, "TOPLEFT", 24, page.y - 20)
|
||||
slider:SetWidth(200)
|
||||
slider:SetMinMaxValues(minValue, maxValue)
|
||||
slider:SetValueStep(1)
|
||||
slider:SetValueStep(step)
|
||||
slider.tooltipText = tooltip
|
||||
getglobal(slider:GetName() .. "Text"):SetText(label)
|
||||
getglobal(slider:GetName() .. "Low"):SetText(minValue)
|
||||
@@ -255,21 +282,24 @@ local function AddSlider(page, label, tooltip, setting, minValue, maxValue)
|
||||
|
||||
local value = slider:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
||||
value:SetPoint("LEFT", slider, "RIGHT", 8, 0)
|
||||
local function Format(v)
|
||||
if step < 1 then return string.format("%.2f", v) end
|
||||
return v
|
||||
end
|
||||
|
||||
slider:SetScript("OnValueChanged", function()
|
||||
local v = math.floor(this:GetValue() + 0.5)
|
||||
value:SetText(v)
|
||||
local v = math.floor(this:GetValue() / step + 0.5) * step
|
||||
value:SetText(Format(v))
|
||||
if not page.updating and v ~= setting.get() then
|
||||
setting.set(v)
|
||||
end
|
||||
end)
|
||||
slider.Update = function(self)
|
||||
local v = setting.get()
|
||||
local v = setting.get() or minValue
|
||||
self:SetValue(v)
|
||||
value:SetText(v)
|
||||
value:SetText(Format(v))
|
||||
end
|
||||
AddControl(page, slider, setting, 56)
|
||||
return slider
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -277,57 +307,127 @@ end
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local version = GetAddOnMetadata("!!!ClassicAPI", "Version")
|
||||
local main = NewPage("ClassicAPI", nil, "ClassicAPI " .. version, L.ABOUT)
|
||||
local main = NewPage("ClassicAPI", nil, "ClassicAPI " .. version, CLASSICAPI_OPTIONS_ABOUT)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Controls
|
||||
-- Interact Key
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local controls = NewPage(CONTROLS_LABEL, main.name, CONTROLS_LABEL)
|
||||
local interact = NewPage(CLASSICAPI_INTERACT_KEY, main.name)
|
||||
|
||||
AddCheckbox(controls, ENABLE_INTERACT_TEXT, OPTION_TOOLTIP_ENABLE_INTERACT, SoftTargetSetting("SoftTargetInteract"))
|
||||
AddDropDown(controls, INTERACT_ICONS_OPTION, OPTION_TOOLTIP_INTERACT_ICONS, interactIconsSetting, function()
|
||||
return {
|
||||
{ value = 1, text = INTERACT_ICONS_DEFAULT },
|
||||
{ value = 2, text = INTERACT_ICONS_SHOW_ALL },
|
||||
{ value = 3, text = INTERACT_ICONS_SHOW_NONE },
|
||||
}
|
||||
end)
|
||||
AddCheckbox(controls, L.AUTO_STAND, L.AUTO_STAND_TOOLTIP, CVarSetting("autoStand"))
|
||||
AddCheckbox(controls, L.AUTO_DISMOUNT, L.AUTO_DISMOUNT_TOOLTIP, CVarSetting("autoDismount"))
|
||||
AddCheckbox(controls, L.AUTO_UNSHIFT, L.AUTO_UNSHIFT_TOOLTIP, CVarSetting("autoUnshift"))
|
||||
AddCheckbox(interact, ENABLE_INTERACT_TEXT, OPTION_TOOLTIP_ENABLE_INTERACT, SoftTargetSetting("SoftTargetInteract"))
|
||||
AddDropDown(interact, CLASSICAPI_INTERACT_DIRECTION, CLASSICAPI_INTERACT_DIRECTION_TOOLTIP, NumberCVarSetting("SoftTargetInteractArc"), ARC_CHOICES)
|
||||
AddNumberBox(interact, CLASSICAPI_INTERACT_RANGE, CLASSICAPI_RANGE_TOOLTIP, NumberCVarSetting("SoftTargetInteractRange"))
|
||||
AddCheckbox(interact, CLASSICAPI_STRICT_INTERACT_RANGE, CLASSICAPI_STRICT_INTERACT_RANGE_TOOLTIP, CVarSetting("SoftTargetInteractRangeIsHard"))
|
||||
local interactIcon = CVarSetting("SoftTargetIconInteract")
|
||||
AddCheckbox(interact, CLASSICAPI_INTERACT_ICON, CLASSICAPI_INTERACT_ICON_TOOLTIP, interactIcon)
|
||||
-- The object icon is drawn only while the interact icon is on.
|
||||
AddCheckbox(interact, CLASSICAPI_GAME_OBJECT_ICON, CLASSICAPI_GAME_OBJECT_ICON_TOOLTIP, CVarSetting("SoftTargetIconGameObject"), interactIcon.get)
|
||||
|
||||
AddHeader(interact, CLASSICAPI_ALL_SOFT_TARGET_ICONS)
|
||||
AddCheckbox(interact, CLASSICAPI_LOW_PRIORITY_ICONS, CLASSICAPI_LOW_PRIORITY_ICONS_TOOLTIP, CVarSetting("SoftTargetLowPriorityIcons"))
|
||||
AddNumberBox(interact, CLASSICAPI_ICON_SIZE, CLASSICAPI_ICON_SIZE_TOOLTIP, NumberCVarSetting("SoftTargetNameplateSize"))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Action Targeting
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local targeting = NewPage(CLASSICAPI_ACTION_TARGETING, main.name)
|
||||
|
||||
AddHeader(targeting, CLASSICAPI_ENEMIES)
|
||||
AddCheckbox(targeting, ACTION_TARGETING_OPTION, OPTION_TOOLTIP_ACTION_TARGETING, SoftTargetSetting("SoftTargetEnemy"))
|
||||
AddDropDown(targeting, CLASSICAPI_ENEMY_DIRECTION, CLASSICAPI_UNIT_DIRECTION_TOOLTIP, NumberCVarSetting("SoftTargetEnemyArc"), ARC_CHOICES)
|
||||
AddNumberBox(targeting, CLASSICAPI_ENEMY_RANGE, CLASSICAPI_RANGE_TOOLTIP, NumberCVarSetting("SoftTargetEnemyRange"))
|
||||
AddCheckbox(targeting, CLASSICAPI_ENEMY_ICON, CLASSICAPI_ENEMY_ICON_TOOLTIP, CVarSetting("SoftTargetIconEnemy"))
|
||||
|
||||
AddHeader(targeting, CLASSICAPI_FRIENDS)
|
||||
AddCheckbox(targeting, CLASSICAPI_FRIEND_TARGETING, CLASSICAPI_FRIEND_TARGETING_TOOLTIP, SoftTargetSetting("SoftTargetFriend"))
|
||||
AddDropDown(targeting, CLASSICAPI_FRIEND_DIRECTION, CLASSICAPI_UNIT_DIRECTION_TOOLTIP, NumberCVarSetting("SoftTargetFriendArc"), ARC_CHOICES)
|
||||
AddNumberBox(targeting, CLASSICAPI_FRIEND_RANGE, CLASSICAPI_RANGE_TOOLTIP, NumberCVarSetting("SoftTargetFriendRange"))
|
||||
AddCheckbox(targeting, CLASSICAPI_FRIEND_ICON, CLASSICAPI_FRIEND_ICON_TOOLTIP, CVarSetting("SoftTargetIconFriend"))
|
||||
|
||||
AddHeader(targeting, CLASSICAPI_WHEN_YOU_HAVE_A_TARGET)
|
||||
AddCheckbox(targeting, CLASSICAPI_KEEP_ACTION_TARGETING, CLASSICAPI_KEEP_ACTION_TARGETING_TOOLTIP, CVarSetting("SoftTargetWithLocked"))
|
||||
AddCheckbox(targeting, CLASSICAPI_PREFER_YOUR_TARGET, CLASSICAPI_PREFER_YOUR_TARGET_TOOLTIP, CVarSetting("SoftTargetMatchLocked"))
|
||||
AddDropDown(targeting, CLASSICAPI_LOOSE_TARGET_FOLLOWS, CLASSICAPI_LOOSE_TARGET_FOLLOWS_TOOLTIP, NumberCVarSetting("SoftTargetForce"), FORCE_CHOICES)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Combat
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local combat = NewPage(COMBAT_LABEL, main.name, COMBAT_LABEL)
|
||||
local combat = NewPage(COMBAT_LABEL, main.name)
|
||||
|
||||
AddCheckbox(combat, ACTION_TARGETING_OPTION, OPTION_TOOLTIP_ACTION_TARGETING, SoftTargetSetting("SoftTargetEnemy"))
|
||||
-- Retail has no friend checkbox; same mapping as its enemy one.
|
||||
AddCheckbox(combat, L.FRIEND_TARGETING, L.FRIEND_TARGETING_TOOLTIP, SoftTargetSetting("SoftTargetFriend"))
|
||||
AddHeader(combat, CLASSICAPI_ACTION_BARS)
|
||||
AddCheckbox(combat, ACTION_BUTTON_USE_KEY_DOWN, OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN, CVarSetting("ActionButtonUseKeyDown"))
|
||||
AddCheckbox(combat, COUNTDOWN_FOR_COOLDOWNS_TEXT, OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS, CVarSetting("countdownForCooldowns"))
|
||||
|
||||
AddHeader(combat, CLASSICAPI_CASTING_AND_ATTACKING)
|
||||
AddCheckbox(combat, CLASSICAPI_AUTO_STAND, CLASSICAPI_AUTO_STAND_TOOLTIP, CVarSetting("autoStand"))
|
||||
AddCheckbox(combat, CLASSICAPI_AUTO_DISMOUNT, CLASSICAPI_AUTO_DISMOUNT_TOOLTIP, CVarSetting("autoDismount"))
|
||||
AddCheckbox(combat, CLASSICAPI_AUTO_UNSHIFT, CLASSICAPI_AUTO_UNSHIFT_TOOLTIP, CVarSetting("autoUnshift"))
|
||||
AddCheckbox(combat, STOP_AUTO_ATTACK, OPTION_TOOLTIP_STOP_AUTO_ATTACK, CVarSetting("stopAutoAttackOnTargetChange"))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Bags and Items
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local bags = NewPage(CLASSICAPI_BAGS_AND_ITEMS, main.name)
|
||||
|
||||
AddCheckbox(bags, CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT, CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT_TOOLTIP, CVarSetting("sortBagsRightToLeft"))
|
||||
AddCheckbox(bags, CLASSICAPI_IGNORE_BACKPACK, CLASSICAPI_IGNORE_BACKPACK_TOOLTIP, CVarSetting("backpackAutosortDisabled"))
|
||||
AddCheckbox(bags, CLASSICAPI_IGNORE_BANK, CLASSICAPI_IGNORE_BANK_TOOLTIP, CVarSetting("bankAutosortDisabled"))
|
||||
AddCheckbox(bags, CLASSICAPI_HIDE_EQUIPMENT_SETS, CLASSICAPI_HIDE_EQUIPMENT_SETS_TOOLTIP, CVarSetting("dontShowEquipmentSetsOnItems"))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Interface
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local interface = NewPage(CLASSICAPI_INTERFACE, main.name)
|
||||
|
||||
AddHeader(interface, CLASSICAPI_NAMEPLATES)
|
||||
AddSlider(interface, CLASSICAPI_TARGET_NAMEPLATE_ALPHA, CLASSICAPI_TARGET_NAMEPLATE_ALPHA_TOOLTIP, NumberCVarSetting("nameplateSelectedAlpha"), 0, 1, 0.05)
|
||||
AddSlider(interface, CLASSICAPI_OTHER_NAMEPLATE_ALPHA, CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP, NumberCVarSetting("nameplateNotSelectedAlpha"), 0, 1, 0.05)
|
||||
|
||||
AddHeader(interface, CLASSICAPI_NUMBERS)
|
||||
AddCheckbox(interface, CLASSICAPI_SEPARATE_LARGE_NUMBERS, CLASSICAPI_SEPARATE_LARGE_NUMBERS_TOOLTIP, CVarSetting("breakUpLargeNumbers"))
|
||||
|
||||
AddHeader(interface, CLASSICAPI_MOTION)
|
||||
AddNumberBox(interface, CLASSICAPI_UI_SHAKE_STRENGTH, CLASSICAPI_UI_SHAKE_STRENGTH_TOOLTIP, NumberCVarSetting("ShakeStrengthUI"))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Social
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local social = NewPage(SOCIAL_LABEL, main.name)
|
||||
|
||||
-- C_AutoComplete reads these; the addon that shows the names does the rest.
|
||||
AddHeader(social, CLASSICAPI_NAME_SUGGESTIONS)
|
||||
AddCheckbox(social, CLASSICAPI_RECENT_WHISPERS_FIRST, CLASSICAPI_RECENT_WHISPERS_FIRST_TOOLTIP, CVarSetting("autoCompleteResortNamesOnRecency"))
|
||||
AddCheckbox(social, CLASSICAPI_MATCH_INSIDE_NAMES, CLASSICAPI_MATCH_INSIDE_NAMES_TOOLTIP, CVarSetting("autoCompleteWhenEditingFromCenter"))
|
||||
AddCheckbox(social, CLASSICAPI_MATCH_THE_COMMAND, CLASSICAPI_MATCH_THE_COMMAND_TOOLTIP, CVarSetting("autoCompleteUseContext"))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Text To Speech
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local tts = NewPage(TEXT_TO_SPEECH, main.name, TEXT_TO_SPEECH)
|
||||
local tts = NewPage(TEXT_TO_SPEECH, main.name)
|
||||
|
||||
AddDropDown(tts, TEXT_TO_SPEECH_VOICE_OPTION, TEXT_TO_SPEECH_VOICE_OPTION, NumberCVarSetting("ttsVoice"), function()
|
||||
local options = {}
|
||||
local choices = {}
|
||||
for _, voice in ipairs(C_VoiceChat.GetTtsVoices()) do
|
||||
table.insert(options, { value = voice.voiceID, text = voice.name })
|
||||
table.insert(choices, { value = voice.voiceID, text = voice.name })
|
||||
end
|
||||
return options
|
||||
return choices
|
||||
end)
|
||||
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_RATE, TEXT_TO_SPEECH_ADJUST_RATE, NumberCVarSetting("ttsSpeed"), -10, 10)
|
||||
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_VOLUME, TEXT_TO_SPEECH_ADJUST_VOLUME, NumberCVarSetting("ttsVolume"), 0, 100)
|
||||
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_RATE, TEXT_TO_SPEECH_ADJUST_RATE, NumberCVarSetting("ttsSpeed"), -10, 10, 1)
|
||||
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_VOLUME, TEXT_TO_SPEECH_ADJUST_VOLUME, NumberCVarSetting("ttsVolume"), 0, 100, 1)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Player Cache
|
||||
-- Advanced
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local playerCache = NewPage(L.PLAYER_CACHE, main.name, L.PLAYER_CACHE)
|
||||
AddCheckbox(playerCache, L.REMEMBER_PLAYERS, L.REMEMBER_PLAYERS_TOOLTIP, rememberSetting)
|
||||
local advanced = NewPage(CLASSICAPI_ADVANCED, main.name)
|
||||
|
||||
AddCheckbox(advanced, CLASSICAPI_REMEMBER_PLAYERS, CLASSICAPI_REMEMBER_PLAYERS_TOOLTIP, rememberSetting)
|
||||
AddNumberBox(advanced, CLASSICAPI_COMBAT_LOG_RETENTION, CLASSICAPI_COMBAT_LOG_RETENTION_TOOLTIP, NumberCVarSetting("combatLogRetentionTime"))
|
||||
AddCheckbox(advanced, CLASSICAPI_SCRIPT_PROFILE, CLASSICAPI_SCRIPT_PROFILE_TOOLTIP, CVarSetting("scriptProfile"))
|
||||
|
||||
@@ -129,8 +129,8 @@ function AnchorUtil.GridLayout(frames, initialAnchor, layout)
|
||||
return;
|
||||
end
|
||||
|
||||
local width = layout.horizontalSpacing or frames[1]:GetWidth();
|
||||
local height = layout.verticalSpacing or frames[1]:GetHeight();
|
||||
local width = layout.horizontalSpacing or frames[1]:GetUnscaledWidth();
|
||||
local height = layout.verticalSpacing or frames[1]:GetUnscaledHeight();
|
||||
local stride = layout.stride;
|
||||
local paddingX = layout.paddingX;
|
||||
local paddingY = layout.paddingY;
|
||||
@@ -243,8 +243,8 @@ function AnchorUtil.GridLayoutFactory(factoryFunction, initialAnchor, totalWidth
|
||||
|
||||
-- If we have an override padding, count it in the frame width. We add a padding to totalWidth/totalHeight to account for the
|
||||
-- extra space we save for the last element which doesn't need padding.
|
||||
local width = Round(frame:GetWidth()) + (overridePaddingX or 0);
|
||||
local height = Round(frame:GetHeight()) + (overridePaddingY or 0);
|
||||
local width = Round(frame:GetUnscaledWidth()) + (overridePaddingX or 0);
|
||||
local height = Round(frame:GetUnscaledHeight()) + (overridePaddingY or 0);
|
||||
local rowSize = math.floor((totalWidth + (overridePaddingX or 0)) / width);
|
||||
local colSize = math.floor((totalHeight + (overridePaddingY or 0)) / height);
|
||||
|
||||
@@ -439,8 +439,8 @@ local function DebugAnchorGraph(frame, indent, visited, output)
|
||||
|
||||
local function FormatFrame(frame)
|
||||
local color = frame:IsRectValid() and GREEN_FONT_COLOR or RED_FONT_COLOR;
|
||||
local x, y = frame:GetSize();
|
||||
local x2, y2 = frame:GetSize(true);
|
||||
local x, y = frame:GetUnscaledSize();
|
||||
local x2, y2 = frame:GetUnscaledSize(true);
|
||||
return color:WrapTextInColorCode(frame:GetDebugName() .. (" calculated size <%.2f, %.2f> explicit size <%.2f, %.2f> points <%d>, scale <%.2f> effective scale <%.2f>"):format(
|
||||
x, y, x2, y2, frame:GetNumPoints(), frame:GetScale(), frame:GetEffectiveScale()));
|
||||
end
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
-- Backport of retail's Blizzard_FrameXMLUtil/AnimationSystem.lua.
|
||||
|
||||
--[[ animTable = { --Note that only consistent data should be in here. These tables are meant to be shared across "sessions" of animations. Put changing data in the frame.
|
||||
totalTime = number, --Time to complete the animation in seconds.
|
||||
updateFunc = function, --The function called to do the actual change. Takes self, elapsed fraction. Usually frame.SetPoint, frame.SetAlpha, ect.
|
||||
getPosFunc = function, --The function returning the data being passed into updateFunc. For example. might return .18 if updateFunc is frame.SetAlpha.
|
||||
--]]
|
||||
local AnimatingFrames = {};
|
||||
|
||||
local AnimUpdateFrame = CreateFrame("Frame");
|
||||
|
||||
local function Animation_UpdateFrame(self, animElapsed, animTable)
|
||||
local totalTime = animTable.totalTime
|
||||
if ( animElapsed and (animElapsed < totalTime)) then --Should be animating
|
||||
local elapsedFraction = self.animReverse and (1-animElapsed/totalTime) or (animElapsed/totalTime);
|
||||
animTable.updateFunc(self, animTable.getPosFunc(self, elapsedFraction));
|
||||
else --Just finished animating
|
||||
animTable.updateFunc(self, animTable.getPosFunc(self, self.animReverse and 0 or 1));
|
||||
self.animating = false;
|
||||
|
||||
AnimatingFrames[self][animTable.updateFunc] = 0; --We use 0 instead of nil'ing out because we don't want to mess with 'next' (used in pairs)
|
||||
|
||||
if ( self.animPostFunc ) then
|
||||
self.animPostFunc(self);
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
local totalElapsed = 0;
|
||||
local function Animation_OnUpdate(self, elapsed)
|
||||
totalElapsed = totalElapsed + elapsed;
|
||||
local isAnyFrameAnimating = false;
|
||||
for frame, frameTable in pairs(AnimatingFrames) do
|
||||
for _, animTable in pairs(frameTable) do
|
||||
if ( animTable ~= 0 ) then
|
||||
Animation_UpdateFrame(frame, totalElapsed - frame.animStartTime, animTable);
|
||||
isAnyFrameAnimating = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
if ( not isAnyFrameAnimating ) then
|
||||
table.wipe(AnimatingFrames);
|
||||
AnimUpdateFrame:SetScript("OnUpdate", nil);
|
||||
end
|
||||
end
|
||||
|
||||
function SetUpAnimation(frame, animTable, postFunc, reverse)
|
||||
if ( type(animTable.updateFunc) == "string" ) then
|
||||
animTable.updateFunc = frame[animTable.updateFunc];
|
||||
end
|
||||
if ( not AnimatingFrames[frame] ) then
|
||||
AnimatingFrames[frame] = {};
|
||||
end
|
||||
|
||||
AnimatingFrames[frame][animTable.updateFunc] = animTable;
|
||||
|
||||
frame.animStartTime = totalElapsed;
|
||||
frame.animReverse = reverse;
|
||||
frame.animPostFunc = postFunc;
|
||||
frame.animating = true;
|
||||
|
||||
animTable.updateFunc(frame, animTable.getPosFunc(frame, frame.animReverse and 1 or 0));
|
||||
|
||||
AnimUpdateFrame:SetScript("OnUpdate", Animation_OnUpdate);
|
||||
end
|
||||
|
||||
function CancelAnimations(frame)
|
||||
local anims = AnimatingFrames[frame];
|
||||
if ( anims ) then
|
||||
for k, v in pairs(anims) do
|
||||
anims[k] = 0;
|
||||
end
|
||||
frame.animating = false;
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,114 @@
|
||||
-- Backport of retail's Blizzard_ObjectAPI/Mainline/AsyncCallbackSystem.lua.
|
||||
|
||||
--[[
|
||||
Queries some data retrieval API (specifically where the data may not be currently available) and when it becomes available
|
||||
calls a user-supplied function. The callback can be canceled if necessary (e.g. the frame that would use the data becomes
|
||||
hidden before the data arrives).
|
||||
|
||||
The API is managed so that arbitrary query functions cannot be executed.
|
||||
--]]
|
||||
|
||||
AsyncCallbackAPIType = {
|
||||
ASYNC_QUEST = 1,
|
||||
ASYNC_ITEM = 2,
|
||||
ASYNC_SPELL = 3,
|
||||
}
|
||||
|
||||
local permittedAPI =
|
||||
{
|
||||
[AsyncCallbackAPIType.ASYNC_QUEST] = { event = "QUEST_DATA_LOAD_RESULT", accessor = C_QuestLog.RequestLoadQuestByID },
|
||||
[AsyncCallbackAPIType.ASYNC_ITEM] = { event = "ITEM_DATA_LOAD_RESULT", accessor = C_Item.RequestLoadItemDataByID },
|
||||
[AsyncCallbackAPIType.ASYNC_SPELL] = { event = "SPELL_DATA_LOAD_RESULT", accessor = C_Spell.RequestLoadSpellData },
|
||||
};
|
||||
|
||||
AsyncCallbackSystemMixin = {};
|
||||
|
||||
function AsyncCallbackSystemMixin:Init(apiType)
|
||||
self.callbacks = {};
|
||||
|
||||
-- API Type should be set up from key value pairs before OnLoad.
|
||||
self.api = permittedAPI[apiType];
|
||||
|
||||
self:SetScript("OnEvent",
|
||||
function(self, event, ...)
|
||||
if event == self.api.event then
|
||||
local id, success = ...;
|
||||
if success or apiType == AsyncCallbackAPIType.ASYNC_ITEM then
|
||||
self:FireCallbacks(id, success);
|
||||
else
|
||||
self:ClearCallbacks(id);
|
||||
end
|
||||
end
|
||||
end
|
||||
);
|
||||
self:RegisterEvent(self.api.event);
|
||||
end
|
||||
|
||||
local CANCELED_SENTINEL = -1;
|
||||
|
||||
function AsyncCallbackSystemMixin:AddCallback(id, callbackFunction)
|
||||
local callbacks = self:GetOrCreateCallbacks(id);
|
||||
table.insert(callbacks, callbackFunction);
|
||||
local needsAccessorCall = #callbacks == 1;
|
||||
if needsAccessorCall then
|
||||
self.api.accessor(id);
|
||||
end
|
||||
|
||||
return #callbacks, callbacks;
|
||||
end
|
||||
|
||||
function AsyncCallbackSystemMixin:AddCancelableCallback(id, callbackFunction)
|
||||
-- NOTE: If the data is currently availble then the callback will be executed and callbacks cleared, so there will be nothing to cancel.
|
||||
local index, callbacks = self:AddCallback(id, callbackFunction);
|
||||
return function()
|
||||
if #callbacks > 0 and callbacks[index] ~= CANCELED_SENTINEL then
|
||||
callbacks[index] = CANCELED_SENTINEL;
|
||||
return true;
|
||||
end
|
||||
return false;
|
||||
end;
|
||||
end
|
||||
|
||||
function AsyncCallbackSystemMixin:FireCallbacks(id, success)
|
||||
local callbacks = self:GetCallbacks(id);
|
||||
if callbacks then
|
||||
self:ClearCallbacks(id);
|
||||
for i, callback in ipairs(callbacks) do
|
||||
if callback ~= CANCELED_SENTINEL then
|
||||
xpcall(callback, CallErrorHandler, id, success);
|
||||
end
|
||||
end
|
||||
|
||||
-- The cancel functions have a reference to this table, so ensure that it's cleared out.
|
||||
for i = #callbacks, 1, -1 do
|
||||
callbacks[i] = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AsyncCallbackSystemMixin:ClearCallbacks(id)
|
||||
self.callbacks[id] = nil;
|
||||
end
|
||||
|
||||
function AsyncCallbackSystemMixin:GetCallbacks(id)
|
||||
return self.callbacks[id];
|
||||
end
|
||||
|
||||
function AsyncCallbackSystemMixin:GetOrCreateCallbacks(id)
|
||||
local callbacks = self.callbacks[id];
|
||||
if not callbacks then
|
||||
callbacks = {};
|
||||
self.callbacks[id] = callbacks;
|
||||
end
|
||||
return callbacks;
|
||||
end
|
||||
|
||||
local function CreateListener(apiType)
|
||||
local listener = Mixin(CreateFrame("Frame"), AsyncCallbackSystemMixin);
|
||||
listener:Init(apiType);
|
||||
return listener;
|
||||
end
|
||||
|
||||
ItemEventListener = CreateListener(AsyncCallbackAPIType.ASYNC_ITEM);
|
||||
SpellEventListener = CreateListener(AsyncCallbackAPIType.ASYNC_SPELL);
|
||||
QuestEventListener = CreateListener(AsyncCallbackAPIType.ASYNC_QUEST);
|
||||
@@ -1,214 +1,268 @@
|
||||
-- Backport of 3.3.5 FrameXML/CallbackRegistryMixin.lua to vanilla 1.12 / Lua 5.0.
|
||||
-- Backport of retail Blizzard_SharedXMLBase/CallbackRegistry.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the 3.3.5 source:
|
||||
-- - 1.12 has no taint or Secure-Templates system, so the entire
|
||||
-- AttributeDelegate/SetForbidden machinery for inserting event keys
|
||||
-- into the callback tables is unnecessary. Sub-tables are allocated
|
||||
-- lazily inside RegisterCallback instead.
|
||||
-- - securecallfunction/secureexecuterange don't exist; they reduce to
|
||||
-- direct calls / explicit for-loops here.
|
||||
-- - GenerateClosure doesn't exist in 1.12; provide a local one that
|
||||
-- captures registration-time varargs via SafePack-style storage.
|
||||
-- - Lua 5.0 has no select() or `wipe()`; we use the implicit `arg`
|
||||
-- table and assign-nil to clear tables.
|
||||
-- Implementation differences from the retail source:
|
||||
-- - No AttributeDelegate. Retail inserts event keys and creates deferred
|
||||
-- callback tables through a forbidden frame's OnAttributeChanged, as a
|
||||
-- taint barrier. 1.12 has no taint and no SetForbidden, so
|
||||
-- SecureInsertEvent and GetMutableCallbacksByEvent do that work directly.
|
||||
-- - TriggerEvent calls securecallfunction from its own loops, where retail
|
||||
-- passes a vararg helper to secureexecuterange. The contract is the same
|
||||
-- (an error goes to the error handler and the other callbacks still run),
|
||||
-- but every call to a vararg function builds an `arg` table in Lua 5.0,
|
||||
-- which would cost a table per callback per event.
|
||||
-- - wipe is ClassicAPI.wipe, so an addon's own global wipe cannot
|
||||
-- replace it.
|
||||
|
||||
local generateOwnerID = CreateCounter()
|
||||
local secureexecuterange = secureexecuterange;
|
||||
local securecallfunction = securecallfunction;
|
||||
local unpack = unpack;
|
||||
local error = error;
|
||||
local pairs = pairs;
|
||||
local rawset = rawset;
|
||||
local next = next;
|
||||
local wipe = ClassicAPI.wipe;
|
||||
local GetOrCreateTableEntry = GetOrCreateTableEntry;
|
||||
|
||||
local CallbackType = EnumUtil.MakeEnum("Closure", "Function")
|
||||
local CallbackType = EnumUtil.MakeEnum("Closure", "Function");
|
||||
|
||||
-- Local GenerateClosure: binds `func` plus all of `...` so a later call
|
||||
-- invokes func(boundArgs..., callArgs...). Mirrors 3.3.5's helper of the
|
||||
-- same name. SafePack/SafeUnpack semantics so embedded nils survive.
|
||||
local function GenerateClosure(func, ...)
|
||||
local boundN = arg.n
|
||||
local bound = {}
|
||||
for i = 1, boundN do bound[i] = arg[i] end
|
||||
return function(...)
|
||||
local callN = arg.n
|
||||
-- Forward bound + call args faithfully, embedded/leading nils and all.
|
||||
-- The ONLY thing that makes unpack span holes on this Lua 5.0 build is
|
||||
-- a `.n` field: unpack takes its count from getn (which returns t.n)
|
||||
-- and ignores any explicit i/j, so a `.n`-less table stops dead at the
|
||||
-- first hole (verified: unpack({1, 1, nil, 1}, 1, 4) yields just 1, 1).
|
||||
local all = {}
|
||||
local n = 0
|
||||
for i = 1, boundN do n = n + 1; all[n] = bound[i] end
|
||||
for i = 1, callN do n = n + 1; all[n] = arg[i] end
|
||||
all.n = n
|
||||
return func(unpack(all))
|
||||
end
|
||||
end
|
||||
-- Callbacks can be registered without an owner as a matter of convenience. Generally this is fine when you never
|
||||
-- intend to release the callback.
|
||||
local generateOwnerID = CreateCounter();
|
||||
|
||||
CallbackRegistryMixin = CallbackRegistryMixin or {}
|
||||
CallbackRegistryMixin = {};
|
||||
|
||||
function CallbackRegistryMixin:OnLoad()
|
||||
local callbackTables = {}
|
||||
for _, value in pairs(CallbackType) do
|
||||
callbackTables[value] = {}
|
||||
end
|
||||
self.callbackTables = callbackTables
|
||||
local callbackTables = {};
|
||||
for callbackType, value in pairs(CallbackType) do
|
||||
local callbacks = {};
|
||||
callbackTables[value] = callbacks;
|
||||
end
|
||||
self.callbackTables = callbackTables;
|
||||
|
||||
self.executingEvents = {};
|
||||
self.deferredCallbacks = {};
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:SetUndefinedEventsAllowed(allowed)
|
||||
self.isUndefinedEventAllowed = allowed
|
||||
self.isUndefinedEventAllowed = allowed;
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:GetCallbackTables()
|
||||
return self.callbackTables
|
||||
return self.callbackTables;
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:GetCallbackTable(callbackType)
|
||||
return self.callbackTables[callbackType]
|
||||
return self.callbackTables[callbackType];
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:GetCallbacksByEvent(callbackType, event)
|
||||
local callbackTable = self:GetCallbackTable(callbackType)
|
||||
return callbackTable[event]
|
||||
local callbackTable = self:GetCallbackTable(callbackType);
|
||||
return callbackTable[event];
|
||||
end
|
||||
|
||||
-- Returns either the underlying callback table or a temporary table depending
|
||||
-- on if the event is currently being dispatched. The callbacks stored in the
|
||||
-- temporary table will be merged into the underlying callback table after the
|
||||
-- dispatch is complete.
|
||||
function CallbackRegistryMixin:GetMutableCallbacksByEvent(callbackType, event)
|
||||
if securecallfunction(rawget, self.executingEvents, event) == nil then
|
||||
return self:GetCallbacksByEvent(callbackType, event);
|
||||
end
|
||||
|
||||
local deferrals = GetOrCreateTableEntry(self.deferredCallbacks, event);
|
||||
return GetOrCreateTableEntry(deferrals, callbackType);
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:HasRegistrantsForEvent(event)
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
local callbacks = callbackTable[event]
|
||||
if callbacks and next(callbacks) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
local callbacks = callbackTable[event];
|
||||
if callbacks and securecallfunction(next, callbacks) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
-- Lazy allocation of per-event sub-tables. In 3.3.5 this went through a
|
||||
-- forbidden attribute frame for taint-isolation reasons; that's a no-op
|
||||
-- consideration on 1.12, so we just assign here.
|
||||
function CallbackRegistryMixin:SecureInsertEvent(event)
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
if not callbackTable[event] then
|
||||
callbackTable[event] = {}
|
||||
end
|
||||
end
|
||||
if not self:HasRegistrantsForEvent(event) then
|
||||
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
if not callbackTable[event] then
|
||||
rawset(callbackTable, event, {});
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:RegisterCallback(event, func, owner, ...)
|
||||
if type(event) ~= "string" then
|
||||
error("CallbackRegistryMixin::RegisterCallback 'event' requires string type.")
|
||||
elseif type(func) ~= "function" then
|
||||
error("CallbackRegistryMixin::RegisterCallback 'func' requires function type.")
|
||||
else
|
||||
if owner == nil then
|
||||
owner = generateOwnerID()
|
||||
elseif type(owner) == "number" then
|
||||
error("CallbackRegistryMixin:RegisterCallback 'owner' as number is reserved internally.")
|
||||
end
|
||||
end
|
||||
if type(event) ~= "string" then
|
||||
error("CallbackRegistryMixin::RegisterCallback 'event' requires string type.");
|
||||
elseif type(func) ~= "function" then
|
||||
error("CallbackRegistryMixin::RegisterCallback 'func' requires function type.");
|
||||
else
|
||||
if owner == nil then
|
||||
owner = generateOwnerID();
|
||||
elseif type(owner) == "number" then
|
||||
error("CallbackRegistryMixin:RegisterCallback 'owner' as number is reserved internally.")
|
||||
end
|
||||
end
|
||||
|
||||
self:SecureInsertEvent(event)
|
||||
-- Taint barrier for inserting event key into callback tables.
|
||||
self:SecureInsertEvent(event);
|
||||
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
local callbacks = callbackTable[event]
|
||||
callbacks[owner] = nil
|
||||
end
|
||||
-- An owner can have a single callback per event. The simpliest way to ensure
|
||||
-- this is to remove all callbacks for the owner prior to new registration.
|
||||
self:UnregisterCallback(event, owner);
|
||||
|
||||
if arg.n > 0 then
|
||||
local callbacks = self:GetCallbacksByEvent(CallbackType.Closure, event)
|
||||
callbacks[owner] = GenerateClosure(func, owner, unpack(arg))
|
||||
else
|
||||
local callbacks = self:GetCallbacksByEvent(CallbackType.Function, event)
|
||||
callbacks[owner] = func
|
||||
end
|
||||
local count = select("#", ...);
|
||||
if count > 0 then
|
||||
local callbacks = self:GetMutableCallbacksByEvent(CallbackType.Closure, event);
|
||||
callbacks[owner] = GenerateClosure(func, owner, ...);
|
||||
else
|
||||
local callbacks = self:GetMutableCallbacksByEvent(CallbackType.Function, event);
|
||||
callbacks[owner] = func;
|
||||
end
|
||||
|
||||
return owner
|
||||
return owner;
|
||||
end
|
||||
|
||||
local function CreateCallbackHandle(cbr, event, owner)
|
||||
local handle =
|
||||
{
|
||||
Unregister = function()
|
||||
cbr:UnregisterCallback(event, owner)
|
||||
end,
|
||||
}
|
||||
return handle
|
||||
-- Wrapped in a table for future flexibility.
|
||||
local handle =
|
||||
{
|
||||
Unregister = function()
|
||||
cbr:UnregisterCallback(event, owner);
|
||||
end,
|
||||
};
|
||||
return handle;
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:RegisterCallbackWithHandle(event, func, owner, ...)
|
||||
owner = self:RegisterCallback(event, func, owner, unpack(arg))
|
||||
return CreateCallbackHandle(self, event, owner)
|
||||
owner = self:RegisterCallback(event, func, owner, ...);
|
||||
return CreateCallbackHandle(self, event, owner);
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:ReconcileDeferredCallbacks(event, closures, funcs)
|
||||
self:CopyDeferredCallbacks(CallbackType.Closure, event, closures);
|
||||
self:CopyDeferredCallbacks(CallbackType.Function, event, funcs);
|
||||
self.deferredCallbacks[event] = nil;
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:CopyDeferredCallbacks(callbackType, event, target)
|
||||
local deferrals = self.deferredCallbacks[event];
|
||||
if deferrals == nil then
|
||||
return;
|
||||
end
|
||||
|
||||
local callbacks = deferrals[callbackType];
|
||||
if callbacks == nil then
|
||||
return;
|
||||
end
|
||||
|
||||
local function ExecuteAssignCallback(owner, callback)
|
||||
target[owner] = callback;
|
||||
end
|
||||
|
||||
secureexecuterange(callbacks, ExecuteAssignCallback);
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:TriggerEvent(event, ...)
|
||||
if type(event) ~= "string" then
|
||||
error("CallbackRegistryMixin:TriggerEvent 'event' requires string type.")
|
||||
elseif not self.isUndefinedEventAllowed and not (self.Event and self.Event[event]) then
|
||||
error(string.format("CallbackRegistryMixin:TriggerEvent event '%s' doesn't exist.", event))
|
||||
end
|
||||
if type(event) ~= "string" then
|
||||
error("CallbackRegistryMixin:TriggerEvent 'event' requires string type.");
|
||||
elseif not self.isUndefinedEventAllowed and not self.Event[event] then
|
||||
error(string.format("CallbackRegistryMixin:TriggerEvent event '%s' doesn't exist.", event));
|
||||
end
|
||||
|
||||
local closures = self:GetCallbacksByEvent(CallbackType.Closure, event)
|
||||
if closures then
|
||||
for _, closure in pairs(closures) do
|
||||
closure(unpack(arg))
|
||||
end
|
||||
end
|
||||
-- TriggerEvent appears to need to support reentrant calls for now.
|
||||
local count = (self.executingEvents[event] or 0) + 1;
|
||||
|
||||
local funcs = self:GetCallbacksByEvent(CallbackType.Function, event)
|
||||
if funcs then
|
||||
for owner, func in pairs(funcs) do
|
||||
-- Stock 3.3.5 passes the owner as the first arg so callers can
|
||||
-- distinguish multiple registrations of the same function. `arg`
|
||||
-- carries its own `.n`, so unpack(arg) forwards the payload with
|
||||
-- leading/embedded nils intact. Do NOT instead merge into a
|
||||
-- `{ owner, ... }` table and unpack that WITHOUT setting `.n`: this
|
||||
-- build's unpack counts via getn, so a `.n`-less table collapses at
|
||||
-- the first hole (verified in-game: reload's PLAYER_ENTERING_WORLD,
|
||||
-- arg1=nil arg2=1, dropped the payload).
|
||||
func(owner, unpack(arg))
|
||||
end
|
||||
end
|
||||
-- Set before invoking any callback so a reentrant call does not
|
||||
-- attempt to call ReconcileDeferredCallbacks.
|
||||
self.executingEvents[event] = count;
|
||||
|
||||
local closures = self:GetCallbacksByEvent(CallbackType.Closure, event);
|
||||
if closures then
|
||||
for owner, closure in pairs(closures) do
|
||||
securecallfunction(closure, ...);
|
||||
end
|
||||
end
|
||||
|
||||
local funcs = self:GetCallbacksByEvent(CallbackType.Function, event);
|
||||
if funcs then
|
||||
for owner, func in pairs(funcs) do
|
||||
securecallfunction(func, owner, ...);
|
||||
end
|
||||
end
|
||||
|
||||
-- The local count is the only value we care about for the purpose
|
||||
-- of flushing the event key and reconciling the deferred callbacks.
|
||||
if count == 1 then
|
||||
self.executingEvents[event] = nil;
|
||||
|
||||
self:ReconcileDeferredCallbacks(event, closures, funcs);
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:UnregisterCallback(event, owner)
|
||||
if type(event) ~= "string" then
|
||||
error("CallbackRegistryMixin:UnregisterCallback 'event' requires string type.")
|
||||
elseif owner == nil then
|
||||
error("CallbackRegistryMixin:UnregisterCallback 'owner' is required.")
|
||||
end
|
||||
if type(event) ~= "string" then
|
||||
error("CallbackRegistryMixin:UnregisterCallback 'event' requires string type.");
|
||||
elseif owner == nil then
|
||||
error("CallbackRegistryMixin:UnregisterCallback 'owner' is required.");
|
||||
end
|
||||
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
local callbacks = callbackTable[event]
|
||||
if callbacks then
|
||||
callbacks[owner] = nil
|
||||
end
|
||||
end
|
||||
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
-- Only assign nil if the owner is present in the table, otherwise
|
||||
-- its insertion could cause a rehash during iteration by secureexecuterange.
|
||||
local callbacks = callbackTable[event];
|
||||
if callbacks and callbacks[owner] then
|
||||
callbacks[owner] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
local deferrals = self.deferredCallbacks[event];
|
||||
if deferrals then
|
||||
for callbackType, callbacks in pairs(deferrals) do
|
||||
-- Freely assign nil because deferredCallbacks are not subject
|
||||
-- to secureexecuterange table mutilation stress disorder.
|
||||
callbacks[owner] = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:UnregisterEvents(eventTable)
|
||||
if eventTable then
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
for event in pairs(eventTable) do
|
||||
if callbackTable[event] then
|
||||
callbackTable[event] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
for k in pairs(callbackTable) do
|
||||
callbackTable[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
function CallbackRegistryMixin:UnregisterEvents()
|
||||
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
wipe(callbackTable);
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:GenerateCallbackEvents(events)
|
||||
if not self.Event then
|
||||
self.Event = {}
|
||||
end
|
||||
function CallbackRegistryMixin:UnregisterEventsByEventTable(eventTable)
|
||||
if type(eventTable) ~= "table" then
|
||||
error("CallbackRegistryMixin:UnregisterEventsByEventTable 'eventTable' requires table type.");
|
||||
end
|
||||
|
||||
for eventIndex, eventName in ipairs(events) do
|
||||
if self.Event[eventName] then
|
||||
error(string.format("CallbackRegistryMixin:GenerateCallbackEvents: event '%s' already exists.", eventName))
|
||||
end
|
||||
self.Event[eventName] = eventName
|
||||
end
|
||||
for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
for event in pairs(eventTable) do
|
||||
callbackTable[event] = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:GenerateCallbackEvents(eventTable)
|
||||
if not self.Event then
|
||||
self.Event = {};
|
||||
end
|
||||
|
||||
if type(eventTable) ~= "table" then
|
||||
error("CallbackRegistryMixin:GenerateCallbackEvents 'eventTable' requires table type.");
|
||||
end
|
||||
|
||||
for eventIndex, eventName in ipairs(eventTable) do
|
||||
if self.Event[eventName] then
|
||||
error(string.format("CallbackRegistryMixin:GenerateCallbackEvents: event '%s' already exists.", eventName));
|
||||
end
|
||||
self.Event[eventName] = eventName;
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin.DoesFrameHaveEvent(frame, event)
|
||||
return frame.Event and frame.Event[event]
|
||||
return frame.Event and frame.Event[event];
|
||||
end
|
||||
|
||||
@@ -132,7 +132,7 @@ local function ApplyItemQualityColors()
|
||||
end
|
||||
|
||||
function CAPI_ApplyStandardColorGlobals()
|
||||
-- `C_UIColor.GetColors()` makes a color object for each of its 187 rows,
|
||||
-- `C_UIColor.GetColors()` makes a color object for each of its 331 rows,
|
||||
-- so keep the values from the first call. Each later pass is then one
|
||||
-- lookup per tag.
|
||||
if not standardColors then
|
||||
|
||||
@@ -87,11 +87,7 @@ function GetClassColoredTextForUnit(unit, text)
|
||||
end
|
||||
|
||||
function GetFactionColor(factionGroupTag)
|
||||
-- Both PLAYER_FACTION_GROUP (tag -> index) and PLAYER_FACTION_COLORS
|
||||
-- (index -> color) live in our Constants.lua. Guarded defensively in
|
||||
-- case a client variant leaves one unset.
|
||||
local group = PLAYER_FACTION_GROUP[factionGroupTag];
|
||||
return PLAYER_FACTION_COLORS and PLAYER_FACTION_COLORS[group];
|
||||
return PLAYER_FACTION_COLORS[PLAYER_FACTION_GROUP[factionGroupTag]];
|
||||
end
|
||||
|
||||
function RGBToColorCode(r, g, b)
|
||||
|
||||
@@ -117,12 +117,6 @@ FACTION_LABELS_FROM_STRING = {
|
||||
["Alliance"] = FACTION_ALLIANCE,
|
||||
}
|
||||
|
||||
PLAYER_FACTION_COLORS =
|
||||
{
|
||||
[0] = PLAYER_FACTION_COLOR_HORDE,
|
||||
[1] = PLAYER_FACTION_COLOR_ALLIANCE,
|
||||
};
|
||||
|
||||
--
|
||||
-- Glyph
|
||||
--
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Backport of the bag-count globals from retail's
|
||||
-- Blizzard_UIPanels_Game/Mainline/ContainerFrame.lua.
|
||||
--
|
||||
-- Differences from the retail source:
|
||||
-- - NUM_TOTAL_BAG_FRAMES is NUM_BAG_FRAMES: there is no reagent bag.
|
||||
-- - ContainerFrame_GetContainerNumSlots gives the backpack no extra slots.
|
||||
-- Retail counts 4 more for an account without an authenticator; here
|
||||
-- the backpack's size does not depend on the account.
|
||||
|
||||
NUM_TOTAL_BAG_FRAMES = NUM_BAG_FRAMES;
|
||||
|
||||
function ContainerFrame_GetContainerNumSlots(bagId)
|
||||
if bagId == KEYRING_CONTAINER then
|
||||
local keyRingSize = GetKeyRingSize();
|
||||
return keyRingSize, keyRingSize;
|
||||
end
|
||||
|
||||
local currentNumSlots = C_Container.GetContainerNumSlots(bagId);
|
||||
local maxNumSlots = currentNumSlots;
|
||||
|
||||
return maxNumSlots, currentNumSlots;
|
||||
end
|
||||
@@ -0,0 +1,114 @@
|
||||
-- Backport of retail's Blizzard_ObjectAPI/Mainline/ContinuableContainer.lua.
|
||||
|
||||
ContinuableContainer = {};
|
||||
|
||||
--[[
|
||||
Expected usage:
|
||||
|
||||
local continuableContainer = ContinuableContainer:Create();
|
||||
local item = Item:CreateFromItemLocation(itemLoc);
|
||||
continuableContainer:AddContinuable(item);
|
||||
... add more continuables
|
||||
|
||||
continuableContainer:ContinueOnLoad(function()
|
||||
-- code to execute once all continuables are ready, could be immediate
|
||||
end);
|
||||
]]
|
||||
|
||||
--[[static]] function ContinuableContainer:Create()
|
||||
return CreateFromMixins(self);
|
||||
end
|
||||
|
||||
function ContinuableContainer:AddContinuables(tbl)
|
||||
for index, continuable in pairs(tbl) do
|
||||
self:AddContinuable(continuable);
|
||||
end
|
||||
end
|
||||
|
||||
function ContinuableContainer:AddContinuable(continuable)
|
||||
if not self.continuables or not self.evictableObjects then
|
||||
self.continuables = {};
|
||||
self.evictableObjects = {};
|
||||
|
||||
self.onContinuableLoadedCallback = function(id, success)
|
||||
self.numOutstanding = self.numOutstanding - 1;
|
||||
if not success then
|
||||
for i, evictableObject in ipairs(self.evictableObjects) do
|
||||
if evictableObject:GetItemID() == id then
|
||||
table.remove(self.evictableObjects, i);
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
self:CheckIfSatisfied();
|
||||
end;
|
||||
end
|
||||
|
||||
self.numOutstanding = self:GetNumOutstandingLoads() + 1;
|
||||
if continuable:IsDataEvictable() then
|
||||
table.insert(self.evictableObjects, continuable);
|
||||
end
|
||||
table.insert(self.continuables, continuable:ContinueWithCancelOnRecordLoad(self.onContinuableLoadedCallback));
|
||||
end
|
||||
|
||||
function ContinuableContainer:ContinueOnLoad(callbackFunction)
|
||||
if type(callbackFunction) ~= "function" then
|
||||
error("Usage: ContinuableContainer:ContinueOnLoad(callbackFunction)", 2);
|
||||
end
|
||||
|
||||
self.callbackFunction = callbackFunction;
|
||||
return self:CheckIfSatisfied();
|
||||
end
|
||||
|
||||
function ContinuableContainer:GetNumOutstandingLoads()
|
||||
return self.numOutstanding or 0;
|
||||
end
|
||||
|
||||
function ContinuableContainer:AreAnyLoadsOutstanding()
|
||||
return self:GetNumOutstandingLoads() > 0;
|
||||
end
|
||||
|
||||
function ContinuableContainer:Cancel()
|
||||
local continuables = self.continuables;
|
||||
if continuables then
|
||||
self.continuables = nil;
|
||||
self.evictableObjects = nil;
|
||||
self.numOutstanding = nil;
|
||||
|
||||
for i, continuable in ipairs(continuables) do
|
||||
continuable();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- "private"
|
||||
function ContinuableContainer:CheckIfSatisfied()
|
||||
if not self:AreAnyLoadsOutstanding() and self.callbackFunction and self:RecheckEvictableContinuables() then
|
||||
local callbackFunction = self.callbackFunction;
|
||||
self.callbackFunction = nil;
|
||||
|
||||
self.continuables = nil;
|
||||
self.evictableObjects = nil;
|
||||
|
||||
xpcall(callbackFunction, CallErrorHandler);
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function ContinuableContainer:RecheckEvictableContinuables()
|
||||
local areAllLoaded = true;
|
||||
if self.evictableObjects then
|
||||
for i, evictableObject in ipairs(self.evictableObjects) do
|
||||
if not evictableObject:IsRecordDataCached() then
|
||||
areAllLoaded = false;
|
||||
|
||||
self.numOutstanding = self.numOutstanding + 1;
|
||||
table.insert(self.continuables, evictableObject:ContinueWithCancelOnRecordLoad(self.onContinuableLoadedCallback));
|
||||
end
|
||||
end
|
||||
end
|
||||
return areAllLoaded;
|
||||
end
|
||||
@@ -1,13 +1,14 @@
|
||||
-- Backport of retail's Blizzard_SharedXMLBase/ErrorUtil.lua.
|
||||
--
|
||||
-- Implementation differences from the retail source:
|
||||
-- - 1.12 has no SetErrorCallstackHeight / GetCallstackHeight, so an
|
||||
-- error's stack starts at assertsafe rather than at its caller.
|
||||
-- - Lua 5.0 has no `...` as an expression and strings have no methods;
|
||||
-- varargs go through the implicit `arg` table and string.format.
|
||||
|
||||
function CallErrorHandler(...)
|
||||
return geterrorhandler()(unpack(arg));
|
||||
SetErrorCallstackHeight(GetCallstackHeight() - 1); -- report error from the previous function
|
||||
local result = geterrorhandler()(unpack(arg));
|
||||
SetErrorCallstackHeight(nil);
|
||||
return result;
|
||||
end
|
||||
|
||||
function assertsafe(cond, msgStringOrFunction, ...)
|
||||
@@ -19,6 +20,7 @@ function assertsafe(cond, msgStringOrFunction, ...)
|
||||
error = msgStringOrFunction(unpack(arg));
|
||||
end
|
||||
|
||||
SetErrorCallstackHeight(GetCallstackHeight() - 1); -- report error from the previous function
|
||||
if geterrorhandler() then
|
||||
geterrorhandler()(error);
|
||||
elseif ProcessExceptionClient then
|
||||
@@ -26,6 +28,7 @@ function assertsafe(cond, msgStringOrFunction, ...)
|
||||
ProcessExceptionClient(error, error, framesToSkip);
|
||||
|
||||
end
|
||||
SetErrorCallstackHeight(nil);
|
||||
end
|
||||
|
||||
-- Parity with regular 'assert' which returns the input.
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
-- Backport of 3.3.5 FrameXML/EventUtil.lua to vanilla 1.12 / Lua 5.0.
|
||||
-- Backport of retail Blizzard_SharedXML/EventUtil.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the 3.3.5 source:
|
||||
-- - Lua 5.0 has no select(); we work off the implicit `arg` table
|
||||
-- in vararg functions.
|
||||
-- - 1.12 has no C_AddOns; the legacy global IsAddOnLoaded(name)
|
||||
-- fills the same role.
|
||||
-- - 1.12 has no secureexecuterange; queued callbacks are dispatched
|
||||
-- by a plain ipairs loop.
|
||||
-- - AreVariablesLoaded in 3.3.5 sniffs GlueParent / UIParent.firstTimeLoaded.
|
||||
-- 1.12 has neither; we track a private flag flipped on VARIABLES_LOADED.
|
||||
-- Implementation differences from the retail source:
|
||||
-- - Retail's UIParent sets UIParent.variablesLoaded and calls
|
||||
-- EventUtil.TriggerOnVariablesLoaded on VARIABLES_LOADED. 1.12's
|
||||
-- FrameXML does neither, so the frame at the bottom of this file does
|
||||
-- both.
|
||||
-- - Varargs are read from the implicit `arg` table, which every Lua 5.0
|
||||
-- vararg function already has, rather than walked with select().
|
||||
|
||||
EventUtil = {}
|
||||
|
||||
@@ -19,7 +17,7 @@ function ContinueAfterAllEventsMixin:Init(callback, ...)
|
||||
|
||||
for index = 1, arg.n do
|
||||
local event = arg[index]
|
||||
assert(C_EventUtils.IsEventValid(event), "Unknown event \"" .. event .. "\"")
|
||||
assert(C_EventUtils.IsEventValid(event), ("Unknown event \"%s\""):format(event))
|
||||
self.events[event] = false
|
||||
|
||||
local function OnEventReceived()
|
||||
@@ -50,13 +48,8 @@ function EventUtil.ContinueAfterAllEvents(callback, ...)
|
||||
obj:Init(callback, unpack(arg))
|
||||
end
|
||||
|
||||
-- Tracked locally because 1.12 has no GlueParent / UIParent.firstTimeLoaded.
|
||||
-- Bootstrap frame at the bottom of the file listens for VARIABLES_LOADED
|
||||
-- and flips the flag, then drains the queued callbacks via the
|
||||
-- TriggerOnVariablesLoaded path.
|
||||
local variablesLoaded = false
|
||||
function EventUtil.AreVariablesLoaded()
|
||||
return variablesLoaded
|
||||
return GlueParent or (UIParent and UIParent.variablesLoaded)
|
||||
end
|
||||
|
||||
local eventUtilVariablesLoadedCallbacks = {}
|
||||
@@ -69,25 +62,26 @@ function EventUtil.ContinueOnVariablesLoaded(callback)
|
||||
table.insert(eventUtilVariablesLoadedCallbacks, callback)
|
||||
end
|
||||
|
||||
local secureexecuterange = secureexecuterange
|
||||
local eventUtilContinueOnVariablesLoadedTriggered = false
|
||||
function EventUtil.TriggerOnVariablesLoaded()
|
||||
if eventUtilContinueOnVariablesLoadedTriggered then
|
||||
-- If TriggerOnVariablesLoaded has been called once already, don't do anything further.
|
||||
return
|
||||
end
|
||||
eventUtilContinueOnVariablesLoadedTriggered = true
|
||||
variablesLoaded = true
|
||||
|
||||
for _, callback in ipairs(eventUtilVariablesLoadedCallbacks) do
|
||||
secureexecuterange(eventUtilVariablesLoadedCallbacks, function(_, callback)
|
||||
callback()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function EventUtil.ContinueOnAddOnLoaded(addOnName, callback)
|
||||
local isLoadedOrLoading, isLoaded = C_AddOns.IsAddOnLoaded(addOnName)
|
||||
if isLoaded then
|
||||
callback();
|
||||
return;
|
||||
end
|
||||
local isLoadedOrLoading, isLoaded = C_AddOns.IsAddOnLoaded(addOnName)
|
||||
if isLoaded then
|
||||
callback()
|
||||
return
|
||||
end
|
||||
|
||||
EventUtil.RegisterOnceFrameEventAndCallback("ADDON_LOADED", callback, addOnName)
|
||||
end
|
||||
@@ -125,6 +119,7 @@ function CallbackHandleContainerMixin:Init()
|
||||
self.handles = {}
|
||||
end
|
||||
|
||||
-- Accepts any CBR and stores a handle to the registered callback.
|
||||
function CallbackHandleContainerMixin:RegisterCallback(cbr, event, callback, owner)
|
||||
self:AddHandle(cbr:RegisterCallbackWithHandle(event, callback, owner))
|
||||
end
|
||||
@@ -141,7 +136,7 @@ function CallbackHandleContainerMixin:Unregister()
|
||||
end
|
||||
|
||||
function CallbackHandleContainerMixin:IsEmpty()
|
||||
return table.getn(self.handles) == 0
|
||||
return #self.handles == 0
|
||||
end
|
||||
|
||||
function EventUtil.CreateCallbackHandleContainer()
|
||||
@@ -150,13 +145,13 @@ function EventUtil.CreateCallbackHandleContainer()
|
||||
return cbrHandles
|
||||
end
|
||||
|
||||
-- VARIABLES_LOADED bootstrap. Drains anything queued via
|
||||
-- ContinueOnVariablesLoaded the first time the event fires. We use a
|
||||
-- dedicated frame so we don't pollute EventRegistry's payload routing.
|
||||
-- Retail UIParent's VARIABLES_LOADED work for EventUtil: mark the variables
|
||||
-- loaded, then run what ContinueOnVariablesLoaded queued.
|
||||
do
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("VARIABLES_LOADED")
|
||||
f:SetScript("OnEvent", function()
|
||||
UIParent.variablesLoaded = true
|
||||
EventUtil.TriggerOnVariablesLoaded()
|
||||
f:UnregisterEvent("VARIABLES_LOADED")
|
||||
end)
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
-- Backport of retail's Blizzard_SharedXMLBase/ExportUtil.lua.
|
||||
|
||||
-- Utility for compressing and uncompressing data into strings for export and import.
|
||||
|
||||
ExportUtil = {};
|
||||
|
||||
local BitsPerChar = 6;
|
||||
|
||||
local function MakeBase64ConversionTable()
|
||||
local base64ConversionTable = {};
|
||||
base64ConversionTable[0] = 'A';
|
||||
for num = 1, 25 do
|
||||
table.insert(base64ConversionTable, string.char(65 + num));
|
||||
end
|
||||
|
||||
for num = 0, 25 do
|
||||
table.insert(base64ConversionTable, string.char(97 + num));
|
||||
end
|
||||
|
||||
for num = 0, 9 do
|
||||
table.insert(base64ConversionTable, tostring(num));
|
||||
end
|
||||
|
||||
table.insert(base64ConversionTable, '+');
|
||||
table.insert(base64ConversionTable, '/');
|
||||
return base64ConversionTable;
|
||||
end
|
||||
|
||||
local NumberToBase64CharConversionTable = MakeBase64ConversionTable();
|
||||
local Base64CharToNumberConversionTable = tInvert(MakeBase64ConversionTable());
|
||||
|
||||
function ExportUtil.ConvertToBase64(dataEntries)
|
||||
local exportString = "";
|
||||
local currentValue = 0;
|
||||
local currentReservedBits = 0;
|
||||
local totalBits = 0;
|
||||
for i, dataEntry in ipairs(dataEntries) do
|
||||
local remainingValue = dataEntry.value;
|
||||
local remainingRequiredBits = dataEntry.bitWidth;
|
||||
-- TODO: bit.lshift doesnt work with > 32 bits. Maybe use maxValue = 2^X instead?
|
||||
local maxValue = bit.lshift(1, remainingRequiredBits);
|
||||
if remainingValue >= maxValue then
|
||||
error(("Data entry has higher value than storable in bitWidth. (%d in %d bits)"):format(remainingValue, remainingRequiredBits));
|
||||
return "";
|
||||
end
|
||||
|
||||
totalBits = totalBits + remainingRequiredBits;
|
||||
while remainingRequiredBits > 0 do
|
||||
local spaceInCurrentValue = (BitsPerChar - currentReservedBits);
|
||||
local maxStorableValue = bit.lshift(1, spaceInCurrentValue);
|
||||
local remainder = remainingValue % maxStorableValue;
|
||||
remainingValue = bit.rshift(remainingValue, spaceInCurrentValue);
|
||||
currentValue = currentValue + bit.lshift(remainder, currentReservedBits);
|
||||
|
||||
if spaceInCurrentValue > remainingRequiredBits then
|
||||
currentReservedBits = (currentReservedBits + remainingRequiredBits) % BitsPerChar;
|
||||
remainingRequiredBits = 0;
|
||||
else
|
||||
exportString = exportString..NumberToBase64CharConversionTable[currentValue];
|
||||
currentValue = 0;
|
||||
currentReservedBits = 0;
|
||||
remainingRequiredBits = remainingRequiredBits - spaceInCurrentValue;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if currentReservedBits > 0 then
|
||||
exportString = exportString..NumberToBase64CharConversionTable[currentValue];
|
||||
end
|
||||
|
||||
return exportString;
|
||||
end
|
||||
|
||||
function ExportUtil.ConvertFromBase64(exportString)
|
||||
local dataValues = {};
|
||||
for i = 1, #exportString do
|
||||
table.insert(dataValues, Base64CharToNumberConversionTable[string.sub(exportString, i, i)]);
|
||||
end
|
||||
|
||||
return dataValues;
|
||||
end
|
||||
|
||||
function ExportUtil.MakeExportDataStream()
|
||||
return CreateAndInitFromMixin(ExportDataStreamMixin);
|
||||
end
|
||||
|
||||
function ExportUtil.MakeImportDataStream(exportString)
|
||||
return CreateAndInitFromMixin(ImportDataStreamMixin, exportString);
|
||||
end
|
||||
|
||||
|
||||
ExportDataStreamMixin = {};
|
||||
|
||||
function ExportDataStreamMixin:Init()
|
||||
self.dataEntries = {};
|
||||
end
|
||||
|
||||
function ExportDataStreamMixin:AddValue(bitWidth, value)
|
||||
table.insert(self.dataEntries, { bitWidth = bitWidth, value = value, });
|
||||
end
|
||||
|
||||
function ExportDataStreamMixin:GetExportString()
|
||||
return ExportUtil.ConvertToBase64(self.dataEntries);
|
||||
end
|
||||
|
||||
ImportDataStreamMixin = {};
|
||||
|
||||
function ImportDataStreamMixin:Init(exportString)
|
||||
self.dataValues = ExportUtil.ConvertFromBase64(exportString);
|
||||
self.currentIndex = 1;
|
||||
self.currentExtractedBits = 0;
|
||||
self.currentRemainingValue = self.dataValues[1];
|
||||
end
|
||||
|
||||
function ImportDataStreamMixin:ExtractValue(bitWidth)
|
||||
if self.currentIndex > #self.dataValues then
|
||||
return nil;
|
||||
end
|
||||
|
||||
local value = 0;
|
||||
local bitsNeeded = bitWidth;
|
||||
local extractedBits = 0;
|
||||
while bitsNeeded > 0 do
|
||||
local remainingBits = BitsPerChar - self.currentExtractedBits;
|
||||
local bitsToExtract = math.min(remainingBits, bitsNeeded);
|
||||
self.currentExtractedBits = self.currentExtractedBits + bitsToExtract;
|
||||
local maxStorableValue = bit.lshift(1, bitsToExtract);
|
||||
local remainder = self.currentRemainingValue % maxStorableValue;
|
||||
self.currentRemainingValue = bit.rshift(self.currentRemainingValue, bitsToExtract);
|
||||
value = value + bit.lshift(remainder, extractedBits);
|
||||
extractedBits = extractedBits + bitsToExtract;
|
||||
bitsNeeded = bitsNeeded - bitsToExtract;
|
||||
|
||||
if bitsToExtract < remainingBits then
|
||||
break;
|
||||
elseif bitsToExtract >= remainingBits then
|
||||
self.currentIndex = self.currentIndex + 1;
|
||||
self.currentExtractedBits = 0;
|
||||
self.currentRemainingValue = self.dataValues[self.currentIndex];
|
||||
end
|
||||
end
|
||||
|
||||
return value;
|
||||
end
|
||||
|
||||
function ImportDataStreamMixin:GetNumberOfBits()
|
||||
return BitsPerChar * getn(self.dataValues);
|
||||
end
|
||||
|
||||
@@ -1,110 +1,244 @@
|
||||
-- Backport of FrameXML/FormattingUtil.lua (feasible subset) to vanilla
|
||||
-- 1.12 / Lua 5.0.
|
||||
-- Backport of retail Blizzard_SharedXML/FormattingUtil.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- 5.0 notes: no `#`/`%`/string-method syntax — uses string.*, math.mod.
|
||||
-- GetMoneyString gives the text-symbol form ("12g 34s 56c"). The coin-icon form
|
||||
-- (GetCoinTextureString / C_CurrencyInfo.GetCoinTextureString) is provided by the
|
||||
-- ClassicAPI DLL in C++ (src/currency/CoinText.cpp) — like retail, where it's a
|
||||
-- C function — now that the inline-texture backport renders |T markup in 1.12.
|
||||
-- Implementation differences from the retail source:
|
||||
-- - GetMoneyString always gives the coin-icon form, retail's default. Retail
|
||||
-- switches to the text-symbol form ("12g 34s 56c") under its colorblind
|
||||
-- mode, which 1.12 does not have.
|
||||
-- - Not ported: GetCurrencyString / GetCurrenciesString (1.12 has no
|
||||
-- currencies).
|
||||
--
|
||||
-- The FrameXML format-string globals these functions consume
|
||||
-- (LARGE_NUMBER_SEPERATOR, PERCENTAGE_STRING, GOLD_AMOUNT_SYMBOL,
|
||||
-- GOLD_AMOUNT_TEXTURE, …) are defined in the locale layer (locales/enUS.lua),
|
||||
-- which loads first.
|
||||
-- The GlobalStrings these functions consume (LARGE_NUMBER_SEPERATOR,
|
||||
-- PERCENTAGE_STRING, GOLD_AMOUNT_TEXTURE, SYMBOLIC_NEGATIVE_NUMBER, ...) are
|
||||
-- defined in the locale layer (locales/*.lua), which loads first. The coin
|
||||
-- formats there crop the one 1.12 sprite sheet, UI-MoneyIcons.
|
||||
|
||||
function SplitTextIntoLines(text, delimiter)
|
||||
local lines = {};
|
||||
local startIndex = 1;
|
||||
local foundIndex = string.find(text, delimiter);
|
||||
while foundIndex do
|
||||
table.insert(lines, text:sub(startIndex, foundIndex - 1));
|
||||
startIndex = foundIndex + 2;
|
||||
foundIndex = string.find(text, delimiter, startIndex);
|
||||
end
|
||||
if startIndex <= #text then
|
||||
table.insert(lines, text:sub(startIndex));
|
||||
end
|
||||
return lines;
|
||||
end
|
||||
|
||||
function SplitTextIntoHeaderAndNonHeader(text)
|
||||
local foundIndex = string.find(text, "|n");
|
||||
if not foundIndex then
|
||||
-- There was no newline...the whole thing is a header
|
||||
return text;
|
||||
elseif #text == 2 then
|
||||
-- There was a newline, but that was all that was in the string.
|
||||
return nil;
|
||||
elseif foundIndex == 1 then
|
||||
-- There was a newline at the very beginning...the whole rest of the string is a header
|
||||
return text:sub(3);
|
||||
elseif foundIndex == #text - 1 then
|
||||
-- There was a newline at the very end...the whole rest of the string is a header
|
||||
return text:sub(1, foundIndex - 1);
|
||||
else
|
||||
-- There was a newline somewhere in the middle...everything before it is the header and everything after it is the non-header
|
||||
return text:sub(1, foundIndex - 1), text:sub(foundIndex + 2);
|
||||
end
|
||||
end
|
||||
|
||||
function FormatValueWithSign(value)
|
||||
local formatString = value < 0 and SYMBOLIC_NEGATIVE_NUMBER or SYMBOLIC_POSITIVE_NUMBER;
|
||||
return formatString:format(math.abs(value));
|
||||
end
|
||||
|
||||
function FormatLargeNumber(amount)
|
||||
amount = tostring(amount);
|
||||
local newDisplay = "";
|
||||
local strlen = amount:len();
|
||||
--Add each thing behind a comma
|
||||
for i=4, strlen, 3 do
|
||||
newDisplay = LARGE_NUMBER_SEPERATOR..amount:sub(-(i - 1), -(i - 3))..newDisplay;
|
||||
end
|
||||
--Add everything before the first comma
|
||||
newDisplay = amount:sub(1, (strlen % 3 == 0) and 3 or (strlen % 3))..newDisplay;
|
||||
return newDisplay;
|
||||
end
|
||||
|
||||
COPPER_PER_SILVER = 100;
|
||||
SILVER_PER_GOLD = 100;
|
||||
COPPER_PER_GOLD = COPPER_PER_SILVER * SILVER_PER_GOLD;
|
||||
|
||||
function FormatLargeNumber(amount)
|
||||
amount = tostring(amount);
|
||||
local newDisplay = "";
|
||||
local strlen = string.len(amount);
|
||||
-- Insert a separator before each group of three trailing digits.
|
||||
for i = 4, strlen, 3 do
|
||||
newDisplay = LARGE_NUMBER_SEPERATOR ..
|
||||
string.sub(amount, -(i - 1), -(i - 3)) .. newDisplay;
|
||||
end
|
||||
-- Prepend everything before the first separator.
|
||||
local lead = (math.mod(strlen, 3) == 0) and 3 or math.mod(strlen, 3);
|
||||
newDisplay = string.sub(amount, 1, lead) .. newDisplay;
|
||||
return newDisplay;
|
||||
local COPPER_PER_SILVER = COPPER_PER_SILVER;
|
||||
local SILVER_PER_GOLD = SILVER_PER_GOLD;
|
||||
local COPPER_PER_GOLD = COPPER_PER_GOLD;
|
||||
|
||||
MoneyStringConstants =
|
||||
{
|
||||
SeparateThousands = true,
|
||||
DontSeparateThousands = false,
|
||||
CheckGoldThreshold = true,
|
||||
DontCheckGoldThreshold = false,
|
||||
ShowZeroAsGold = true,
|
||||
ShowZeroAsLowerDenomination = false,
|
||||
};
|
||||
|
||||
function GetMoneyString(money, separateThousands, checkGoldThreshold, showZeroAsGold, color)
|
||||
local goldString, silverString, copperString;
|
||||
local gold = floor(money / (COPPER_PER_SILVER * SILVER_PER_GOLD));
|
||||
local silver = floor((money - (gold * COPPER_PER_SILVER * SILVER_PER_GOLD)) / COPPER_PER_SILVER);
|
||||
local copper = mod(money, COPPER_PER_SILVER);
|
||||
|
||||
if (separateThousands) then
|
||||
goldString = GOLD_AMOUNT_TEXTURE_STRING:format(FormatLargeNumber(gold), 0, 0);
|
||||
else
|
||||
goldString = GOLD_AMOUNT_TEXTURE:format(gold, 0, 0);
|
||||
end
|
||||
silverString = SILVER_AMOUNT_TEXTURE:format(silver, 0, 0);
|
||||
copperString = COPPER_AMOUNT_TEXTURE:format(copper, 0, 0);
|
||||
|
||||
if showZeroAsGold and ( money == 0 ) then
|
||||
return color and color:WrapTextInColorCode(goldString) or goldString;
|
||||
end
|
||||
|
||||
copper = FormatDisplayCopper(checkGoldThreshold, gold, silver, copper);
|
||||
|
||||
local moneyString = "";
|
||||
local separator = "";
|
||||
if ( gold > 0 ) then
|
||||
moneyString = goldString;
|
||||
separator = " ";
|
||||
end
|
||||
if ( silver > 0 ) then
|
||||
moneyString = moneyString..separator..silverString;
|
||||
separator = " ";
|
||||
end
|
||||
if ( copper > 0 or moneyString == "" ) then
|
||||
moneyString = moneyString..separator..copperString;
|
||||
end
|
||||
|
||||
return color and color:WrapTextInColorCode(moneyString) or moneyString;
|
||||
end
|
||||
|
||||
function FormatDisplayCopper(checkGoldThreshold, gold, silver, copper)
|
||||
return (checkGoldThreshold and gold > Constants.MoneyFormattingConstants.GOLD_REWARD_THRESHOLD_TO_HIDE_COPPER ) and 0 or copper;
|
||||
end
|
||||
|
||||
function FormatPercentage(percentage, roundToNearestInteger)
|
||||
if roundToNearestInteger then
|
||||
percentage = Round(percentage * 100);
|
||||
else
|
||||
percentage = percentage * 100;
|
||||
end
|
||||
return string.format(PERCENTAGE_STRING, percentage);
|
||||
if roundToNearestInteger then
|
||||
percentage = Round(percentage * 100);
|
||||
else
|
||||
percentage = percentage * 100;
|
||||
end
|
||||
|
||||
return PERCENTAGE_STRING:format(percentage);
|
||||
end
|
||||
|
||||
function FormatPercentageRounded(value)
|
||||
local roundToNearestInteger = true;
|
||||
return FormatPercentage(value, roundToNearestInteger);
|
||||
end
|
||||
|
||||
function FormatFraction(numerator, denominator)
|
||||
return string.format(GENERIC_FRACTION_STRING, numerator, denominator);
|
||||
return GENERIC_FRACTION_STRING:format(numerator, denominator);
|
||||
end
|
||||
|
||||
function SplitTextIntoLines(text, delimiter)
|
||||
local lines = {};
|
||||
local startIndex = 1;
|
||||
local foundIndex = string.find(text, delimiter);
|
||||
while foundIndex do
|
||||
table.insert(lines, string.sub(text, startIndex, foundIndex - 1));
|
||||
startIndex = foundIndex + 2;
|
||||
foundIndex = string.find(text, delimiter, startIndex);
|
||||
end
|
||||
if startIndex <= string.len(text) then
|
||||
table.insert(lines, string.sub(text, startIndex));
|
||||
end
|
||||
return lines;
|
||||
function GetHighlightedNumberDifferenceString(baseString, newString)
|
||||
local outputString = "";
|
||||
-- output string is being built from the new string
|
||||
local newStringIndex = 1;
|
||||
-- find a stretch of digits (including . and , because of different locales) - but has to end in a digit
|
||||
local PATTERN = "([,%.%d]*%d+)";
|
||||
local start1, end1, baseNumberString = string.find(baseString, PATTERN);
|
||||
local start2, end2, newNumberString = string.find(newString, PATTERN);
|
||||
while start1 and start2 do
|
||||
-- add from the new string until the matched spot
|
||||
outputString = outputString .. string.sub(newString, newStringIndex, start2 - 1);
|
||||
newStringIndex = end2 + 1;
|
||||
|
||||
if baseNumberString ~= newNumberString then
|
||||
-- need to remove , and . before comparing numbers because of locales
|
||||
local scrubbedBaseNumberString = gsub(baseNumberString, "[,%.]", "");
|
||||
local scrubbedNewNumberString = gsub(newNumberString, "[,%.]", "");
|
||||
local baseNumber = tonumber(scrubbedBaseNumberString);
|
||||
local newNumber = tonumber(scrubbedNewNumberString);
|
||||
if baseNumber and newNumber then
|
||||
local delta = newNumber - baseNumber;
|
||||
if delta > 0 then
|
||||
newNumberString = GREEN_FONT_COLOR_CODE..string.format(newNumberString)..FONT_COLOR_CODE_CLOSE;
|
||||
elseif delta < 0 then
|
||||
newNumberString = RED_FONT_COLOR_CODE..string.format(newNumberString)..FONT_COLOR_CODE_CLOSE;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
outputString = outputString..newNumberString;
|
||||
|
||||
start1, end1, baseNumberString = string.find(baseString, PATTERN, end1 + 1);
|
||||
start2, end2, newNumberString = string.find(newString, PATTERN, end2 + 1);
|
||||
end
|
||||
|
||||
outputString = outputString .. string.sub(newString, newStringIndex, string.len(newString));
|
||||
return outputString;
|
||||
end
|
||||
|
||||
function SplitTextIntoHeaderAndNonHeader(text)
|
||||
local foundIndex = string.find(text, "|n");
|
||||
local len = string.len(text);
|
||||
if not foundIndex then
|
||||
return text; -- no newline: whole thing is a header
|
||||
elseif len == 2 then
|
||||
return nil; -- the string was just a newline
|
||||
elseif foundIndex == 1 then
|
||||
return string.sub(text, 3); -- leading newline: rest is a header
|
||||
elseif foundIndex == len - 1 then
|
||||
return string.sub(text, 1, foundIndex - 1); -- trailing newline
|
||||
else
|
||||
return string.sub(text, 1, foundIndex - 1), string.sub(text, foundIndex + 2);
|
||||
end
|
||||
function FormatUnreadMailTooltip(tooltip, headerText, senders)
|
||||
for i, sender in ipairs(senders) do
|
||||
headerText = headerText.."\n"..sender;
|
||||
end
|
||||
|
||||
tooltip:SetText(headerText);
|
||||
end
|
||||
|
||||
-- (money, separateThousands [, checkGoldThreshold, showZeroAsGold])
|
||||
-- Text-symbol form ("12g 34s 56c"); for the coin-icon form use
|
||||
-- GetCoinTextureString below. The checkGoldThreshold arg is accepted for
|
||||
-- signature compatibility but not applied (it relied on modern Constants tables).
|
||||
function GetMoneyString(money, separateThousands, checkGoldThreshold, showZeroAsGold)
|
||||
local gold = math.floor(money / COPPER_PER_GOLD);
|
||||
local silver = math.floor(math.mod(money, COPPER_PER_GOLD) / COPPER_PER_SILVER);
|
||||
local copper = math.mod(money, COPPER_PER_SILVER);
|
||||
FormattingUtil = {};
|
||||
|
||||
local goldString = (separateThousands and FormatLargeNumber(gold) or tostring(gold)) ..
|
||||
GOLD_AMOUNT_SYMBOL;
|
||||
local silverString = silver .. SILVER_AMOUNT_SYMBOL;
|
||||
local copperString = copper .. COPPER_AMOUNT_SYMBOL;
|
||||
function FormattingUtil.GetCostString(icon, quantity, colorCode, abbreviate)
|
||||
colorCode = colorCode or HIGHLIGHT_FONT_COLOR_CODE;
|
||||
|
||||
if showZeroAsGold and money == 0 then
|
||||
return goldString;
|
||||
end
|
||||
|
||||
local moneyString = "";
|
||||
local separator = "";
|
||||
if gold > 0 then
|
||||
moneyString = goldString;
|
||||
separator = " ";
|
||||
end
|
||||
if silver > 0 then
|
||||
moneyString = moneyString .. separator .. silverString;
|
||||
separator = " ";
|
||||
end
|
||||
if copper > 0 or moneyString == "" then
|
||||
moneyString = moneyString .. separator .. copperString;
|
||||
end
|
||||
return moneyString;
|
||||
local markup = CreateTextureMarkup(icon, 64, 64, 16, 16, 0, 1, 0, 1);
|
||||
local amountString;
|
||||
if abbreviate then
|
||||
amountString = AbbreviateNumbers(quantity);
|
||||
else
|
||||
amountString = BreakUpLargeNumbers(quantity);
|
||||
end
|
||||
return ("%s%s %s|r"):format(colorCode, amountString, markup);
|
||||
end
|
||||
|
||||
function FormattingUtil.GetItemCostString(itemID, quantity, colorCode, abbreviate)
|
||||
local icon = C_Item.GetItemIconByID(itemID);
|
||||
if icon then
|
||||
return FormattingUtil.GetCostString(icon, quantity, colorCode, abbreviate);
|
||||
end
|
||||
|
||||
return "";
|
||||
end
|
||||
|
||||
function FormattingUtil.AddLeadingZeroes(digits, numZeroes, zeroesColor)
|
||||
numZeroes = numZeroes or 7;
|
||||
zeroesColor = zeroesColor or GRAY_FONT_COLOR;
|
||||
|
||||
local digitsString = tostring(digits);
|
||||
local numDigits = #digitsString;
|
||||
if numDigits < numZeroes then
|
||||
local leadingZeroes = "";
|
||||
for i = 1, (numZeroes - numDigits) do
|
||||
leadingZeroes = leadingZeroes.."0";
|
||||
end
|
||||
|
||||
leadingZeroes = zeroesColor:WrapTextInColorCode(leadingZeroes);
|
||||
|
||||
return leadingZeroes .. digits;
|
||||
end
|
||||
|
||||
return digits;
|
||||
end
|
||||
|
||||
function ReplaceGenderTokens(string, gender)
|
||||
if not strfind(string, "%$") then
|
||||
return string;
|
||||
end
|
||||
|
||||
-- This is a very simple parser that will only handle $G/$g tokens
|
||||
return gsub(string, "$[Gg]([^:]+):([^;]+);", "%"..gender);
|
||||
end
|
||||
|
||||
@@ -1,71 +1,191 @@
|
||||
-- Backport of FrameXML/FrameUtil.lua (core subset) to vanilla 1.12.
|
||||
-- Backport of retail Blizzard_SharedXMLBase/FrameUtil.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- Only the dependency-light, commonly-called helpers are ported. Omitted:
|
||||
-- - the fade/flash functions (UIFrameFade*/UIFrameFlash*) are already
|
||||
-- native in 1.12.
|
||||
-- - the scale/layout/secure helpers (rely on APIs absent here).
|
||||
-- Implementation differences from the retail source:
|
||||
-- - Not ported: the fade and flash functions (UIFrameFade*, UIFrameFlash*,
|
||||
-- FADEFRAMES, FLASHFRAMES). 1.12's FrameXML defines them in UIParent.lua,
|
||||
-- and UIParent's OnUpdate drives them.
|
||||
-- - Not ported: FrameUtil.RegisterFrameForEventCallbacks,
|
||||
-- UnregisterFrameForEventCallbacks and RegisterFrameForEventCallbackFields
|
||||
-- (no frame:RegisterEventCallback), and ApplyDefaultScale (no
|
||||
-- GetDefaultScale).
|
||||
-- - FrameUtil.SetParentMaintainRenderLayering does not carry the frame's
|
||||
-- window over. 1.12 has one window and no GetWindow/SetWindow.
|
||||
-- - IsScrollingMessageFrame, at the bottom, is from retail's
|
||||
-- Blizzard_SharedXML/ScrollingMessageFrame.lua.
|
||||
|
||||
FrameUtil = FrameUtil or {};
|
||||
FrameUtil = {};
|
||||
|
||||
function FrameUtil.RegisterFrameForEvents(frame, events)
|
||||
for i, event in ipairs(events) do
|
||||
frame:RegisterEvent(event);
|
||||
end
|
||||
function FrameUtil.RegisterUpdateFunction(frame, frequencySeconds, func)
|
||||
-- Prevents the OnUpdate handler from running the same frame it was
|
||||
-- removed.
|
||||
frame.canUpdate = true;
|
||||
|
||||
local elapsed = frequencySeconds;
|
||||
frame:SetScript("OnUpdate", function(self, dt)
|
||||
if self.canUpdate then
|
||||
elapsed = elapsed - dt;
|
||||
if elapsed <= 0 then
|
||||
elapsed = frequencySeconds;
|
||||
func(frame, dt);
|
||||
end
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
function FrameUtil.RegisterFrameForUnitEvents(frame, events, ...)
|
||||
for i, event in ipairs(events) do
|
||||
frame:RegisterUnitEvent(event, unpack(arg));
|
||||
end
|
||||
function FrameUtil.UnregisterUpdateFunction(frame)
|
||||
frame.canUpdate = false;
|
||||
frame:SetScript("OnUpdate", nil);
|
||||
end
|
||||
|
||||
function FrameUtil.RegisterFrameForEvents(frame, events)
|
||||
for i, event in ipairs(events) do
|
||||
frame:RegisterEvent(event);
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.UnregisterFrameForEvents(frame, events)
|
||||
for i, event in ipairs(events) do
|
||||
frame:UnregisterEvent(event);
|
||||
end
|
||||
for i, event in ipairs(events) do
|
||||
frame:UnregisterEvent(event);
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.RegisterFrameForUnitEvents(frame, events, ...)
|
||||
for i, event in ipairs(events) do
|
||||
frame:RegisterUnitEvent(event, ...);
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.DialogStyleGlobalMouseDown(frame, buttonName, ...)
|
||||
local mouseFoci = GetMouseFoci();
|
||||
if DoesAncestryIncludeAny(frame, mouseFoci) then
|
||||
return;
|
||||
end
|
||||
|
||||
for i = 1, select("#", ...) do
|
||||
local alternateFrame = select(i, ...);
|
||||
if DoesAncestryIncludeAny(alternateFrame, mouseFoci) then
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
frame:Hide();
|
||||
end
|
||||
|
||||
local StandardScriptHandlerSet = {
|
||||
OnLoad = true,
|
||||
OnShow = true,
|
||||
OnHide = true,
|
||||
OnEvent = true,
|
||||
OnEnter = true,
|
||||
OnLeave = true,
|
||||
OnClick = true,
|
||||
OnDragStart = true,
|
||||
OnReceiveDrag = true,
|
||||
|
||||
-- Other scripts can/should be added here as needed.
|
||||
|
||||
-- Many OnUpdates are set dynamically. Leave this off for now.
|
||||
-- OnUpdate = false,
|
||||
};
|
||||
|
||||
-- ... is a list of tables to mixin.
|
||||
function FrameUtil.SpecializeFrameWithMixins(frame, ...)
|
||||
Mixin(frame, ...);
|
||||
FrameUtil.ReflectStandardScriptHandlers(frame);
|
||||
end
|
||||
|
||||
function FrameUtil.ReflectStandardScriptHandlers(frame)
|
||||
for scriptHandlerKey, shouldBeSet in pairs(StandardScriptHandlerSet) do
|
||||
local scriptHandler = frame[scriptHandlerKey];
|
||||
if scriptHandler ~= nil then
|
||||
frame:SetScript(scriptHandlerKey, scriptHandler);
|
||||
end
|
||||
end
|
||||
|
||||
if frame.OnLoad then
|
||||
frame:OnLoad();
|
||||
end
|
||||
|
||||
if frame.OnShow and frame:IsVisible() then
|
||||
frame:OnShow();
|
||||
end
|
||||
end
|
||||
|
||||
-- This doesn't strictly speaking require a frame, but that's the likely usage of it so I'm leaving it here for now.
|
||||
function FrameUtil.RegisterForVariablesLoaded(frame, loadMethod)
|
||||
if frame.savedVariablesEventRegistered then
|
||||
error("Cannot re-register for variables loaded.");
|
||||
return;
|
||||
end
|
||||
|
||||
frame.savedVariablesEventRegistered = true;
|
||||
|
||||
local function VariablesLoadedCallback(callbackSelf)
|
||||
loadMethod(frame);
|
||||
end
|
||||
|
||||
EventUtil.ContinueOnVariablesLoaded(VariablesLoadedCallback);
|
||||
end
|
||||
|
||||
-- Only expected to return differently in glues and in-game.
|
||||
function FrameUtil.GetRootParent(frame)
|
||||
local parent = frame:GetParent();
|
||||
while parent do
|
||||
local nextParent = parent:GetParent();
|
||||
if not nextParent then
|
||||
break;
|
||||
end
|
||||
parent = nextParent;
|
||||
end
|
||||
return parent;
|
||||
local parent = frame:GetParent();
|
||||
while parent do
|
||||
local nextParent = parent:GetParent();
|
||||
if not nextParent then
|
||||
break;
|
||||
end
|
||||
parent = nextParent;
|
||||
end
|
||||
return parent;
|
||||
end
|
||||
|
||||
-- Reparents while preserving the frame's render layering.
|
||||
function FrameUtil.SetParentMaintainRenderLayering(frame, parent)
|
||||
local origStrata = frame:GetFrameStrata();
|
||||
local origFrameLevel = frame:GetFrameLevel();
|
||||
frame:SetParent(parent);
|
||||
frame:SetFrameStrata(origStrata);
|
||||
frame:SetFrameLevel(origFrameLevel);
|
||||
function FrameUtil.CreateFrame(name, parent, template)
|
||||
-- NOTE: This use of the template type is not strictly correct, but should mostly work.
|
||||
local templateInfo = C_XMLUtil.GetTemplateInfo(template);
|
||||
local frameType = templateInfo and templateInfo.type or "Frame";
|
||||
return CreateFrame(frameType, name, parent, template);
|
||||
end
|
||||
|
||||
function FrameUtil.UpdateScaleForFit(frame, extraWidth, extraHeight)
|
||||
extraWidth = extraWidth or 0;
|
||||
extraHeight = extraHeight or 0;
|
||||
|
||||
FrameUtil.UpdateScaleForFitSpecific(frame, frame:GetUnscaledWidth() + extraWidth, frame:GetUnscaledHeight() + extraHeight);
|
||||
end
|
||||
|
||||
function FrameUtil.UpdateScaleForFitSpecific(frame, specificWidth, specificHeight)
|
||||
frame:SetScale(1);
|
||||
|
||||
local topLevelParent = GetAppropriateTopLevelParent();
|
||||
local horizRatio = topLevelParent:GetUnscaledWidth() / (specificWidth or frame:GetUnscaledWidth());
|
||||
local vertRatio = topLevelParent:GetUnscaledHeight() / (specificHeight or frame:GetUnscaledHeight());
|
||||
|
||||
if (horizRatio < 1 or vertRatio < 1) then
|
||||
frame:SetScale(min(horizRatio, vertRatio));
|
||||
end
|
||||
end
|
||||
|
||||
function DoesAncestryInclude(ancestry, frame)
|
||||
if ancestry then
|
||||
local currentFrame = frame;
|
||||
while currentFrame do
|
||||
if currentFrame == ancestry then
|
||||
return true;
|
||||
end
|
||||
currentFrame = currentFrame:GetParent();
|
||||
end
|
||||
end
|
||||
return false;
|
||||
if ancestry then
|
||||
local currentFrame = frame;
|
||||
while currentFrame do
|
||||
if currentFrame == ancestry then
|
||||
return true;
|
||||
end
|
||||
currentFrame = currentFrame:GetParent();
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function DoesAncestryIncludeAny(ancestry, frames)
|
||||
for _, frame in ipairs(frames) do
|
||||
if DoesAncestryInclude(ancestry, frame) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return false;
|
||||
for _, frame in ipairs(frames) do
|
||||
if DoesAncestryInclude(ancestry, frame) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function GetUnscaledFrameRect(frame, scale)
|
||||
@@ -79,6 +199,29 @@ function GetUnscaledFrameRect(frame, scale)
|
||||
return frameLeft / scale, frameBottom / scale, frameWidth / scale, frameHeight / scale;
|
||||
end
|
||||
|
||||
function GetScaledCenter(frame)
|
||||
local x, y = frame:GetCenter();
|
||||
local effectiveScale = frame:GetEffectiveScale();
|
||||
if x == nil or y == nil or effectiveScale == nil then
|
||||
-- Defaulted returned for diagnosing invalid rects in layout frames.
|
||||
local defaulted = true;
|
||||
return 0, 0, defaulted;
|
||||
end
|
||||
|
||||
return x * effectiveScale, y * effectiveScale;
|
||||
end
|
||||
|
||||
function FitToParent(parent, frame)
|
||||
local horizRatio = parent:GetUnscaledWidth() / frame:GetUnscaledWidth();
|
||||
local vertRatio = parent:GetUnscaledHeight() / frame:GetUnscaledHeight();
|
||||
|
||||
if ( horizRatio < 1 or vertRatio < 1 ) then
|
||||
frame:SetScale(min(horizRatio, vertRatio));
|
||||
frame:SetPoint("CENTER", 0, 0);
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local alternateTopLevelParent;
|
||||
function SetAlternateTopLevelParent(parent)
|
||||
alternateTopLevelParent = parent;
|
||||
@@ -105,11 +248,171 @@ function SetAppropriateTopLevelParent(frame)
|
||||
frame:SetParent(parent);
|
||||
end
|
||||
end
|
||||
|
||||
function GetAppropriateTooltip()
|
||||
return UIParent and GameTooltip or GlueTooltip;
|
||||
end
|
||||
|
||||
-- Sets a frame parent, but retains old frame strata and frame level
|
||||
function FrameUtil.SetParentMaintainRenderLayering(frame, parent)
|
||||
local origStrata = frame:GetFrameStrata();
|
||||
local origFrameLevel = frame:GetFrameLevel();
|
||||
|
||||
frame:SetParent(parent);
|
||||
frame:SetFrameStrata(origStrata);
|
||||
frame:SetFrameLevel(origFrameLevel);
|
||||
end
|
||||
|
||||
-- Sets a frame to the appropriate top level parent.
|
||||
function FrameUtil.RegisterForTopLevelParentChanged(frame)
|
||||
EventRegistry:RegisterCallback("UI.AlternateTopLevelParentChanged", function (callbackFrame)
|
||||
FrameUtil.UpdateTopLevelParent(frame);
|
||||
end, frame);
|
||||
end
|
||||
|
||||
-- Sets a frame to the appropriate top level parent.
|
||||
function FrameUtil.UpdateTopLevelParent(frame)
|
||||
local oldParent = frame:GetParent();
|
||||
local newParent = GetAppropriateTopLevelParent();
|
||||
if newParent and newParent ~= oldParent then
|
||||
FrameUtil.SetParentMaintainRenderLayering(frame, newParent);
|
||||
end
|
||||
end
|
||||
|
||||
-- AnimTransitionMixin - A configurable mixin for animated transitions when showing and hiding.
|
||||
-- Alpha and/or scale could be added in the future if needed.
|
||||
|
||||
-- Mixes in and initializes the AnimTransitionMixin to a frame to give it animated show/hide capabilities.
|
||||
-- config: see DEFAULT_ANIM_TRANSITION_CONFIG for parameters.
|
||||
function FrameUtil.InitFrameAnimTransition(frame, config)
|
||||
Mixin(frame, AnimTransitionMixin);
|
||||
frame:InitAnimTransition(config);
|
||||
end
|
||||
|
||||
AnimTransitionMixinTransitionType = {
|
||||
SlideLeft = 1,
|
||||
SlideRight = 2,
|
||||
SlideTop = 3,
|
||||
SlideBottom = 4,
|
||||
};
|
||||
|
||||
AnimTransitionMixin = {};
|
||||
|
||||
local DEFAULT_ANIM_TRANSITION_CONFIG = {
|
||||
distanceX = nil, -- If nil, uses frame width
|
||||
distanceY = nil, -- If nil, uses frame height
|
||||
translationType = nil, -- Value from AnimTransitionMixinTransitionType enum
|
||||
duration = 0.3, -- Animation duration in seconds
|
||||
easingFunction = nil, -- Easing function from EasingUtil
|
||||
hideOnAnimOut = false, -- Hide the frame when out animation completes
|
||||
onAnimInFinish = nil, -- Callback when in animation finishes
|
||||
onAnimOutFinish = nil, -- Callback when out animation finishes
|
||||
};
|
||||
|
||||
-- Initialize the animation configuration for this instance
|
||||
-- config: A table with optional properties matching DEFAULT_ANIM_TRANSITION_CONFIG
|
||||
function AnimTransitionMixin:InitAnimTransition(config)
|
||||
config = config or {};
|
||||
|
||||
local defaultConfig = CopyTable(DEFAULT_ANIM_TRANSITION_CONFIG);
|
||||
self.animTransitionConfig = Mixin(defaultConfig, config);
|
||||
end
|
||||
|
||||
function AnimTransitionMixin:StartAnimIn()
|
||||
if not self.animTransitionConfig then
|
||||
error("AnimTransitionMixin: Must call InitAnimTransition before StartAnimIn");
|
||||
return;
|
||||
end
|
||||
|
||||
self:StartAnimTransition(false);
|
||||
end
|
||||
|
||||
function AnimTransitionMixin:StartAnimOut()
|
||||
if not self.animTransitionConfig then
|
||||
error("AnimTransitionMixin: Must call InitAnimTransition before StartAnimOut");
|
||||
return;
|
||||
end
|
||||
|
||||
self:StartAnimTransition(true);
|
||||
end
|
||||
|
||||
function AnimTransitionMixin:CancelAnimTransition()
|
||||
if self.animTransitionCancelFunc then
|
||||
self.animTransitionCancelFunc();
|
||||
self.animTransitionCancelFunc = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- Only meant to be used internally. Call StartAnimIn or StartAnimOut instead.
|
||||
function AnimTransitionMixin:StartAnimTransition(isOut)
|
||||
self:CancelAnimTransition();
|
||||
|
||||
local config = self.animTransitionConfig;
|
||||
|
||||
if not isOut then
|
||||
self:Show();
|
||||
end
|
||||
|
||||
local distanceX, distanceY = self:GetTranslationDeltas(config.translationType, isOut);
|
||||
|
||||
local needsClampRestore = false;
|
||||
if distanceX ~= 0 or distanceY ~= 0 then
|
||||
if self:IsClampedToScreen() then
|
||||
needsClampRestore = true;
|
||||
self:SetClampedToScreen(false);
|
||||
end
|
||||
|
||||
-- Set starting state for "in" animation (reverse of the final state)
|
||||
if not isOut then
|
||||
self:AdjustPointsOffset(-distanceX, -distanceY);
|
||||
end
|
||||
end
|
||||
|
||||
local variationCallback = ScriptAnimationUtil.GenerateEasedVariationCallback(config.easingFunction, distanceX, distanceY);
|
||||
|
||||
local function OnAnimFinish()
|
||||
if isOut and config.hideOnAnimOut then
|
||||
self:Hide();
|
||||
end
|
||||
|
||||
if needsClampRestore then
|
||||
self:SetClampedToScreen(true);
|
||||
end
|
||||
|
||||
if isOut then
|
||||
if distanceX ~= 0 or distanceY ~= 0 then
|
||||
self:AdjustPointsOffset(-distanceX, -distanceY);
|
||||
end
|
||||
end
|
||||
|
||||
self.animTransitionCancelFunc = nil;
|
||||
|
||||
local callback = isOut and config.onAnimOutFinish or config.onAnimInFinish;
|
||||
if callback then
|
||||
callback(self);
|
||||
end
|
||||
end
|
||||
|
||||
self.animTransitionCancelFunc = ScriptAnimationUtil.StartScriptAnimation(self, variationCallback, config.duration, OnAnimFinish);
|
||||
end
|
||||
|
||||
function AnimTransitionMixin:GetTranslationDeltas(translationType, isOut)
|
||||
if translationType == AnimTransitionMixinTransitionType.SlideLeft then
|
||||
local width = self.animTransitionConfig.distanceX or self:GetUnscaledWidth();
|
||||
return isOut and -width or width, 0;
|
||||
elseif translationType == AnimTransitionMixinTransitionType.SlideRight then
|
||||
local width = self.animTransitionConfig.distanceX or self:GetUnscaledWidth();
|
||||
return isOut and width or -width, 0;
|
||||
elseif translationType == AnimTransitionMixinTransitionType.SlideTop then
|
||||
local height = self.animTransitionConfig.distanceY or self:GetUnscaledHeight();
|
||||
return 0, isOut and height or -height;
|
||||
elseif translationType == AnimTransitionMixinTransitionType.SlideBottom then
|
||||
local height = self.animTransitionConfig.distanceY or self:GetUnscaledHeight();
|
||||
return 0, isOut and -height or height;
|
||||
end
|
||||
|
||||
return 0, 0;
|
||||
end
|
||||
|
||||
-- From retail's Blizzard_SharedXML/ScrollingMessageFrame.lua, where the key
|
||||
-- comes from the Lua intrinsic's XML. 1.12's scrolling message frames are
|
||||
-- native and have no such key.
|
||||
|
||||
@@ -53,10 +53,10 @@ function GameTooltip_ShowCompareItem(self, shift)
|
||||
if ( self:GetAnchorType() and self:GetAnchorType() ~= "ANCHOR_PRESERVE" ) then
|
||||
local totalWidth = 0;
|
||||
if ( item1 ) then
|
||||
totalWidth = totalWidth + shoppingTooltip1:GetWidth();
|
||||
totalWidth = totalWidth + shoppingTooltip1:GetUnscaledWidth();
|
||||
end
|
||||
if ( item2 ) then
|
||||
totalWidth = totalWidth + shoppingTooltip2:GetWidth();
|
||||
totalWidth = totalWidth + shoppingTooltip2:GetUnscaledWidth();
|
||||
end
|
||||
|
||||
if ( (side == "left") and (totalWidth > leftPos) ) then
|
||||
@@ -91,6 +91,13 @@ function GameTooltip_ShowCompareItem(self, shift)
|
||||
end
|
||||
end
|
||||
|
||||
-- Retail's automatic comparison (the alwaysCompareItems cvar) skips a
|
||||
-- tooltip with this flag. Comparison here runs only when a caller asks for
|
||||
-- it, so nothing reads the flag.
|
||||
function GameTooltip_SuppressAutomaticCompareItem(tooltip)
|
||||
tooltip.suppressAutomaticCompareItem = true;
|
||||
end
|
||||
|
||||
function GameTooltip_SetTitle(tooltip, text, overrideColor, wrap)
|
||||
tooltip:ClearLines();
|
||||
GameTooltip_AddColoredLine(tooltip, text, overrideColor or HIGHLIGHT_FONT_COLOR, wrap)
|
||||
@@ -100,6 +107,10 @@ function GameTooltip_AddNormalLine(tooltip, text, wrap, leftOffset)
|
||||
GameTooltip_AddColoredLine(tooltip, text, NORMAL_FONT_COLOR, wrap, leftOffset);
|
||||
end
|
||||
|
||||
function GameTooltip_AddHighlightLine(tooltip, text, wrap, leftOffset)
|
||||
GameTooltip_AddColoredLine(tooltip, text, HIGHLIGHT_FONT_COLOR, wrap, leftOffset);
|
||||
end
|
||||
|
||||
function GameTooltip_AddColoredLine(tooltip, text, color, wrap, leftOffset)
|
||||
local r, g, b = color:GetRGB();
|
||||
if wrap == nil then
|
||||
@@ -107,3 +118,12 @@ function GameTooltip_AddColoredLine(tooltip, text, color, wrap, leftOffset)
|
||||
end
|
||||
tooltip:AddLine(text, r, g, b, wrap, leftOffset);
|
||||
end
|
||||
|
||||
function GameTooltip_AddColoredDoubleLine(tooltip, leftText, rightText, leftColor, rightColor, wrap, leftOffset)
|
||||
local leftR, leftG, leftB = leftColor:GetRGB();
|
||||
local rightR, rightG, rightB = rightColor:GetRGB();
|
||||
if wrap == nil then
|
||||
wrap = true;
|
||||
end
|
||||
tooltip:AddDoubleLine(leftText, rightText, leftR, leftG, leftB, rightR, rightG, rightB, wrap, leftOffset);
|
||||
end
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
|
||||
EventRegistry = CreateFromMixins(CallbackRegistryMixin);
|
||||
|
||||
-- Keep every instance field in OnLoad. Addons derive their own registry from
|
||||
-- this instance (pfUI: CreateFromMixins(EventRegistry) then :OnLoad()), and a
|
||||
-- field set anywhere else would be shared with the global registry.
|
||||
function EventRegistry:OnLoad()
|
||||
CallbackRegistryMixin.OnLoad(self);
|
||||
self:SetUndefinedEventsAllowed(true);
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
-- GridLayoutManagerMixin is used to house all the different specifications required to described different layouts.
|
||||
--
|
||||
-- The primary elements of GridLayoutManagerMixin are:
|
||||
-- primarySizeCalculator: how to measure regions. Typically GetWidth or GetHeight.
|
||||
-- secondarySizeCalculator: how to measure regions. Typically GetWidth or GetHeight.
|
||||
-- primarySizeCalculator: how to measure regions. Typically GetUnscaledWidth or GetUnscaledHeight.
|
||||
-- secondarySizeCalculator: how to measure regions. Typically GetUnscaledWidth or GetUnscaledHeight.
|
||||
-- cellSizeCalculator: how to measure cell size of regions. Optional. Typically only used for cell-size-based layout.
|
||||
-- primaryMultiplier: to be used when applying offsets. Typically 1 or -1.
|
||||
-- secondaryMultiplier: to be used when applying offsets. Typically 1 or -1.
|
||||
@@ -61,8 +61,8 @@ function GridLayoutUtil.GridLayoutRegions(regions, initialAnchor, layout)
|
||||
end
|
||||
|
||||
local isColumnBased = layout.isColumnBased;
|
||||
local primarySizeCalculator = isColumnBased and regions[1].GetHeight or regions[1].GetWidth;
|
||||
local secondarySizeCalculator = isColumnBased and regions[1].GetWidth or regions[1].GetHeight;
|
||||
local primarySizeCalculator = isColumnBased and regions[1].GetUnscaledHeight or regions[1].GetUnscaledWidth;
|
||||
local secondarySizeCalculator = isColumnBased and regions[1].GetUnscaledWidth or regions[1].GetUnscaledHeight;
|
||||
local primarySizePadding = isColumnBased and layout.paddingY or layout.paddingX;
|
||||
local secondarySizePadding = isColumnBased and layout.paddingX or layout.paddingY;
|
||||
local primaryMultiplier = isColumnBased and layout.direction.y or layout.direction.x;
|
||||
@@ -152,8 +152,8 @@ function GridLayoutUtil.ApplyGridLayout(regions, initialAnchor, gridLayout)
|
||||
end
|
||||
|
||||
local isColumnBased = gridLayout.isColumnBased;
|
||||
local primarySizeCalculator = gridLayout.primarySizeCalculator or (isColumnBased and regions[1].GetHeight or regions[1].GetWidth);
|
||||
local secondarySizeCalculator = gridLayout.secondarySizeCalculator or (isColumnBased and regions[1].GetWidth or regions[1].GetHeight);
|
||||
local primarySizeCalculator = gridLayout.primarySizeCalculator or (isColumnBased and regions[1].GetUnscaledHeight or regions[1].GetUnscaledWidth);
|
||||
local secondarySizeCalculator = gridLayout.secondarySizeCalculator or (isColumnBased and regions[1].GetUnscaledWidth or regions[1].GetUnscaledHeight);
|
||||
local layoutManager = CreateAndInitFromMixin(GridLayoutManagerMixin, primarySizeCalculator, secondarySizeCalculator, gridLayout.primaryMultiplier, gridLayout.secondaryMultiplier,
|
||||
gridLayout.primarySizePadding, gridLayout.secondarySizePadding);
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/IndexRangeDataProvider.lua.
|
||||
|
||||
-- IndexRangeDataProviderMixin originally written for parity with ScrollBox API to
|
||||
-- simulate extremely large ranges (i.e. 20,000 macro icons). As a result, this
|
||||
-- data provider has a minimal API.
|
||||
IndexRangeDataProviderMixin = CreateFromMixins(CallbackRegistryMixin);
|
||||
|
||||
IndexRangeDataProviderMixin:GenerateCallbackEvents(
|
||||
{
|
||||
"OnSizeChanged",
|
||||
}
|
||||
);
|
||||
|
||||
function IndexRangeDataProviderMixin:Init(size)
|
||||
CallbackRegistryMixin.OnLoad(self);
|
||||
|
||||
self:SetSize(size);
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:IsVirtual()
|
||||
return true;
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:Enumerate(indexBegin, indexEnd)
|
||||
indexBegin = indexBegin and (indexBegin - 1) or 0;
|
||||
indexEnd = indexEnd or self.size;
|
||||
|
||||
local function Enumerator(invariant, index)
|
||||
index = index + 1;
|
||||
if index <= indexEnd then
|
||||
return index, index;
|
||||
end
|
||||
end
|
||||
|
||||
local invariant = nil;
|
||||
return Enumerator, invariant, indexBegin;
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:EnumerateEntireRange()
|
||||
local indexBegin, indexEnd = nil, nil;
|
||||
return self:Enumerate(indexBegin, indexEnd);
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:GetSize()
|
||||
return self.size;
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:SetSize(size)
|
||||
self.size = math.max(0, size);
|
||||
|
||||
local pendingSort = false;
|
||||
self:TriggerEvent(IndexRangeDataProviderMixin.Event.OnSizeChanged, pendingSort);
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:Flush()
|
||||
self:SetSize(0);
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:Find(index)
|
||||
if index <= self:GetSize() then
|
||||
return index;
|
||||
end
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:FindByPredicate(predicate)
|
||||
for index = 1, self:GetSize() do
|
||||
if predicate(index) then
|
||||
return index;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function IndexRangeDataProviderMixin:ContainsByPredicate(predicate)
|
||||
return self:FindByPredicate(predicate) ~= nil;
|
||||
end
|
||||
|
||||
function CreateIndexRangeDataProvider(size)
|
||||
return CreateAndInitFromMixin(IndexRangeDataProviderMixin, size or 0);
|
||||
end
|
||||
@@ -0,0 +1,354 @@
|
||||
-- Backport of retail's Blizzard_ObjectAPI/Mainline/Item.lua.
|
||||
--
|
||||
-- Differences from the retail source:
|
||||
-- - GetItemQualityColor reads ITEM_QUALITY_COLORS directly; retail's
|
||||
-- ColorManager.GetColorDataForItemQuality returns the same entry.
|
||||
|
||||
Item = {};
|
||||
ItemMixin = {};
|
||||
|
||||
--[[static]] function Item:CreateFromItemLocation(itemLocation)
|
||||
if type(itemLocation) ~= "table" or type(itemLocation.HasAnyLocation) ~= "function" or not itemLocation:HasAnyLocation() then
|
||||
error("Usage: Item:CreateFromItemLocation(notEmptyItemLocation)", 2);
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin);
|
||||
item:SetItemLocation(itemLocation);
|
||||
return item;
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromBagAndSlot(bagID, slotIndex)
|
||||
if type(bagID) ~= "number" or type(slotIndex) ~= "number" then
|
||||
error("Usage: Item:CreateFromBagAndSlot(bagID, slotIndex)", 2);
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin);
|
||||
item:SetItemLocation(ItemLocation:CreateFromBagAndSlot(bagID, slotIndex));
|
||||
return item;
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromEquipmentSlot(equipmentSlotIndex)
|
||||
if type(equipmentSlotIndex) ~= "number" then
|
||||
error("Usage: Item:CreateFromEquipmentSlot(equipmentSlotIndex)", 2);
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin);
|
||||
item:SetItemLocation(ItemLocation:CreateFromEquipmentSlot(equipmentSlotIndex));
|
||||
return item;
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromItemLink(itemLink)
|
||||
if type(itemLink) ~= "string" then
|
||||
error("Usage: Item:CreateFromItemLink(itemLinkString)", 2);
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin);
|
||||
item:SetItemLink(itemLink);
|
||||
return item;
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromItemID(itemID)
|
||||
if type(itemID) ~= "number" then
|
||||
error("Usage: Item:CreateFromItemID(itemID)", 2);
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin);
|
||||
item:SetItemID(itemID);
|
||||
return item;
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromItemGUID(itemGUID)
|
||||
if type(itemGUID) ~= "string" then
|
||||
error("Usage: Item:CreateFromItemGUID(itemGUIDString)", 2);
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin);
|
||||
item:SetItemGUID(itemGUID);
|
||||
return item;
|
||||
end
|
||||
|
||||
--[[static]] function Item:DoItemsMatch(item1, item2)
|
||||
if not item1 or not item2 then
|
||||
return false;
|
||||
end
|
||||
|
||||
return item1:Matches(item2);
|
||||
end
|
||||
|
||||
function ItemMixin:Matches(item)
|
||||
if not item then
|
||||
return false;
|
||||
end
|
||||
|
||||
local itemID = item:GetItemID();
|
||||
if (itemID ~= nil) and (self:GetItemID() == itemID) then
|
||||
return true;
|
||||
end
|
||||
|
||||
local itemLocation = item:GetItemLocation();
|
||||
if (itemLocation ~= nil) and itemLocation:IsEqualTo(self:GetItemLocation()) then
|
||||
return true;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemLocation(itemLocation)
|
||||
self:Clear();
|
||||
self.itemLocation = itemLocation;
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemLink(itemLink)
|
||||
self:Clear();
|
||||
self.itemLink = itemLink;
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemID(itemID)
|
||||
self:Clear();
|
||||
self.itemID = itemID;
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemGUID(itemGUID)
|
||||
self:Clear();
|
||||
self.itemGUID = itemGUID;
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemLocation()
|
||||
if self.itemLocation then
|
||||
return self.itemLocation;
|
||||
end
|
||||
|
||||
if self.itemGUID then
|
||||
return C_Item.GetItemLocation(self.itemGUID);
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemGUID()
|
||||
if self.itemGUID then
|
||||
return self.itemGUID;
|
||||
end
|
||||
|
||||
if self.itemLocation then
|
||||
return C_Item.GetItemGUID(self.itemLocation);
|
||||
end
|
||||
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:HasItemLocation()
|
||||
return self.itemLocation ~= nil;
|
||||
end
|
||||
|
||||
function ItemMixin:Clear()
|
||||
self.itemLocation = nil;
|
||||
self.itemLink = nil;
|
||||
self.itemID = nil;
|
||||
self.itemGUID = nil;
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemEmpty()
|
||||
if self:GetStaticBackingItem() then
|
||||
return not C_Item.DoesItemExistByID(self:GetStaticBackingItem());
|
||||
end
|
||||
|
||||
return not self:IsItemInPlayersControl();
|
||||
end
|
||||
|
||||
function ItemMixin:GetStaticBackingItem()
|
||||
return self.itemLink or self.itemID;
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemInPlayersControl()
|
||||
local itemLocation = self:GetItemLocation();
|
||||
return itemLocation and C_Item.DoesItemExist(itemLocation);
|
||||
end
|
||||
|
||||
-- Item API
|
||||
function ItemMixin:GetItemID()
|
||||
local backingItem = self:GetStaticBackingItem();
|
||||
if backingItem then
|
||||
return C_Item.GetItemInfoInstant(backingItem) or C_Item.GetItemIDForItemInfo(backingItem);
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemID(self:GetItemLocation());
|
||||
end
|
||||
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemLocked()
|
||||
return self:IsItemInPlayersControl() and C_Item.IsLocked(self:GetItemLocation());
|
||||
end
|
||||
|
||||
function ItemMixin:LockItem()
|
||||
if self:IsItemInPlayersControl() then
|
||||
C_Item.LockItem(self:GetItemLocation());
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:UnlockItem()
|
||||
if self:IsItemInPlayersControl() then
|
||||
C_Item.UnlockItem(self:GetItemLocation());
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemIcon() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemIconByID(self:GetStaticBackingItem());
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemIcon(self:GetItemLocation());
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemName() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemNameByID(self:GetStaticBackingItem());
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemName(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemLink() -- requires item data to be loaded
|
||||
if self.itemLink then
|
||||
return self.itemLink;
|
||||
end
|
||||
|
||||
if self.itemID then
|
||||
return (select(2, C_Item.GetItemInfo(self.itemID)));
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemLink(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemQuality() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemQualityByID(self:GetStaticBackingItem());
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemQuality(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:GetStackCount() -- requires item data to be loaded
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetStackCount(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:GetCurrentItemLevel() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return (C_Item.GetDetailedItemLevelInfo(self:GetStaticBackingItem()));
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetCurrentItemLevel(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemQualityColor() -- requires item data to be loaded
|
||||
local itemQuality = self:GetItemQuality();
|
||||
return ITEM_QUALITY_COLORS[itemQuality]; -- may be nil if item data isn't loaded
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemQualityColorRGB() -- requires item data to be loaded
|
||||
local colorTbl = self:GetItemQualityColor();
|
||||
return colorTbl.color:GetRGB();
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemMaxStackSize() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemMaxStackSizeByID(self:GetStaticBackingItem());
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemMaxStackSize(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:IsStackable() -- requires item data to be loaded
|
||||
local maxStackSize = self:GetItemMaxStackSize();
|
||||
return maxStackSize and maxStackSize > 1;
|
||||
end
|
||||
|
||||
function ItemMixin:GetInventoryType()
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemInventoryTypeByID(self:GetStaticBackingItem());
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemInventoryType(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:GetInventoryTypeName()
|
||||
if not self:IsItemEmpty() then
|
||||
return select(4, C_Item.GetItemInfoInstant(self:GetItemID()));
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemDataCached()
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.IsItemDataCachedByID(self:GetStaticBackingItem());
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.IsItemDataCached(self:GetItemLocation());
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
function ItemMixin:IsDataEvictable()
|
||||
-- Item data could be evicted from the cache
|
||||
return true;
|
||||
end
|
||||
|
||||
function ItemMixin:ValidateForContinueOnItemLoad(methodName, callbackFunction)
|
||||
if type(callbackFunction) ~= "function" then
|
||||
error(("Usage: NonEmptyItem:%s(callbackFunction): invalid callbackFunction"):format(methodName), 3);
|
||||
end
|
||||
|
||||
if self:IsItemEmpty() then
|
||||
if self.itemLink then
|
||||
error(("Usage: NonEmptyItem:%s(callbackFunction) invalid itemLink: <%s>"):format(methodName, self.itemLink), 3);
|
||||
elseif self.itemID then
|
||||
error(("Usage: NonEmptyItem:%s(callbackFunction) invalid itemID: <%d>"):format(methodName, self.itemID), 3);
|
||||
end
|
||||
|
||||
error(("Usage: NonEmptyItem:%s(callbackFunction): invalid item"):format(methodName), 3);
|
||||
else
|
||||
if not self:GetItemID() then
|
||||
error(("Usage: NonEmptyItem:%s(callbackFunction): item appeared non-empty, but had invalid itemID. itemID: <%s>, itemLink: <%s>"):format(methodName, tostring(self.itemID), tostring(self.itemLink)), 3);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Add a callback to be executed when item data is loaded, if the item data is already loaded then execute it immediately
|
||||
function ItemMixin:ContinueOnItemLoad(callbackFunction)
|
||||
self:ValidateForContinueOnItemLoad("ContinueOnItemLoad", callbackFunction);
|
||||
ItemEventListener:AddCallback(self:GetItemID(), callbackFunction);
|
||||
end
|
||||
|
||||
-- Same as ContinueOnItemLoad, except it returns a function that when called will cancel the continue
|
||||
function ItemMixin:ContinueWithCancelOnItemLoad(callbackFunction)
|
||||
self:ValidateForContinueOnItemLoad("ContinueWithCancelOnItemLoad", callbackFunction);
|
||||
return ItemEventListener:AddCancelableCallback(self:GetItemID(), callbackFunction);
|
||||
end
|
||||
|
||||
-- Generic aliases for use with ContinuableContainer
|
||||
function ItemMixin:ContinueWithCancelOnRecordLoad(callbackFunction)
|
||||
return self:ContinueWithCancelOnItemLoad(callbackFunction);
|
||||
end
|
||||
|
||||
function ItemMixin:IsRecordDataCached()
|
||||
return self:IsItemDataCached();
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user