mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 07:00:37 +00:00
Compare 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 |
+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")
|
||||
|
||||
@@ -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. |
|
||||
@@ -51,11 +51,12 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
|
||||
| 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` |
|
||||
@@ -72,18 +73,19 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [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` |
|
||||
@@ -104,7 +106,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [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), `wipe`, `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` |
|
||||
@@ -120,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` |
|
||||
@@ -223,6 +226,9 @@ click AddOn Options, then ClassicAPI.
|
||||
| | `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. |
|
||||
@@ -274,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)) |
|
||||
@@ -325,6 +333,11 @@ 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` |
|
||||
@@ -381,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>
|
||||
|
||||
@@ -437,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
|
||||
|
||||
@@ -485,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
|
||||
|
||||
|
||||
+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")
|
||||
+1192
-107
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();
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -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);
|
||||
@@ -581,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;
|
||||
|
||||
+14
@@ -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.
|
||||
|
||||
+375
-76
@@ -359,6 +359,13 @@ enum Offsets {
|
||||
// read as `*(int*)VAR_FRAMESCRIPT_ERROR_HANDLER_REF`, pushed via
|
||||
// lua_rawgeti(REGISTRY, ref). Set once at engine init.
|
||||
VAR_FRAMESCRIPT_ERROR_HANDLER_REF = 0x008722C8,
|
||||
// The C function that ref holds (benilla FrameScriptCallErrorHandler),
|
||||
// `int __fastcall(L)`: when the seterrorhandler ref (0x008722CC) is not
|
||||
// -1 it pushes that function (lua_rawgeti REGISTRY), moves it under the
|
||||
// message (lua_insert -2), lua_call(1, 1)s it and returns 1. No code
|
||||
// calls it directly; state open pushes it as a C closure and refs it
|
||||
// (0x00703A8C). Verified by disassembly.
|
||||
FUN_FRAMESCRIPT_CALL_ERROR_HANDLER = 0x00703B40,
|
||||
// The base Frame script-name resolver — __thiscall(frame, const char *name)
|
||||
// -> int* slot, 0 for an unknown name. Maps the standard base-frame scripts
|
||||
// to their 8-byte {handler, context} slots on the frame (OnLoad@+0x118,
|
||||
@@ -605,6 +612,19 @@ enum Offsets {
|
||||
VAR_SIMPLEFONTSTRING_CLASS_TAG = 0x00846544, // ".?AVCSimpleFontString@@" (alloc debug tag)
|
||||
FUN_REGION_POOL_ALLOC = 0x00760450, // __thiscall(pool, zeroInit=0, tag, line=-2) -> raw mem
|
||||
FUN_SIMPLEFONTSTRING_CTOR = 0x00770D30, // __thiscall(mem, parent, layer, sublayer) -> fs
|
||||
// SimpleRegionDestruct, CSimpleRegion's destructor body — `void
|
||||
// __thiscall(region)`, plain RET: unparents the region, frees its two
|
||||
// arrays, then the script region destructor. Every region's destruction
|
||||
// reaches it, and it has exactly three callers: the base region's deleting
|
||||
// destructor (0x0077F6C6), ~CSimpleTexture (0x0076FD40, tail JMP; called
|
||||
// only by its deleting destructor 0x0076FD10) and ~CSimpleFontString
|
||||
// (0x00770E50, tail JMP; called by its deleting destructor 0x00770E20 and
|
||||
// a subclass's, 0x00789320). The texture and font string vtables are
|
||||
// written only by their constructor and destructor, so no inlined
|
||||
// destructor skips it. The blocks then go back to the region pool, where
|
||||
// they can come back as new regions. Xrefs + decompiles; benilla-re
|
||||
// ui/regions. `Frame::RegionDestruction` hooks it.
|
||||
FUN_REGION_DESTRUCT = 0x0077F6F0,
|
||||
FUN_REGION_SET_NAME = 0x0076C650, // __thiscall(region, name) — registers _G[name]
|
||||
FUN_FONTSTRING_SET_FONT = 0x00770C60, // __thiscall(fs + OFF_FONTSTRING_FONT_HOLDER, fontObject)
|
||||
// CSimpleFontString::SetFont — `uint32_t __thiscall(fs, const char *path,
|
||||
@@ -698,6 +718,18 @@ enum Offsets {
|
||||
VAR_COORD_DDC_HEIGHT_SCALE = 0x00832A48,
|
||||
VAR_COORD_ASPECT_COMPENSATION = 0x00832A4C,
|
||||
UI_COORD_SCALE_UNIT = 1024, // DAT_007ffd68
|
||||
// `SetUiParentScale` — `void __stdcall(float scale)`, `ret 4`. The one
|
||||
// setter of UIParent's scale: raises the value to 0.64, calls SetScale
|
||||
// with force (0x0076AC10), drains the layout queue (0x00768ED0), and does
|
||||
// nothing while VAR_UI_PARENT is null. Its six callers (Ghidra xrefs):
|
||||
// the uiScale and useUiScale cvar callbacks (0x004907C7/0x004907D8,
|
||||
// 0x0049084A/0x0049085D), OnDisplaySizeChanged (0x00492F0E) and the
|
||||
// automatic scale ApplyAutoUiScale (0x0049301D).
|
||||
FUN_SET_UI_PARENT_SCALE = 0x00494550,
|
||||
// CSimpleFrame* UIParent — stored by the game UI's Initialize
|
||||
// (0x00490077) and cleared by its Shutdown (0x00490CA4), so it is null on
|
||||
// the glue screen. Read at 0x0049456A (SetUiParentScale) and 0x00494590.
|
||||
VAR_UI_PARENT = 0x00B4B44C,
|
||||
// gxu text-raster dimensions — INT dwords holding the live render-target
|
||||
// size in PIXELS ({0,0,640,480} fallback), written by FUN_005c2b50 from
|
||||
// the render-target rect (FUN_0058a240) whenever it changes; every font
|
||||
@@ -843,6 +875,42 @@ enum Offsets {
|
||||
// texcoords (no v-flip — the engine region path is top-left origin).
|
||||
FUN_SIMPLETEXTURE_SET_TEXCOORD = 0x00770410,
|
||||
OFF_SIMPLETEXTURE_HTEXTURE = 0xCC, // owned HTEXTURE ref (diagnostic)
|
||||
// The eight coordinates, UL, LL, UR, LR as (x, y) pairs — what GetTexCoord
|
||||
// pushes (0x0079BDF0) and both SetTexCoord forms write. The layer batch
|
||||
// keeps a pointer to them (FUN_00772FD0 0x00773020) and reads them on
|
||||
// every draw.
|
||||
OFF_SIMPLETEXTURE_TEXCOORDS = 0x104,
|
||||
// CSimpleTexture::Emit — `void __thiscall(texture, batch)`, `ret 4`, the
|
||||
// layer-batch rebuild's per-texture call (4.3.4's CSimpleTexture::Draw):
|
||||
// with an image it runs the quad append FUN_00772FD0, whose device-texture
|
||||
// lookup loads the texture, so after it the device texture exists.
|
||||
FUN_SIMPLETEXTURE_EMIT = 0x007706E0,
|
||||
// CSimpleFontString::Emit — `void __thiscall(fs, batch)`, `ret 4`, the
|
||||
// font string's slot of the same call (4.3.4's CSimpleFontString::Draw):
|
||||
// with a font and a nonempty text it runs AppendToBatch (0x00773080),
|
||||
// which adds the built string (+0xF8) when the rect is valid and not
|
||||
// wholly off screen.
|
||||
FUN_FONTSTRING_EMIT = 0x00772E50,
|
||||
// CSimpleFontString::OnColorChanged — `void __thiscall(fs, int changed)`,
|
||||
// `ret 4`, the font string's region slot 8 (VTBL_REGION_RECOLOR): calls the
|
||||
// region colour fold 0x0077FAC0, then, with a built string, hands
|
||||
// colors[0] (or white when the count is not 1) to TextBlockUpdateColor
|
||||
// 0x0044D650 and redoes the shadow. SetVertexColor (0x0077F750) calls it
|
||||
// after storing a colour, and the frame alpha setter (0x0076A690) on each
|
||||
// region, so every change of OFF_FONTSTRING_COLOR_ARRAY ends here.
|
||||
// Verified by disassembly.
|
||||
FUN_FONTSTRING_ON_COLOR_CHANGED = 0x00772180,
|
||||
// A layer batch's entry count: the quad append and the text append each
|
||||
// add one (0x00772FD0, 0x00773080).
|
||||
OFF_LAYER_BATCH_COUNT = 0x4,
|
||||
// A region's vtable slot 0, the deleting destructor `void __thiscall(region,
|
||||
// int flags)`: the frame destructor (0x007693B0) deletes each of its regions
|
||||
// with flags 1.
|
||||
VTBL_REGION_DELETING_DESTRUCTOR = 0x00,
|
||||
// CScriptRegion::NotifyLayerChanged — `void __thiscall(region)`: marks the
|
||||
// region's layer for a batch rebuild. SetTexture (0x007702E2) and SetImage
|
||||
// (0x00770300) call it after storing a new image.
|
||||
FUN_REGION_NOTIFY_LAYER_CHANGED = 0x0077FDF0,
|
||||
// The four drawn-quad corner POSITIONS: 4 vertices of {x, y, z} floats (0xC
|
||||
// stride), order BL, TL, BR, TR (from FUN_REGION_STORE_CORNERS's writes). The
|
||||
// region draw enqueue FUN_00772fd0 (0x00772fd0) hands region+0xD4 straight to
|
||||
@@ -858,9 +926,12 @@ enum Offsets {
|
||||
// populated (verified: the draw gate tests region+0xD4 != 0), and normally
|
||||
// SetPoint→layout-resolve calls this. We call it directly to place an icon by
|
||||
// its screen rect with NO anchors (anchoring 100+ textures/frame corrupts the
|
||||
// UI-manager pending-layout list — the FUN_00765650 crash). Co-hooked by
|
||||
// texture/Rotation.cpp, which re-applies rotation right after the engine
|
||||
// restores the axis-aligned corners (layout resolve / SetTexCoord).
|
||||
// UI-manager pending-layout list — the FUN_00765650 crash). Hooked by
|
||||
// Texture::CornerStore, whose listeners re-apply rotation
|
||||
// (Texture::Transform) and the repeat count (Texture::Tile) right after
|
||||
// the engine restores the axis-aligned corners (layout resolve /
|
||||
// SetTexCoord). Writes only the corners: nothing marks the layer changed,
|
||||
// since the layer batch reads them through a pointer on every draw.
|
||||
FUN_REGION_STORE_CORNERS = 0x007705B0,
|
||||
// CSimpleTexture::OnFrameSizeChanged — `__thiscall(texture+OFF_REGION_ANCHOR,
|
||||
// const float *oldRect)`, the layout callback: lays out dependents, then
|
||||
@@ -951,12 +1022,10 @@ enum Offsets {
|
||||
// {rgba@+4, shown@+8, timeVisibleLeft@+0xC, fadeLeft@+0x10} in the
|
||||
// stride-0x10 line array at smf+0x3A4) animates each visible line
|
||||
// fontstring's alpha byte through this same SetColor every frame, then
|
||||
// hides the fs at fade end. Text::InlineTexturePool mirrors this byte onto
|
||||
// the line's icon regions each tick so inline icons fade with their line
|
||||
// (regions parent to the CHAT FRAME, so the frame's own alpha byte —
|
||||
// frame+0xC8, per Script_GetAlpha 0x00774DC0 — already modulates them via
|
||||
// the engine's parent×region product; only the fs's own component needs
|
||||
// mirroring).
|
||||
// hides the fs at fade end. The byte also carries the frame's alpha
|
||||
// (folded by 0x0077FAC0). Text::InlineTexturePool copies it onto the
|
||||
// line's icon textures from FUN_FONTSTRING_ON_COLOR_CHANGED; the textures
|
||||
// have no frame, so it is the only alpha they get.
|
||||
OFF_FONTSTRING_COLOR_COUNT = 0xB4,
|
||||
OFF_FONTSTRING_COLOR_ARRAY = 0xB8,
|
||||
OFF_TEXTBLOCK_NODE = 0x8, // the gxu text node inside the handle
|
||||
@@ -991,29 +1060,24 @@ enum Offsets {
|
||||
// pass (FUN_005c8fe0) calls per node; safe to call directly (the builder
|
||||
// re-lays the node — a clean node just re-emits the same glyphs). Verified
|
||||
// callers: the paint pass and FUN_005cd4d0 (the width/resize helper).
|
||||
// Text::InlineTexture drives it PRE-RENDER from the SMF-refresh early-apply
|
||||
// so a chat line's icons are recorded before the frame draws.
|
||||
// Text::InlineTexture calls it right after RebuildString (its RefreshIcons,
|
||||
// 4.3.4's RefreshEmbeddedTextures) so a string's icons are recorded and
|
||||
// placed before the string draws.
|
||||
FUN_TEXT_ENSURE_BUILT = 0x005CD6A0,
|
||||
|
||||
// CScrollingMessageFrame display refresh — `__thiscall(smf, int newestIdx)`.
|
||||
// Rebuilds the visible line set bottom-up: per slot it SetTexts the line
|
||||
// fontstring (FUN_00788af0 -> SetText FUN_00771d80 + enqueue-resolve
|
||||
// FUN_007680e0 flag 1), (re)anchors newly-allocated lines, shows/hides by
|
||||
// message. Every new message shifts the newest index, so EVERY visible slot
|
||||
// is re-SetText'd -> its inline icons must be recomputed; the engine builds
|
||||
// the line lazily at paint, so icons land one frame after the glyphs (the
|
||||
// one-frame lag). ScrollingMessageFrame::Layout owns the hook and fans
|
||||
// out after it; Text::InlineTexture listens and, post-refresh,
|
||||
// forces each icon-bearing line to resolve+build and applies its icon
|
||||
// placement synchronously — pre-render (the refresh runs from the SMF's
|
||||
// Lua/event-driven methods, never mid-render), so icons draw with their
|
||||
// glyphs the same frame. Line-entry layout verified from the decompile:
|
||||
// message. ScrollingMessageFrame::Layout owns the hook and fans out around
|
||||
// it. Line-entry layout verified from the decompile:
|
||||
// count@+0x3A0, stride-0x10 entry array@+0x3A4, entry+8 = the line fs.
|
||||
FUN_SMF_DISPLAY_REFRESH = 0x00788750,
|
||||
OFF_SMF_LINE_COUNT = 0x3A0, // int: allocated visible-line count
|
||||
OFF_SMF_LINE_ARRAY = 0x3A4, // base of the stride-0x10 line-entry array
|
||||
SMF_LINE_STRIDE = 0x10, // per-line-entry stride
|
||||
OFF_SMF_LINE_FONTSTRING = 0x8, // entry+8 = the line's CSimpleFontString
|
||||
OFF_SMF_LINE_RECORD = 0xC, // entry+0xC = the ring record it shows (ShowLine 0x00788AF0)
|
||||
// THE pen↔anchor unit bridge. RebuildString (0x7724A0) multiplies every
|
||||
// text-unit quantity by region+0x7C when crossing into node creation:
|
||||
// nodePos = inset×s + rect corner, nodeFontH = fontPx×s, spacing = +0xF4×s,
|
||||
@@ -1959,7 +2023,7 @@ enum Offsets {
|
||||
// (loot sparkle), so bit 0x2000 (4.3.4's focus bit) is taken here, and
|
||||
// UnitXP references 0x0060C578 in its body. Unit::TokenEvents co-hooks
|
||||
// the recompute to keep UNIT_SCRIPT_REG_TOKENS on units that have a
|
||||
// ClassicAPI token (focus, nameplateN, markN).
|
||||
// ClassicAPI token (focus, nameplateN, markN, softenemy, softfriend).
|
||||
OFF_CGUNIT_SCRIPT_REGISTRATION = 0xC90,
|
||||
// ClassicAPI's bit for those units (4.3.4's focus bit is 0x2000, taken
|
||||
// here — see above; 0x4000 is 4.3.4's arena bit).
|
||||
@@ -2049,6 +2113,25 @@ enum Offsets {
|
||||
// own text is empty), as 4.3.4 calls UnitCombatLogSpellFail. Returns for a
|
||||
// null player or spell (disassembly, 0x0062C37C-0x0062C38E).
|
||||
FUN_COMBAT_LOG_SPELL_FAIL = 0x0062C360,
|
||||
// LoggingCombat's flag (s_loggingFlags[1]; [0] at 0x00B4FDB4 is chat):
|
||||
// nonzero while the combat log is written to a file. Cleared by the chat
|
||||
// teardown (0x00498700). Disassembled, 0x0049FE70 / 0x0049B1D3.
|
||||
VAR_COMBAT_LOGGING = 0x00B4FDB8,
|
||||
// Storm SLogCreate — `bool __stdcall(const char *path, uint32_t flags,
|
||||
// uint32_t *log)`. Flag 4 appends; the file is opened (and its folders
|
||||
// made) on the first write. LoggingCombat opens Logs\WoWCombatLog.txt with
|
||||
// it (0x0049FEA2). Storm closes every open log at exit (0x0065A4B0).
|
||||
// benilla-re storm/log.
|
||||
FUN_SLOG_CREATE = 0x0065A0C0,
|
||||
// Storm SLogWrite — `void __cdecl(uint32_t log, const char *format, ...)`:
|
||||
// one line, prefixed with "M/D HH:MM:SS.mmm " and ended with "\r\n". The
|
||||
// combat log's own line writer (0x0049B1E8) calls it with "%s".
|
||||
// benilla-re storm/log.
|
||||
FUN_SLOG_WRITE = 0x0065ACE0,
|
||||
// Storm SLogClose — `void __stdcall(uint32_t log)`: writes the buffered
|
||||
// lines to the file, flushes and closes it. The chat teardown closes the
|
||||
// chat and combat logs with it (0x0049871B). benilla-re storm/log.
|
||||
FUN_SLOG_CLOSE = 0x00659EC0,
|
||||
// A unit's death — `void __fastcall(CGUnit *unit)`: writes the death
|
||||
// line (0x0062C160), shows the pending XP gain, plays the death animation
|
||||
// and sound. Its one caller is CGUnit_C::OnDied (0x006058AE), from the
|
||||
@@ -2629,6 +2712,14 @@ enum Offsets {
|
||||
OFF_NAMEPLATE_LINK_NEXT = 0x4E0,
|
||||
OFF_NAMEPLATE_GUID = 0x4E8,
|
||||
OFF_UNIT_NAMEPLATE = 0xE60,
|
||||
// unit+0xE64 is the unit's chat bubble (0x608AC0 fills it, 0x608C00
|
||||
// releases it; benilla-re objectlayer/unitnames.md "Chat bubbles").
|
||||
OFF_UNIT_CHAT_BUBBLE = 0xE64,
|
||||
// CGUnit_C::ShouldShowName — `int __thiscall(CGUnit_C *, uint32_t mask)`,
|
||||
// RET 4: whether the unit's overhead name shows. The mask is the UnitName*
|
||||
// cvars' (0xCE8720). Its only caller is the name draw
|
||||
// PLAYERNAMEDESC::UpdatePlate (0x6C6EE2), every frame. Disassembled.
|
||||
FUN_CGUNIT_SHOULD_SHOW_NAME = 0x006070A0,
|
||||
// CGNamePlateFrame vtable: the 40 virtual slots the plate constructor
|
||||
// stores as the frame's vfptr. Slot 10 (+0x28) is
|
||||
// FUN_FRAME_SET_FRAME_ALPHA, which is how the plate fade writes 0xFF /
|
||||
@@ -3638,6 +3729,12 @@ enum Offsets {
|
||||
// Lua stack and tail-calls this, so the engine's parser, setter and error
|
||||
// text stay in force.
|
||||
FUN_SCRIPT_BUTTON_REGISTERFORCLICKS = 0x00782490,
|
||||
// Frame:RegisterForDrag (Script table 0x00878EC0). It ORs LeftButton 1,
|
||||
// MiddleButton 2, RightButton 4 (SStrCmpI; any other string 0) over its
|
||||
// string arguments and stores the mask at frame+0xFC; no string clears it
|
||||
// (Ghidra). Xml::NativeSetters calls it for the `registerForDrag`
|
||||
// attribute.
|
||||
FUN_SCRIPT_FRAME_REGISTERFORDRAG = 0x00776D60,
|
||||
// CSimpleButton's mouse handlers, Button vtable 0x0081C7F8 (ctor 0x00778733)
|
||||
// slots +0x68 OnLayerMouseDown (0x00779210) / +0x6C OnLayerMouseUp
|
||||
// (0x007792D0), each `__thiscall(button, CMouseEvent *)` over the base
|
||||
@@ -3686,9 +3783,10 @@ enum Offsets {
|
||||
SITE_SCRIPT_CLICK_RIGHT = 0x00782811,
|
||||
SITE_SCRIPT_CLICK_DEFAULT = 0x00782826,
|
||||
BUTTON_DOUBLE_CLICK_MS = 300,
|
||||
// Button::SetClickMask — __thiscall(button, mask): writes +0x330. The
|
||||
// ctors call it with the type default (Button 0x100 = LeftButtonUp;
|
||||
// hyperlink button 0x500). Reference only.
|
||||
// Button::SetClickMask — __thiscall(button, mask), `ret 4`: writes +0x330
|
||||
// and nothing else (Ghidra). The ctors call it with the type default
|
||||
// (Button 0x100 = LeftButtonUp; hyperlink button 0x500). Frame::ClickMask
|
||||
// calls it for the XML `registerForClicks` attribute.
|
||||
FUN_BUTTON_SET_CLICK_MASK = 0x00779730,
|
||||
// EditBox — 48 methods (table 0x0087BB68), ctx per
|
||||
// docs/BlizzardScriptAPI.md. Backs `EditBox:SetCursorPosition` /
|
||||
@@ -3793,6 +3891,20 @@ enum Offsets {
|
||||
// FUN_SIMPLETEXTURE_SET_TEXCOORD and clears the bit. The texture's
|
||||
// OFF_REGION_TEXCOORD_MODIFIES_RECT crop draws the filled part.
|
||||
FUN_STATUSBAR_ON_FRAME_UPDATE = 0x007833C0,
|
||||
// CSimpleStatusBar::GetFillFraction, 4.3.4's GetAnimValue — `float
|
||||
// __thiscall(bar)`, plain RET, vtable slot +0x98 (read from vtable
|
||||
// 0x0081CA68): (value - min) / (max - min), or 0 when max - min <= 0.
|
||||
// FUN_STATUSBAR_ON_FRAME_UPDATE takes the fill from this slot.
|
||||
// Disassembled.
|
||||
FUN_STATUSBAR_GET_FILL_FRACTION = 0x00783380,
|
||||
OFF_VMT_STATUSBAR_GET_FILL_FRACTION = 0x98,
|
||||
// The engine's StatusBar:SetValue(value) and SetMinMaxValues(min, max)
|
||||
// Lua methods (`int __fastcall(L)`, from the method table 0x0087B010).
|
||||
// They type-check self, raise the usage errors, and call the bar's
|
||||
// SetValue slot / FUN_STATUSBAR_SET_MIN_MAX_VALUES; they read no argument
|
||||
// past their own. Decompiled.
|
||||
FUN_SCRIPT_STATUSBAR_SET_VALUE = 0x0078F690,
|
||||
FUN_SCRIPT_STATUSBAR_SET_MIN_MAX_VALUES = 0x0078F4B0,
|
||||
// CSimpleStatusBar::InstallBarTexture — `void __thiscall(bar, texture,
|
||||
// int drawLayer)`, `ret 8`: deletes the old bar texture, parents the new
|
||||
// one to the bar in the layer, sets its OFF_REGION_TEXCOORD_MODIFIES_RECT
|
||||
@@ -4265,6 +4377,11 @@ enum Offsets {
|
||||
// `Frame::ClampRectInsets` hooks FUN_LAYOUT_CALC_RECT to clamp the rect
|
||||
// grown by the frame's insets instead.
|
||||
OFF_LAYOUT_FLAGS = 0x3C,
|
||||
// Layout deferred: invalidation leaves the frame off the queue and lays
|
||||
// out its dependents instead. FUN_REGION_LAYOUT_REALIZE(1) sets it,
|
||||
// (0) clears it (`and [ecx+0x3c], 0xfffffffd` at 0x007680BF) and then
|
||||
// resolves a pending layout (bit 4, FUN_REGION_LAYOUT_DIRTY) at once.
|
||||
LAYOUT_FLAG_DEFERRED = 0x02,
|
||||
LAYOUT_FLAG_CLAMPED = 0x10,
|
||||
FUN_LAYOUT_CALC_RECT = 0x00767A20,
|
||||
// CSimpleFrame's size-changed vmethod: `void __thiscall(frame, float w,
|
||||
@@ -4418,6 +4535,12 @@ enum Offsets {
|
||||
// pass only 0x1, 0x10, 0x100, 0x200, 0x1000, 0x2000 and 0x4000, so the
|
||||
// 4.3.4 bits below are free in 1.12.
|
||||
FUN_FRAME_SET_FRAME_FLAG = 0x0076A3C0,
|
||||
// CSimpleFrame::SetFrameScale — `bool __thiscall(frame, float scale, int
|
||||
// force)`, `ret 8`: without force, returns 0 and changes nothing when the
|
||||
// scale is within epsilon of the stored one (+0xB8); else stores it and
|
||||
// calls vtable +0x8C (the rescale). Frame:SetScale (0x00775140) calls it
|
||||
// with force 0 after rejecting a negative scale (Ghidra).
|
||||
FUN_FRAME_SET_SCALE = 0x0076AC10,
|
||||
// 4.3.4's disabled bit: CSimpleFrame::RunOnEnableScript sets it for a
|
||||
// disabled Button or EditBox, and EditBox:IsEnabled reads it.
|
||||
FRAME_FLAG_DISABLED = 0x400,
|
||||
@@ -4724,6 +4847,10 @@ enum Offsets {
|
||||
// `int __thiscall()` (slot 21, the hover dispatch's gate at 0x482982).
|
||||
OFF_CGOBJECT_VTBL_GET_FACING = 0x18,
|
||||
OFF_CGOBJECT_VTBL_IS_MOUSEOVERABLE = 0x54,
|
||||
// CanSelect `int __thiscall()` (slot 22): the unit and player vtables both
|
||||
// hold CGUnit_C::CanSelect 0x5F1EC0, which jumps through slot 21 above.
|
||||
// SelectTarget and CGUnit_C::ShouldShowName (0x6070A8) call it.
|
||||
OFF_CGOBJECT_VTBL_CAN_SELECT = 0x58,
|
||||
|
||||
// The selection circle (benilla-re objectlayer/unitdisplay.md, verified
|
||||
// against the bytes of 0x608E00 / 0x614A90). The base per-frame step
|
||||
@@ -4892,7 +5019,13 @@ enum Offsets {
|
||||
// bound the Lua API uses, not the underlying array.
|
||||
VAR_GUILD_ROSTER_PTR = 0x00B72704,
|
||||
VAR_GUILD_ROSTER_TOTAL_COUNT = 0x00B73118,
|
||||
// u64 — the roster handler reads the member's GUID straight into the
|
||||
// record (`push [roster+i*4]; call CDataStore::GetInt64` at 0x004D0BFD).
|
||||
OFF_GUILD_MEMBER_GUID = 0x00,
|
||||
OFF_GUILD_MEMBER_NAME = 0x08,
|
||||
// u32 — `presence byte != 0` (`setne` at 0x004D0C1D, stored at
|
||||
// 0x004D0C22); GetGuildRosterInfo's online return tests it (0x004D13C5).
|
||||
OFF_GUILD_MEMBER_ONLINE = 0x48,
|
||||
|
||||
// PackBagSlot — __fastcall(L, void **outInvMgr, int *outLinearSlot, int *outIsBank) → bool.
|
||||
// Reads bagID at Lua stack[1] and slot at stack[2], validates them, and
|
||||
@@ -4995,7 +5128,7 @@ enum Offsets {
|
||||
// The type arg is a bitmask of object-type bits (see the TYPEMASK_*
|
||||
// block below), not an enum index.
|
||||
|
||||
// `CGObject::GetName` — returns the display-name `const char *` for
|
||||
// `CGUnit_C::GetUnitName` — returns the display-name `const char *` for
|
||||
// a resolved CGObject (CGUnit / CGPlayer / CGCreature). Internally
|
||||
// does a NameCache lookup for players and reads the creature-info
|
||||
// cache for NPCs; falls back to `"UNKNOWNOBJECT"` / `"Unknown Being"`
|
||||
@@ -5003,10 +5136,16 @@ enum Offsets {
|
||||
// "name + PvP-rank decoration" helper used by the SetUnit tooltip
|
||||
// builder) calls internally.
|
||||
//
|
||||
// __thiscall const char *(void *obj, int *outFlags /* may be 0 */)
|
||||
// __thiscall const char *(void *obj, const char **outRealm /* may be 0 */), RET 4
|
||||
//
|
||||
// Caller-owned `outFlags` receives metadata about the lookup; we
|
||||
// pass `nullptr` since we just want the name.
|
||||
// `outRealm` is the player's realm: for a player whose name-cache record
|
||||
// has a non-empty server name (`OFF_PLAYER_INFO_REALM`), it stores a
|
||||
// pointer to that string (0x0060926B-0x00609283); otherwise it leaves
|
||||
// `*outRealm` untouched, so the caller initializes it. Vanilla's
|
||||
// `Script_UnitName` (0x00517020) passes `&realm` here and pushes it as
|
||||
// UnitName's second return (nil when unset, spaces/tabs stripped when
|
||||
// arg 2 is `true`). The player lookup passes callback 0x005FB840, so a
|
||||
// synced player whose name isn't cached yet gets a name query.
|
||||
FUN_OBJECT_GET_NAME = 0x00609210,
|
||||
// `CGGameObject_C::GetName` — gameobject-specific name getter.
|
||||
// Returns `*(obj+0x214)+0x08` (the cached GameObjectStats_C record's
|
||||
@@ -6039,31 +6178,44 @@ enum Offsets {
|
||||
OFF_NAME_CACHE_FIRST_NODE = 0x0C,
|
||||
OFF_NAME_CACHE_NODE_GUID = 0x168,
|
||||
|
||||
// NameCache write entry — `__thiscall(this = cache, packetData,
|
||||
// guidLo, guidHi)` at 0x0055F310. Called once per
|
||||
// SMSG_NAME_QUERY_RESPONSE after the engine's opcode handler
|
||||
// builds a `packetData` buffer with the layout:
|
||||
// +0x000 name[48]
|
||||
// +0x030 realm[256]
|
||||
// +0x130 unknown[8] (probably (charGuidLo, charGuidHi))
|
||||
// +0x138 race (u32)
|
||||
// +0x13C sex (u32, 0/1 wire)
|
||||
// +0x140 class (u32)
|
||||
// Writes that data into the cache entry and dispatches any
|
||||
// pending callbacks. Decoded by Ghidra; field offsets verified
|
||||
// against the `*(uint*)(param_2 + N)` reads in the function body.
|
||||
//
|
||||
// Hooking here gives us a single chokepoint covering every name-
|
||||
// query response the engine processes — chat, group/raid sync,
|
||||
// guild roster, visible-object resolution. The hook runs after
|
||||
// the engine has settled the entry, so reading from the cache
|
||||
// post-hook is safe.
|
||||
FUN_PLAYER_INFO_CACHE_WRITE = 0x0055F310,
|
||||
// SMSG_NAME_QUERY_RESPONSE, handled by HandleNameQueryResponse
|
||||
// 0x005551A0, which reads in this order:
|
||||
// u64 guid CDataStore::GetGuid 0x004190B0 (8 raw bytes)
|
||||
// name C string CDataStore::GetCString 0x004191B0, capacity 0x30
|
||||
// realm C string capacity 0x100
|
||||
// u32 race, u32 sex, u32 class CDataStore::GetUInt32 0x00418EB0
|
||||
// GetCString empties the string and poisons the cursor when no NUL fits
|
||||
// in the capacity. A non-empty name goes to NameCacheStore 0x0055F310
|
||||
// (its only caller, 0x00555230) from a stack copy laid out as the record
|
||||
// below, with the guid at +0x130; an empty name refuses the guid
|
||||
// (0x0055F6F0). Every name the engine learns arrives this way (chat,
|
||||
// group, guild, visible players).
|
||||
SMSG_NAME_QUERY_RESPONSE = 0x51,
|
||||
// SMSG_INVALIDATE_PLAYER: a u64 guid. HandleNameInvalidate 0x00555600
|
||||
// frees that guid's record (NameCacheRemove 0x00560170, its only caller).
|
||||
// tortoise sends it to every client when a character is renamed
|
||||
// (World::InvalidatePlayerDataToAllClients, from the rename handler and
|
||||
// the GM rename command).
|
||||
SMSG_INVALIDATE_PLAYER = 0x31C,
|
||||
// NameCacheClear, `__thiscall(this = cache)`: frees every node. Its only
|
||||
// caller is DBCacheClearUnsaved 0x00555740, whose only caller is
|
||||
// CGGameUI::InitializeGame (0x0048F586), run by ClientInitializeGame at
|
||||
// each world entry. Nothing else empties the cache, so the engine keeps
|
||||
// its names from one world entry to the next, through logout and /reload.
|
||||
FUN_NAME_CACHE_CLEAR = 0x00560400,
|
||||
// The exe's C runtime `_atexit`, `int __cdecl(void (__cdecl *)(void))`, 0
|
||||
// on success. 0x00554CF0 registers the name cache's exit handler with it
|
||||
// at startup: NameCacheSave 0x0055EC70, which writes nothing for names
|
||||
// (persist byte +0x39 is 0) and frees the cache. Storm's exit handler
|
||||
// 0x0064CC30 runs every other handler, newest first (__cexit), before
|
||||
// __exit -> ExitProcess -> DLL_PROCESS_DETACH, so a DLL's detach runs
|
||||
// after the engine has freed its names.
|
||||
FUN_CRT_ATEXIT = 0x00409AEF,
|
||||
|
||||
// Entry-data offsets. The lookup returns `entry + 0x20`, so these
|
||||
// are relative to that pointer (not to the entry's hash-table
|
||||
// header). Field layout verified by decoding the cache writer
|
||||
// FUN_0055F310 at 0x0055F32D-0x0055F365:
|
||||
// NameCacheStore 0x0055F310 at 0x0055F32D-0x0055F365:
|
||||
// - copy 48 bytes from packet[+0] into entry+0x20 (name)
|
||||
// - copy 256 bytes from packet[+0x30] into entry+0x50 (realm)
|
||||
// - copy 4 bytes from packet[+0x138] into entry+0x158 (race)
|
||||
@@ -6074,6 +6226,12 @@ enum Offsets {
|
||||
OFF_PLAYER_INFO_RACE = 0x138, // u32 (race ID 1..8)
|
||||
OFF_PLAYER_INFO_SEX = 0x13C, // u32 (0=male, 1=female on wire)
|
||||
OFF_PLAYER_INFO_CLASS = 0x140, // u32 (class ID 1..11)
|
||||
// Capacities of the two inline strings, NUL included: the store copies
|
||||
// them with SStrCopy(dst, src, 0x30) and SStrCopy(dst, src, 0x100)
|
||||
// (0x0055F3B9, 0x0055F3D4). Names are UTF-8, so a Chinese character
|
||||
// takes 3 of the 48 bytes.
|
||||
PLAYER_INFO_NAME_SIZE = 0x30,
|
||||
PLAYER_INFO_REALM_SIZE = 0x100,
|
||||
|
||||
// Visible-object iterator. Walks the engine's linked list of
|
||||
// currently-in-range CGObjects (the same list the minimap blip
|
||||
@@ -6885,6 +7043,19 @@ enum Offsets {
|
||||
// ACTIONBAR_UPDATE_USABLE (0xD7, via FUN_FIRE_EVENT_NO_ARGS) when one did.
|
||||
// Disassembled; it is 4.3.4's `CGActionBar::UpdateUsable` line for line.
|
||||
FUN_ACTION_UPDATE_USABLE = 0x004E5C00,
|
||||
// The uint[120] caches FUN_ACTION_UPDATE_USABLE compares and stores:
|
||||
// FUN_ACTION_SLOT_USABLE's return at `0x004E5C1A` / `0x004E5C2F`, its
|
||||
// `*outNoMana` at `0x004E5C26` / `0x004E5C36`. `IsUsableAction` reads them.
|
||||
VAR_ACTION_USABLE = 0x00BC6B60,
|
||||
VAR_ACTION_NO_MANA = 0x00BC67A0,
|
||||
// FUN_ACTION_UPDATE_USABLE's per-slot `call FUN_ACTION_SLOT_USABLE` (ecx
|
||||
// slot0, edx &noMana), compared against the caches above before they are
|
||||
// stored. E8 call, disassembled.
|
||||
CALL_UPDATE_USABLE_SLOT_USABLE = 0x004E5C15,
|
||||
// FUN_ACTION_UPDATE_USABLE's `mov ecx, 0xD7; call FUN_FIRE_EVENT_NO_ARGS`
|
||||
// (ACTIONBAR_UPDATE_USABLE), reached only when a slot changed. E8 call,
|
||||
// disassembled.
|
||||
CALL_UPDATE_USABLE_FIRE = 0x004E5C53,
|
||||
// Drop the cursor onto a slot — `__fastcall(uint slot0)`. Sole entry
|
||||
// for `PlaceAction`, and the tail of both FUN_ACTION_PICKUP and
|
||||
// nampower's detoured `CGActionBar_UseAction` (`0x004E5EE0`) when the
|
||||
@@ -7257,6 +7428,34 @@ enum Offsets {
|
||||
// firing LEARNED_SPELL_IN_TAB. Read by `Spell::Learn`.
|
||||
VAR_PLAYER_SPELLBOOK_COUNT = 0x00B7116C,
|
||||
|
||||
// The reactive-spell list: 8-byte entries `{spellID, usable}`, one per
|
||||
// NAME among the learned spells whose record has AttributesEx4 bit 0x200
|
||||
// (Overpower, Revenge, Execute, Riposte, Counterattack, Mongoose Bite,
|
||||
// Hammer of Wrath on stock 1.12.1 — the bit is in 26 records of 7 names).
|
||||
// `AddSpellToBook` appends `{id, 0}` for a new name and moves an existing
|
||||
// entry to a strictly higher rank (`SStrCmp` of the locale name);
|
||||
// unlearning removes nothing, and the book reset `0x004B23A0` frees the
|
||||
// list on active-player arrival. `FUN_FIRE_USABLE_EVENTS` is the only
|
||||
// other reader: it retests each entry with FUN_SPELL_IS_USABLE, stores
|
||||
// the answer at +4 (`0x004B3231`), raises COMBAT_TEXT_UPDATE SPELL_ACTIVE
|
||||
// for each 0→1, then signals SPELL_UPDATE_USABLE (disassembled; benilla-re
|
||||
// ui/reactivespells). Read by `Spell::ActivationOverlay`.
|
||||
VAR_REACTIVE_SPELL_COUNT = 0x00B7114C,
|
||||
VAR_REACTIVE_SPELL_ENTRIES = 0x00B71150, // pointer to the entry array
|
||||
REACTIVE_SPELL_ENTRY_STRIDE = 8,
|
||||
OFF_REACTIVE_SPELL_ENTRY_USABLE = 4, // u32, the last answer (0/1)
|
||||
// `FireUsableEvents` — `void __cdecl()`. Runs the action bar's usable
|
||||
// refresh (`FUN_ACTION_UPDATE_USABLE`), retests the reactive list, then
|
||||
// `mov ecx, 0x107; call FUN_FIRE_EVENT_NO_ARGS` at 0x004B3241 — so an
|
||||
// `Event::SignalHook` subscriber seeing EVENT_SPELL_UPDATE_USABLE reads
|
||||
// the list already updated. Called from target select / release, player
|
||||
// control, combo points, shapeshift, the player's flags / bytes1 /
|
||||
// health / power / aura-state field watchers and the cooldown starters
|
||||
// (twelve functions) — the same triggers 4.3.4 pairs with
|
||||
// `CGSpellActivationOverlay::UpdateUsable`.
|
||||
FUN_FIRE_USABLE_EVENTS = 0x004B31C0,
|
||||
EVENT_SPELL_UPDATE_USABLE = 0x107,
|
||||
|
||||
// The player's pet GUID as a u64 (lo @ +0, hi @ +4). Verified three
|
||||
// ways: `Script_HasPetUI` (0x004BE670) resolves it with TYPEMASK_UNIT
|
||||
// through FUN_OBJECT_RESOLVE_BY_GUID, FUN_PET_ACTIONS_USABLE compares it
|
||||
@@ -8125,6 +8324,11 @@ enum Offsets {
|
||||
LUA_REF_UNREF = 0x6F5400,
|
||||
LUA_PUSH_CCLOSURE = 0x6F3920,
|
||||
LUA_NEW_TABLE = 0x6F3C90,
|
||||
// `lua_newuserdata(L, size)` — `__fastcall`, plain RET; pushes a full
|
||||
// userdata (tt 7, stamped with the exec context like every push) and
|
||||
// returns its block, the Udata + 0x10. Verified by disassembly: GC step
|
||||
// test, luaS_newudata (FUN_LUA_S_NEWUDATA), `ADD EAX,0x10`.
|
||||
LUA_NEW_USERDATA = 0x6F4560,
|
||||
LUA_GET_TABLE = 0x6F3A40, // (was 0x6F3EA0, which is lua_rawset)
|
||||
LUA_RAW_GET = 0x6F3B00,
|
||||
// `lua_rawgeti(L, idx, n)` — pushes `table_at_idx[n]` without invoking
|
||||
@@ -8702,6 +8906,11 @@ enum Offsets {
|
||||
// otherwise. Used by `coroutine.create` to reject C functions
|
||||
// (Lua 5.0 only allows Lua-defined functions as coroutine bodies).
|
||||
LUA_IS_C_FUNCTION = 0x6F34A0,
|
||||
// `lua_tocfunction(L, idx)` — the C function of a C closure, else null.
|
||||
// `__fastcall(L, idx)`: index2adr (0x6F3410) on the same registers, tag
|
||||
// 6, the C byte at closure+6, the function at closure+0xC (benilla-re
|
||||
// bytes; the engine never calls it, so Ghidra has no function there).
|
||||
LUA_TO_C_FUNCTION = 0x6F3720,
|
||||
// `lua_tothread(L, idx)` — returns the embedded `lua_State *` for
|
||||
// type-8 (LUA_TTHREAD) TValues, NULL for anything else. Used by
|
||||
// the `coroutine` library to extract the coroutine pointer from a
|
||||
@@ -11558,14 +11767,6 @@ enum Offsets {
|
||||
FUN_GREETING_SELECT_AVAILABLE_QUEST = 0x005012A0,
|
||||
FUN_GREETING_SELECT_ACTIVE_QUEST = 0x00501320,
|
||||
|
||||
// CDataStore primitive readers — `__thiscall void(this, void *out)`.
|
||||
// Read at the current cursor (`+0x14`) and advance it by the value
|
||||
// size. We call these directly from `Unit::MirrorTimer` to peek
|
||||
// packet contents before the SMSG handler consumes them, then
|
||||
// restore the cursor so the original handler reads the same bytes.
|
||||
FUN_DATASTORE_READ_U32 = 0x00418E30,
|
||||
FUN_DATASTORE_READ_U8 = 0x00418CB0,
|
||||
OFF_DATASTORE_CURSOR = 0x14,
|
||||
// CDataStore writers and the stack-packet shape the DBCache query send
|
||||
// builds (FUN_DBCACHE_ITEMSTATS_GET_RECORD, 0x0055BB18..0x0055BB89):
|
||||
// {vtable, buffer, base, alloc, size, read} with buffer/base/alloc/size 0
|
||||
@@ -11606,28 +11807,24 @@ enum Offsets {
|
||||
// selection flag at `0x00884C80`.
|
||||
FUN_ENGINE_TICK_MS = 0x0042B750,
|
||||
|
||||
// Mirror timer (BREATH / FATIGUE-style) SMSG handler. Dispatches
|
||||
// opcodes `0x1D9` (START), `0x1DA` (PAUSE), `0x1DB` (STOP) — but
|
||||
// the vanilla engine doesn't cache any timer state, just reads
|
||||
// the packet and immediately fires the corresponding
|
||||
// MIRROR_TIMER_* Lua event. Our hook (`Unit::MirrorTimer`) peeks
|
||||
// the packet via cursor save/restore on the CDataStore and
|
||||
// builds a side cache that `GetMirrorTimerInfo` /
|
||||
// `GetMirrorTimerProgress` can read back.
|
||||
// Mirror timers (EXHAUSTION = deep-water fatigue, BREATH, FEIGNDEATH).
|
||||
// The engine's handler for all three SMSGs is `0x005E7990`,
|
||||
// `uint32_t __fastcall(param, opcode, time, CDataStore *)`; it only
|
||||
// reads each packet and signals MIRROR_TIMER_START / _PAUSE / _STOP.
|
||||
// START is `u32 type, i32 value, i32 max, i32 scale, u8 paused, u32
|
||||
// spell`; STOP is `u32 type`. `Unit::MirrorTimer` reads both through
|
||||
// `Net::PacketDispatch` (verified: Ghidra + benilla-re
|
||||
// objectlayer/playermessages#mirror-timers).
|
||||
//
|
||||
// __fastcall void(?, int opcode, ?, void *dataStore)
|
||||
//
|
||||
// The helper at `FUN_MIRROR_TIMER_TYPE_NAME` returns the
|
||||
// engine's static type-name string for IDs 0..2 (`"EXHAUSTION"`,
|
||||
// `"BREATH"`, `"FEIGNDEATH"`); `FUN_MIRROR_TIMER_LABEL` builds
|
||||
// the localized display label by looking up `{TYPE}_LABEL` in
|
||||
// the engine's localized strings, or substituting the spell
|
||||
// name when the type is FEIGNDEATH and a spellID is non-zero.
|
||||
FUN_SMSG_MIRROR_TIMER_HANDLER = 0x005E7990,
|
||||
// `FUN_MIRROR_TIMER_TYPE_NAME` is `const char *__fastcall(uint32_t
|
||||
// type)`: "EXHAUSTION" / "BREATH" / "FEIGNDEATH" for 0..2, "UNKNOWN"
|
||||
// otherwise. `FUN_MIRROR_TIMER_LABEL` is `const char *__fastcall(
|
||||
// uint32_t type, uint32_t spell)`: the spell's localized name when the
|
||||
// spell exists, else the global string `<NAME>_LABEL` (FrameScript_GetText,
|
||||
// "" when missing).
|
||||
FUN_MIRROR_TIMER_TYPE_NAME = 0x005E7AE0,
|
||||
FUN_MIRROR_TIMER_LABEL = 0x005E7B10,
|
||||
SMSG_OPCODE_MIRROR_TIMER_START = 0x1D9,
|
||||
SMSG_OPCODE_MIRROR_TIMER_PAUSE = 0x1DA,
|
||||
SMSG_OPCODE_MIRROR_TIMER_STOP = 0x1DB,
|
||||
MIRROR_TIMER_SLOT_COUNT = 3,
|
||||
|
||||
@@ -11907,6 +12104,7 @@ enum Offsets {
|
||||
// names them: 0 info, 1 warning, 2 error, 3 fatal.
|
||||
VTBL_XML_STATUS_ADD = 0x0C,
|
||||
XML_STATUS_WARNING = 1,
|
||||
XML_STATUS_ERROR = 2,
|
||||
// `float __stdcall SStrToFloat(const char *)` (RET 4, x87 result) — the
|
||||
// frame loader's `alpha` conversion (0x00769A10).
|
||||
FUN_SSTR_TO_FLOAT = 0x0064AAA0,
|
||||
@@ -11930,7 +12128,8 @@ enum Offsets {
|
||||
// TEXTURE: reads "inherits", "hidden", "alphaMode" (0x00770179).
|
||||
// FONTSTRING: reads "inherits", "hidden", "justifyH" (0x00771308).
|
||||
// Identified by the attribute-name strings each passes to
|
||||
// FUN_XML_NODE_GET_ATTRIBUTE. Co-hooked by xml/ParentKey.cpp. The FRAME
|
||||
// FUN_XML_NODE_GET_ATTRIBUTE. Hooked by xml/FrameLoad.cpp (FRAME) and
|
||||
// xml/TypeLoad.cpp (TEXTURE, FONTSTRING). The FRAME
|
||||
// loader covers attributes, TitleRegion, ResizeBounds, Backdrop,
|
||||
// HitRectInsets, Layers and Scripts - not <Frames>, which the finalize
|
||||
// step builds (FUN_XML_FRAME_LOAD_CHILD_FRAMES).
|
||||
@@ -11976,6 +12175,22 @@ enum Offsets {
|
||||
// which runs it before firing OnLoad (+0x118) - so a parent's OnLoad
|
||||
// sees its children built.
|
||||
FUN_XML_FRAME_LOAD_CHILD_FRAMES = 0x0076A060,
|
||||
// CSimpleFrame::PostLoadXML, the finalize above: `__thiscall(frame, node,
|
||||
// status)`, `ret 8`. Clears the loading flag (+0x114); when visible
|
||||
// (+0xD4) ends the layout deferral of the frame's layout part (+0x24,
|
||||
// CALL_FRAME_POST_LOAD_XML_UNDEFER_FRAME), its title region's (+0xA8,
|
||||
// CALL_FRAME_POST_LOAD_XML_UNDEFER_TITLE) and each registered region's
|
||||
// (+0x1B8 list, region +0xC8 set, CALL_FRAME_POST_LOAD_XML_UNDEFER_REGION),
|
||||
// each `push 0; lea ecx, [x+0x24]; call FUN_REGION_LAYOUT_REALIZE`; then
|
||||
// builds the children (FUN_XML_FRAME_LOAD_CHILD_FRAMES), fires OnLoad
|
||||
// (+0x118), then OnShow (+0x130) when visible and not loading
|
||||
// (disassembly). Every frame type's vtable points here; the two other
|
||||
// finalizers (minimap 0x004EE2C0, tooltip 0x00529668) call it first and
|
||||
// read no rect after it.
|
||||
FUN_FRAME_POST_LOAD_XML = 0x0076A2F0,
|
||||
CALL_FRAME_POST_LOAD_XML_UNDEFER_FRAME = 0x0076A310,
|
||||
CALL_FRAME_POST_LOAD_XML_UNDEFER_TITLE = 0x0076A324,
|
||||
CALL_FRAME_POST_LOAD_XML_UNDEFER_REGION = 0x0076A35B,
|
||||
// The frame builder — `CSimpleFrame *__fastcall(node, parent, status)`,
|
||||
// ret 4 (disassembly: ECX = node, EDX = parent saved to [ebp-8]). Looks
|
||||
// the node's tag up in the frame-type factory table, picks the parent,
|
||||
@@ -12093,6 +12308,7 @@ enum Offsets {
|
||||
VTBL_LAYOUT_ON_FRAME_SIZE_CHANGED = 0x00,
|
||||
VTBL_LAYOUT_DELETING_DESTRUCTOR = 0x04,
|
||||
VTBL_LAYOUT_GET_LAYOUT_PARENT = 0x0C,
|
||||
// +0x10 SetLayoutScale: VTBL_LAYOUT_SET_SCALE.
|
||||
// +0x1C / +0x20 the explicit width / height, `float __thiscall(this)`:
|
||||
// CLayoutFrame's read +0x50 / +0x54 (0x00768420 / 0x00768410); textures
|
||||
// and font strings override them. Script GetWidth / GetHeight call them
|
||||
@@ -12175,6 +12391,11 @@ enum Offsets {
|
||||
// HighlightFont (0x00778A28) into +0x394 - reading its `inherits`.
|
||||
// In 4 loader vtables; CheckButton's loader 0x00785170 calls it first.
|
||||
FUN_XML_BUTTON_LOAD = 0x007788C0,
|
||||
// The StatusBar node loader (same sub-object convention, `ret 8`). It calls
|
||||
// FUN_XML_FRAME_LOAD first (0x00782F06), then reads `drawLayer`, each
|
||||
// BarTexture (0x00782F5B, set at 0x00782F63) and BarColor, `minValue` /
|
||||
// `maxValue` / `defaultValue` and `orientation` (Ghidra).
|
||||
FUN_XML_STATUSBAR_LOAD = 0x00782EF0,
|
||||
// By-name template lookup — the same one `inherits=` resolves through.
|
||||
// `__fastcall(const char *name) -> definition node`, 0 if unregistered
|
||||
// (case-insensitive, hashed). See FUN_006ee6f0 in Templates.cpp notes.
|
||||
@@ -12391,8 +12612,54 @@ enum Offsets {
|
||||
GXTEX_FORMAT_ARGB8888 = 2,
|
||||
GXTEX_CMD_LATCH = 1,
|
||||
PTR_TEXLOAD_DESC_VTBL = 0x007FFA10,
|
||||
// CGxTexFlags::CGxTexFlags — `void *__thiscall(flags, filter, wrapU,
|
||||
// wrapV, 0, 0, 0, anisotropy, 0)`. CSimpleTexture::SetTexture builds its
|
||||
// word with (filter, wrap, wrap, 0, 0, 0, 1, 0) (0x00770241..0x00770270),
|
||||
// one wrap argument for both axes. The bits: 0..2 the filter, 3 repeat in
|
||||
// U, 4 repeat in V — the OpenGL backend's ApplyTextureParams (0x0059F170)
|
||||
// maps `& 8` / `& 0x10` to GL_REPEAT or a clamp, one bit per axis, so the
|
||||
// device has no mirror or border mode (benilla-re gx/textures, verified).
|
||||
FUN_GX_TEXFLAGS_INIT = 0x0058A980,
|
||||
GXTEX_FLAG_FILTER_MASK = 0x7,
|
||||
GXTEX_FLAG_WRAP_U = 0x8,
|
||||
GXTEX_FLAG_WRAP_V = 0x10,
|
||||
// GetTextureDeviceObject — `CGxTex *__fastcall(HTEXTURE, int load, void
|
||||
// *callback)`: [hTex + OFF_HTEXTURE_DEVICE], or with `load` set the
|
||||
// texture is first waited for (0x0044AD03), so 0 with `load` clear means
|
||||
// not loaded yet.
|
||||
FUN_TEXTURE_GET_RENDERABLE = 0x0044ACF0,
|
||||
// The HTEXTURE object (CTexture, 0x15C bytes, benilla-re gx/textures):
|
||||
// its file name as loaded (the path, extension kept), a source word whose
|
||||
// bit 1 the file loaders TextureCreateFromBlp (0x0044A310),
|
||||
// TextureCreateFromTga (0x0044A140) and TextureCreateFromFile (0x00449610)
|
||||
// set for a load with the file flag CSimpleTexture::SetTexture always
|
||||
// passes (a solid colour or a pixel source never has it), the device
|
||||
// texture (0 until loaded) and, for a BLP only, the texture flags word it
|
||||
// was created with (TextureCreateFromBlp stores it before queuing the
|
||||
// read; the build ORs in 0x40 for mips). A TGA hands its word only to its
|
||||
// device texture (OFF_GXTEX_FLAGS).
|
||||
OFF_HTEXTURE_NAME = 0x0C,
|
||||
OFF_HTEXTURE_SOURCE = 0x110,
|
||||
HTEXTURE_SOURCE_FILE = 0x2,
|
||||
OFF_HTEXTURE_DEVICE = 0x140,
|
||||
OFF_HTEXTURE_TEX_FLAGS = 0x150,
|
||||
// TextureNameMatch — `int __fastcall(HTEXTURE, const char *name)`: 1 when
|
||||
// the texture was made from a file of that name (extension ignored, any
|
||||
// case), else 0. Flags play no part, so SetTexture's same-file early-out
|
||||
// (0x0077021E) keeps a held image whatever wrap a new call asks for.
|
||||
FUN_TEXTURE_NAME_MATCH = 0x0044AC70,
|
||||
// The device texture record (CGxTex): its width and height, stored by the
|
||||
// record constructor 0x00591600 from its arguments. They are the BLP's
|
||||
// size after TextureLimitSize (0x00448B60), which halves only past the
|
||||
// GPU's maximum texture size; the BaseMip skip ([device + 0x31C]) is
|
||||
// applied at upload and never written here. 4.3.4's CRenderBatch::
|
||||
// QueueTexture reads the same `gxTex->m_width` for its tile count.
|
||||
OFF_GXTEX_WIDTH = 0x24,
|
||||
OFF_GXTEX_HEIGHT = 0x28,
|
||||
// Its texture flags word (FUN_GX_TEXFLAGS_INIT bits), stored by 0x00591600
|
||||
// and read on every bind by ApplyTextureParams (0x0059F170): the wrap the
|
||||
// device actually samples with.
|
||||
OFF_GXTEX_FLAGS = 0x3C,
|
||||
// SysMsgAdd — `int __fastcall(CStatus *status, uint32_t level)`: hands a
|
||||
// status's error text to the system-message printer 0x0044CB20, which in
|
||||
// this build formats it and drops it (nothing reaches the screen; seen
|
||||
@@ -13105,6 +13372,17 @@ enum Offsets {
|
||||
// The record the lines are laid out from (4.3.4's m_currentScroll);
|
||||
// FUN_SMF_DISPLAY_REFRESH stores its argument here.
|
||||
OFF_SCROLLINGMESSAGEFRAME_SCROLL_RECORD = 0x348,
|
||||
// The lines the last layout of a shown frame laid out (4.3.4's
|
||||
// m_numDisplayed): FUN_SMF_DISPLAY_REFRESH zeroes it and counts each line
|
||||
// it places, and the fader (0x00788460), RearmLines (0x00788B80) and the
|
||||
// link-button rebuild (0x00788C00) walk display nodes 0..this-1, each
|
||||
// through its record pointer (OFF_SMF_LINE_RECORD).
|
||||
OFF_SCROLLINGMESSAGEFRAME_LINES_SHOWN = 0x34C,
|
||||
// The top flag (4.3.4's m_atTop): the layout sets it when the lines it
|
||||
// placed reach the oldest message (0x00788A37). Clear (0x007882B0) zeroes
|
||||
// it, and a layout of an empty frame returns before it, so it is only
|
||||
// meaningful while the frame has messages.
|
||||
OFF_SCROLLINGMESSAGEFRAME_AT_TOP = 0x350,
|
||||
// A ring that is not full holds records 0..count-1: the writer fills
|
||||
// them in order from an empty ring (Clear: position -1), and only a full
|
||||
// ring wraps. ScrollOffset and ScrollToOffset (below) wrap by the count,
|
||||
@@ -13170,6 +13448,27 @@ enum Offsets {
|
||||
// when it moves off the bottom; a layout at the bottom keeps the fade
|
||||
// state, so 4.3.4 re-arms explicitly after a removal's layout.
|
||||
FUN_SCROLLINGMESSAGEFRAME_REARM_LINES = 0x00788B80,
|
||||
// The ring writer's layout at the bottom: `E8 rel32` to
|
||||
// FUN_SMF_DISPLAY_REFRESH with ECX the frame and the newest record pushed
|
||||
// (0x00788150 tail; StoreLine's only layout call).
|
||||
CALL_SMF_STORE_LINE_LAYOUT = 0x00788234,
|
||||
// FUN_SCROLLINGMESSAGEFRAME_ON_UPDATE's first call, FUN_FRAME_ON_UPDATE
|
||||
// (the OnUpdate script), with ECX the frame and the elapsed time pushed;
|
||||
// the fader and the link-button rebuild follow it. Past the 6 bytes a
|
||||
// prologue hook of the function takes (55 / 8BEC / 83EC0C).
|
||||
CALL_SMF_ON_UPDATE_BASE = 0x00788471,
|
||||
// The layout-queue drain - `void __cdecl()`, plain `ret`: up to 32 passes
|
||||
// resolving every queued frame (benilla-re ui/anchors "The queue drain").
|
||||
FUN_LAYOUT_QUEUE_DRAIN = 0x00768ED0,
|
||||
// The top's update pass (CSimpleTop::OnLayerUpdate 0x00765650) drains the
|
||||
// queue, calls every shown frame's update slot (vtable+0x38), then drains
|
||||
// again here, before the draw pass (0x007657D0): `E8 rel32` to
|
||||
// FUN_LAYOUT_QUEUE_DRAIN. Everything a frame's update changed is laid out
|
||||
// by this call and drawn in the same frame.
|
||||
CALL_TOP_UPDATE_SECOND_DRAIN = 0x00765799,
|
||||
// The GetNumLinesDisplayed method, `int __fastcall(L)`: pushes
|
||||
// OFF_SCROLLINGMESSAGEFRAME_LINES_SHOWN.
|
||||
FUN_SCRIPT_SCROLLINGMESSAGEFRAME_GET_NUM_LINES_DISPLAYED = 0x00793590,
|
||||
FUN_SCRIPT_SCROLLINGMESSAGEFRAME_GET_CURRENT_SCROLL = 0x00793650,
|
||||
// The frame's OnFrameUpdate override (vtable+0x38, see
|
||||
// FUN_FRAME_ON_UPDATE), `void __thiscall(smf, float elapsed)`, `ret 4`:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -683,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.
|
||||
@@ -842,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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
+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,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);
|
||||
|
||||
@@ -134,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
|
||||
|
||||
@@ -14,3 +14,13 @@ C_ActionBar = C_ActionBar or {}
|
||||
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,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -246,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
|
||||
@@ -652,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
|
||||
@@ -1140,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);
|
||||
|
||||
@@ -1323,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
|
||||
|
||||
@@ -1131,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);
|
||||
|
||||
@@ -1287,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;
|
||||
@@ -1766,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)
|
||||
@@ -1787,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);
|
||||
|
||||
|
||||
@@ -394,6 +394,18 @@ AddCheckbox(interface, CLASSICAPI_SEPARATE_LARGE_NUMBERS, CLASSICAPI_SEPARATE_LA
|
||||
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
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -151,15 +151,15 @@ function FrameUtil.UpdateScaleForFit(frame, extraWidth, extraHeight)
|
||||
extraWidth = extraWidth or 0;
|
||||
extraHeight = extraHeight or 0;
|
||||
|
||||
FrameUtil.UpdateScaleForFitSpecific(frame, frame:GetWidth() + extraWidth, frame:GetHeight() + extraHeight);
|
||||
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:GetWidth() / (specificWidth or frame:GetWidth());
|
||||
local vertRatio = topLevelParent:GetHeight() / (specificHeight or frame:GetHeight());
|
||||
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));
|
||||
@@ -212,8 +212,8 @@ function GetScaledCenter(frame)
|
||||
end
|
||||
|
||||
function FitToParent(parent, frame)
|
||||
local horizRatio = parent:GetWidth() / frame:GetWidth();
|
||||
local vertRatio = parent:GetHeight() / frame:GetHeight();
|
||||
local horizRatio = parent:GetUnscaledWidth() / frame:GetUnscaledWidth();
|
||||
local vertRatio = parent:GetUnscaledHeight() / frame:GetUnscaledHeight();
|
||||
|
||||
if ( horizRatio < 1 or vertRatio < 1 ) then
|
||||
frame:SetScale(min(horizRatio, vertRatio));
|
||||
@@ -397,16 +397,16 @@ end
|
||||
|
||||
function AnimTransitionMixin:GetTranslationDeltas(translationType, isOut)
|
||||
if translationType == AnimTransitionMixinTransitionType.SlideLeft then
|
||||
local width = self.animTransitionConfig.distanceX or self:GetWidth();
|
||||
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:GetWidth();
|
||||
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:GetHeight();
|
||||
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:GetHeight();
|
||||
local height = self.animTransitionConfig.distanceY or self:GetUnscaledHeight();
|
||||
return 0, isOut and -height or height;
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -107,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
|
||||
@@ -114,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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
local strsplit = string.split;
|
||||
|
||||
LinkTypes = {
|
||||
AADCOpenConfig = "aadcopenconfig",
|
||||
Action = "action",
|
||||
@@ -75,7 +77,7 @@ function LinkUtil.FormatLink(linkType, linkDisplayText, ...)
|
||||
end
|
||||
|
||||
function LinkUtil.SplitLinkData(linkData)
|
||||
local linkType, linkOptions = string.split(":", linkData, 2);
|
||||
local linkType, linkOptions = strsplit(":", linkData, 2);
|
||||
linkOptions = linkOptions or ""; -- Could be nil if there's no ":" in linkData.
|
||||
return linkType, linkOptions;
|
||||
end
|
||||
@@ -85,7 +87,7 @@ function LinkUtil.SplitLink(link) -- returns linkText and displayText
|
||||
end
|
||||
|
||||
function LinkUtil.SplitLinkOptions(linkOptions)
|
||||
return string.split(":", linkOptions);
|
||||
return strsplit(":", linkOptions);
|
||||
end
|
||||
|
||||
-- Extract the first link from the text given, ignoring leading and trailing characters.
|
||||
|
||||
@@ -69,14 +69,14 @@ function PixelUtil.SetPoint(region, point, relativeTo, relativePoint, offsetX, o
|
||||
end
|
||||
|
||||
function PixelUtil.SetStatusBarValue(statusBar, value)
|
||||
local width = statusBar:GetWidth();
|
||||
local width = statusBar:GetUnscaledWidth();
|
||||
if width and width > 0.0 then
|
||||
local min, max = statusBar:GetMinMaxValues();
|
||||
local percent = ClampedPercentageBetween(value, min, max);
|
||||
if percent == 0.0 or percent == 1.0 then
|
||||
statusBar:SetValue(value);
|
||||
else
|
||||
local numPixels = PixelUtil.GetNearestPixelSize(statusBar:GetWidth() * percent, statusBar:GetEffectiveScale());
|
||||
local numPixels = PixelUtil.GetNearestPixelSize(statusBar:GetUnscaledWidth() * percent, statusBar:GetEffectiveScale());
|
||||
local roundedValue = Lerp(min, max, numPixels / width);
|
||||
statusBar:SetValue(roundedValue);
|
||||
end
|
||||
|
||||
@@ -40,6 +40,6 @@ function IsPlayerGuid(guid)
|
||||
end
|
||||
|
||||
function GetNameAndServerNameFromGUID(unitGUID)
|
||||
local _, _, _, _, _, name = GetPlayerInfoByGUID(unitGUID);
|
||||
return name, GetRealmName()
|
||||
local _, _, _, _, _, name, normalizedRealmName = GetPlayerInfoByGUID(unitGUID);
|
||||
return name, normalizedRealmName;
|
||||
end
|
||||
|
||||
@@ -1000,6 +1000,15 @@ SlashCmdList["STOPMACRO"] = function(msg)
|
||||
end
|
||||
SLASH_STOPMACRO1 = "/stopmacro";
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- The Lua error window is always loaded (FrameXML\ScriptErrorsFrame).
|
||||
|
||||
SlashCmdList["LUAERRORS"] = function(msg)
|
||||
ScriptErrorsFrame_HandleSlashCmd(msg);
|
||||
end
|
||||
SLASH_LUAERRORS1 = "/luaerrors";
|
||||
SLASH_LUAERRORS2 = "/scripterrors";
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Debug tools: load on demand, as retail's chat frame does with
|
||||
-- Blizzard_DebugTools. The commands live here; the tools load on first use.
|
||||
@@ -1024,12 +1033,19 @@ end
|
||||
SLASH_FRAMESTACK1 = "/framestack";
|
||||
SLASH_FRAMESTACK2 = "/fstack";
|
||||
|
||||
SlashCmdList["ETRACE"] = function(msg)
|
||||
if ( LoadDebugTools() ) then
|
||||
EventTraceFrame_HandleSlashCmd(msg);
|
||||
-- Retail keeps EventTrace_LoadUI in Blizzard_EventTrace's [Bootstrap] file,
|
||||
-- which loads before the addon itself. This client has no bootstrap files,
|
||||
-- so it is here. The addon replaces this command with its show / hide toggle
|
||||
-- when it loads.
|
||||
function EventTrace_LoadUI()
|
||||
return UIParentLoadAddOn("ClassicAPI_EventTrace");
|
||||
end
|
||||
|
||||
SlashCmdList["EVENTTRACE"] = function(msg)
|
||||
if ( EventTrace_LoadUI() ) then
|
||||
EventTrace:ProcessChatCommand(msg);
|
||||
end
|
||||
end
|
||||
SLASH_ETRACE1 = "/etrace";
|
||||
|
||||
SlashCmdList["TINSPECT"] = function(msg)
|
||||
if ( not LoadDebugTools() ) then
|
||||
@@ -1056,13 +1072,13 @@ end
|
||||
SLASH_TINSPECT1 = "/tinspect";
|
||||
SLASH_TINSPECT2 = "/tins";
|
||||
|
||||
SlashCmdList["LUAERRORS"] = function(msg)
|
||||
SlashCmdList["PROFILER"] = function(msg)
|
||||
if ( LoadDebugTools() ) then
|
||||
ScriptErrorsFrame_HandleSlashCmd(msg);
|
||||
ClassicAPIProfiler_HandleSlashCmd(msg);
|
||||
end
|
||||
end
|
||||
SLASH_LUAERRORS1 = "/luaerrors";
|
||||
SLASH_LUAERRORS2 = "/scripterrors";
|
||||
SLASH_PROFILER1 = "/profiler";
|
||||
SLASH_PROFILER2 = "/prof";
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Give a contested command back to whoever already owned it
|
||||
|
||||
@@ -161,7 +161,7 @@ end
|
||||
function TableBuilderColumnMixin:ConstrainToHeader(padding)
|
||||
local header = self:GetHeaderFrame();
|
||||
assert(header, "ConstrainToHeader() called with a nil header frame. Use ConstructHeader() or assign one with SetHeaderFrame(), or use SetFixedConstraints to have a headerless column.");
|
||||
self:SetFixedConstraints(header:GetWidth(), padding or 0);
|
||||
self:SetFixedConstraints(header:GetUnscaledWidth(), padding or 0);
|
||||
end
|
||||
|
||||
function TableBuilderColumnMixin:SetFixedConstraints(fixedWidth, padding)
|
||||
@@ -270,7 +270,7 @@ end
|
||||
function TableBuilderMixin:SetHeaderContainer(headerContainer)
|
||||
assert(headerContainer, "SetHeaderContainer() with a nil header container. Use ConstructHeader() or assign one with SetHeaderFrame(), or use SetFixedConstraints to have a headerless column.");
|
||||
self.headerContainer = headerContainer;
|
||||
self:SetTableWidth(headerContainer:GetWidth());
|
||||
self:SetTableWidth(headerContainer:GetUnscaledWidth());
|
||||
end
|
||||
|
||||
function TableBuilderMixin:GetHeaderPoolCollection()
|
||||
@@ -362,7 +362,7 @@ function TableBuilderMixin:ArrangeCells(row)
|
||||
return;
|
||||
end
|
||||
|
||||
local height = row:GetHeight();
|
||||
local height = row:GetUnscaledHeight();
|
||||
local leftMargin, rightMargin = self:GetTableMargins();
|
||||
|
||||
local column = columns[1];
|
||||
|
||||
@@ -20,8 +20,8 @@ function SetClampedTextureRotation(texture, rotationDegrees)
|
||||
|
||||
if not (texture.rotationDegrees) then
|
||||
texture.origTexCoords = {texture:GetTexCoord()};
|
||||
texture.origWidth = texture:GetWidth();
|
||||
texture.origHeight = texture:GetHeight();
|
||||
texture.origWidth = texture:GetUnscaledWidth();
|
||||
texture.origHeight = texture:GetUnscaledHeight();
|
||||
end
|
||||
|
||||
if (texture.rotationDegrees == rotationDegrees) then
|
||||
|
||||
@@ -34,6 +34,30 @@ EQUIPSET_EQUIP = "Anlegen";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Ausrüstungstausch fehlgeschlagen. Inventar ist voll.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Einige Gegenstände des Sets '%s' können nicht gewechselt werden, da Ihr Euch im Kampf befindet.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Mindestens ein Teil des Sets '%s' fehlt und kann nicht ausgerüstet werden.";
|
||||
EVENTTRACE_APPLY_DEFAULT_FILTER = "Standardfilter anwenden";
|
||||
EVENTTRACE_ARG_FMT = "Arg %d:";
|
||||
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Alle abwählen";
|
||||
EVENTTRACE_BUTTON_DISCARD_FILTER = "Alle verwerfen";
|
||||
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Alle wählen";
|
||||
EVENTTRACE_BUTTON_MARKER = "Markierung";
|
||||
EVENTTRACE_BUTTON_PAUSE = "Pause";
|
||||
EVENTTRACE_BUTTON_PLAY = "Spielen";
|
||||
EVENTTRACE_FILTER_HEADER = "Filter";
|
||||
EVENTTRACE_HEADER = "Ereignisprotokoll";
|
||||
EVENTTRACE_LOG_CR_EVENTS = "Globale Registerereignisse anzeigen";
|
||||
EVENTTRACE_LOG_DISCARD = "Protokoll verworfen";
|
||||
EVENTTRACE_LOG_HEADER = "Log";
|
||||
EVENTTRACE_LOG_PAUSE = "Ereignisprotokoll pausiert";
|
||||
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Ereignisprotokoll pausiert wenn ausgeblendet";
|
||||
EVENTTRACE_LOG_START = "Ereignisprotokoll begonnen";
|
||||
EVENTTRACE_LOG_WHEN_HIDDEN = "Ereignisprotokoll wenn ausgeblendet";
|
||||
EVENTTRACE_MARKER = "Markierung";
|
||||
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
|
||||
EVENTTRACE_OPTIONS = "Optionen";
|
||||
EVENTTRACE_RESULTS = "Ergebnisse: %d";
|
||||
EVENTTRACE_SHOW_ARGUMENTS = "Ereignisargumente anzeigen";
|
||||
EVENTTRACE_SHOW_TIMESTAMP = "Ereigniszeitstempel anzeigen";
|
||||
EVENTTRACE_TIMESTAMP = "Zeitstempel:";
|
||||
CLEAR_FOCUS = "Fokus löschen";
|
||||
INTERACT_ICONS_DEFAULT = "Nur NSCs (Standard)";
|
||||
INTERACT_ICONS_OPTION = "Interaktionstastensymbole";
|
||||
@@ -139,6 +163,10 @@ SLASH_EQUIP_SET1 = "/Ausrüstungsset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/platzanlegen";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_EVENTTRACE1 = "/eventtrace";
|
||||
SLASH_EVENTTRACE2 = "/ereignisverfolgung";
|
||||
SLASH_EVENTTRACE3 = "/etrace";
|
||||
SLASH_EVENTTRACE4 = "/etrace";
|
||||
SLASH_FOCUS1 = "/fokus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
SLASH_PET_AGGRESSIVE1 = "/baggressiv";
|
||||
@@ -187,6 +215,7 @@ SLASH_TARGET_NEAREST_RAID1 = "/schlachtzuganvisieren";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/benutzen";
|
||||
SLASH_USE2 = "/use";
|
||||
SOCIAL_LABEL = "Kontakte";
|
||||
LINK_TRADESKILL_TOOLTIP = "Hier klicken, um einen Link zu Eurem Beruf zu erstellen.";
|
||||
TEXT_TO_SPEECH = "Text-to-Speech";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Sprechgeschwindigkeit anpassen";
|
||||
|
||||
@@ -63,7 +63,12 @@ CLASSICAPI_LOOSE_TARGET_FOLLOWS_NONE = "Nothing";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_TOOLTIP = "A target that action targeting set changes with the soft enemy or the soft friend. A target that you chose stays.";
|
||||
CLASSICAPI_LOW_PRIORITY_ICONS = "Show Low Priority Icons";
|
||||
CLASSICAPI_LOW_PRIORITY_ICONS_TOOLTIP = "Also shows an icon over a unit that you can do nothing with, such as a party member. The icon is the normal pointer.";
|
||||
CLASSICAPI_MATCH_INSIDE_NAMES = "Match Edits Inside a Name";
|
||||
CLASSICAPI_MATCH_INSIDE_NAMES_TOOLTIP = "When you type in the middle of a name, the suggested names also match the letters after the cursor.";
|
||||
CLASSICAPI_MATCH_THE_COMMAND = "Match the Command";
|
||||
CLASSICAPI_MATCH_THE_COMMAND_TOOLTIP = "Suggest only the names that fit the command, for example guild members for /gpromote. When off, every name you know is suggested.";
|
||||
CLASSICAPI_MOTION = "Motion";
|
||||
CLASSICAPI_NAME_SUGGESTIONS = "Name Suggestions";
|
||||
CLASSICAPI_NAMEPLATES = "Nameplates";
|
||||
CLASSICAPI_NUMBERS = "Numbers";
|
||||
CLASSICAPI_OPTIONS_ABOUT = "Settings for the features that ClassicAPI adds to the game. Each option changes one setting, and the change applies at once.";
|
||||
@@ -72,6 +77,8 @@ CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP = "The opacity of the nameplates of uni
|
||||
CLASSICAPI_PREFER_YOUR_TARGET = "Prefer Your Target";
|
||||
CLASSICAPI_PREFER_YOUR_TARGET_TOOLTIP = "When the target that you chose can be picked, action targeting picks it. When this is off, it always picks the nearest unit.";
|
||||
CLASSICAPI_RANGE_TOOLTIP = "The maximum distance in yards. It cannot be more than the targetNearestDistance setting.";
|
||||
CLASSICAPI_RECENT_WHISPERS_FIRST = "Recent Whispers First";
|
||||
CLASSICAPI_RECENT_WHISPERS_FIRST_TOOLTIP = "Suggested names start with the players you whispered with most recently.";
|
||||
CLASSICAPI_REMEMBER_PLAYERS = "Remember Players";
|
||||
CLASSICAPI_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.";
|
||||
CLASSICAPI_SCRIPT_PROFILE = "Script Profiling";
|
||||
@@ -117,6 +124,30 @@ EQUIPSET_EQUIP = "Equip";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Equipment swap failed. Inventory is full.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Some items from set %s cannot be swapped because you are in combat.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "One or more items from set %s were missing and could not be equipped.";
|
||||
EVENTTRACE_APPLY_DEFAULT_FILTER = "Apply Default Filter";
|
||||
EVENTTRACE_ARG_FMT = "Arg %d:";
|
||||
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Uncheck All";
|
||||
EVENTTRACE_BUTTON_DISCARD_FILTER = "Discard All";
|
||||
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Check All";
|
||||
EVENTTRACE_BUTTON_MARKER = "Marker";
|
||||
EVENTTRACE_BUTTON_PAUSE = "Pause";
|
||||
EVENTTRACE_BUTTON_PLAY = "Play";
|
||||
EVENTTRACE_FILTER_HEADER = "Filter";
|
||||
EVENTTRACE_HEADER = "Event Log";
|
||||
EVENTTRACE_LOG_CR_EVENTS = "Show Global Registry Events";
|
||||
EVENTTRACE_LOG_DISCARD = "Log discarded";
|
||||
EVENTTRACE_LOG_HEADER = "Log";
|
||||
EVENTTRACE_LOG_PAUSE = "Event logging paused";
|
||||
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Event logging paused while hidden";
|
||||
EVENTTRACE_LOG_START = "Event logging started";
|
||||
EVENTTRACE_LOG_WHEN_HIDDEN = "Log Events When Hidden";
|
||||
EVENTTRACE_MARKER = "Marker";
|
||||
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
|
||||
EVENTTRACE_OPTIONS = "Options";
|
||||
EVENTTRACE_RESULTS = "Results: %d";
|
||||
EVENTTRACE_SHOW_ARGUMENTS = "Show Event Arguments";
|
||||
EVENTTRACE_SHOW_TIMESTAMP = "Show Event Timestamp";
|
||||
EVENTTRACE_TIMESTAMP = "Timestamp:";
|
||||
CLEAR_FOCUS = "Clear Focus";
|
||||
INTERACT_ICONS_DEFAULT = "NPCs Only (Default)";
|
||||
INTERACT_ICONS_OPTION = "Interact Key Icons";
|
||||
@@ -192,11 +223,13 @@ SECOND_NUMBER_CAP = " M";
|
||||
SECOND_NUMBER_CAP_NO_SPACE = "M";
|
||||
THIRD_NUMBER = "billion";
|
||||
FOURTH_NUMBER = "trillion";
|
||||
LUA_ERROR = "Lua Error";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Equipment Manager";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "New Set";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
PLAYER_LIST_DELIMITER = ", ";
|
||||
RELOADUI = "Reload UI";
|
||||
SET_FRIENDNOTE_LABEL = "Set Notes for %s:";
|
||||
SET_NOTE = "Set Note";
|
||||
SILVER_AMOUNT = "%d Silver";
|
||||
@@ -222,6 +255,10 @@ SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/equipslot";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_EVENTTRACE1 = "/eventtrace";
|
||||
SLASH_EVENTTRACE2 = "/eventtrace";
|
||||
SLASH_EVENTTRACE3 = "/etrace";
|
||||
SLASH_EVENTTRACE4 = "/etrace";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
SLASH_PET_AGGRESSIVE1 = "/petaggressive";
|
||||
@@ -270,6 +307,7 @@ SLASH_TARGET_NEAREST_RAID1 = "/targetraid";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/use";
|
||||
SOCIAL_LABEL = "Social";
|
||||
STOP_AUTO_ATTACK = "Stop Auto Attack";
|
||||
SYMBOLIC_NEGATIVE_NUMBER = "-%s";
|
||||
SYMBOLIC_POSITIVE_NUMBER = "+%s";
|
||||
|
||||
@@ -34,6 +34,30 @@ EQUIPSET_EQUIP = "Equipar";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "No se ha podido cambiar el equipamiento. Inventario lleno.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Algunos objetos del equipo %s no se cambiarán porque estás en combate.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Falta uno o más objetos del equipo %s y no se ha podido equipar.";
|
||||
EVENTTRACE_APPLY_DEFAULT_FILTER = "Aplicar filtro predeterminado";
|
||||
EVENTTRACE_ARG_FMT = "Arg. %d:";
|
||||
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Desmarcar todo";
|
||||
EVENTTRACE_BUTTON_DISCARD_FILTER = "Desechar todo";
|
||||
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Marcar todo";
|
||||
EVENTTRACE_BUTTON_MARKER = "Marcador";
|
||||
EVENTTRACE_BUTTON_PAUSE = "Pausa";
|
||||
EVENTTRACE_BUTTON_PLAY = "Jugar";
|
||||
EVENTTRACE_FILTER_HEADER = "Filtro";
|
||||
EVENTTRACE_HEADER = "Sesión de evento";
|
||||
EVENTTRACE_LOG_CR_EVENTS = "Mostrar eventos de registro globales";
|
||||
EVENTTRACE_LOG_DISCARD = "Inicio de sesión desechado";
|
||||
EVENTTRACE_LOG_HEADER = "Registro";
|
||||
EVENTTRACE_LOG_PAUSE = "Se ha pausado el inicio de sesión de evento";
|
||||
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Registro de eventos pausado al estar oculto";
|
||||
EVENTTRACE_LOG_START = "Ha comenzado el inicio de sesión de evento";
|
||||
EVENTTRACE_LOG_WHEN_HIDDEN = "Registrar eventos al estar oculto";
|
||||
EVENTTRACE_MARKER = "Marcador";
|
||||
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
|
||||
EVENTTRACE_OPTIONS = "Opciones";
|
||||
EVENTTRACE_RESULTS = "Resultados: %d";
|
||||
EVENTTRACE_SHOW_ARGUMENTS = "Mostrar argumentos de evento";
|
||||
EVENTTRACE_SHOW_TIMESTAMP = "Mostrar tiempo del evento";
|
||||
EVENTTRACE_TIMESTAMP = "Hora:";
|
||||
CLEAR_FOCUS = "Quitar foco";
|
||||
INTERACT_ICONS_DEFAULT = "Solo PNJ (por defecto)";
|
||||
INTERACT_ICONS_OPTION = "Iconos de tecla Interactuar";
|
||||
@@ -139,6 +163,10 @@ SLASH_EQUIP_SET1 = "/conjequip";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/equipranu";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_EVENTTRACE1 = "/seguimevento";
|
||||
SLASH_EVENTTRACE2 = "/eventtrace";
|
||||
SLASH_EVENTTRACE3 = "/segevento";
|
||||
SLASH_EVENTTRACE4 = "/etrace";
|
||||
SLASH_FOCUS1 = "/enfoque";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
SLASH_PET_AGGRESSIVE1 = "/mascagresiva";
|
||||
@@ -187,6 +215,7 @@ SLASH_TARGET_NEAREST_RAID1 = "/objetivobanda";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/usar";
|
||||
SOCIAL_LABEL = "Social";
|
||||
LINK_TRADESKILL_TOOLTIP = "Haz clic aquí para crear un vínculo a tu profesión.";
|
||||
TEXT_TO_SPEECH = "Texto a voz";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Ajustar velocidad de voz";
|
||||
|
||||
@@ -34,6 +34,30 @@ EQUIPSET_EQUIP = "Equiper";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Échec de l'échange d'équipement. L'inventaire est plein.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Certains objets de l'ensemble %s ne peuvent pas être échangés car vous êtes en combat.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Une ou plusieurs pièces de l'ensemble %s manquaient et n'ont pu être équipées.";
|
||||
EVENTTRACE_APPLY_DEFAULT_FILTER = "Filtre par défaut";
|
||||
EVENTTRACE_ARG_FMT = "Arg %d :";
|
||||
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Tout décocher";
|
||||
EVENTTRACE_BUTTON_DISCARD_FILTER = "Effacer tous les filtres";
|
||||
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Tout cocher";
|
||||
EVENTTRACE_BUTTON_MARKER = "Marqueur";
|
||||
EVENTTRACE_BUTTON_PAUSE = "Pause";
|
||||
EVENTTRACE_BUTTON_PLAY = "Jouer";
|
||||
EVENTTRACE_FILTER_HEADER = "Filtrer";
|
||||
EVENTTRACE_HEADER = "Journal d’évènements";
|
||||
EVENTTRACE_LOG_CR_EVENTS = "Afficher événements du registre global";
|
||||
EVENTTRACE_LOG_DISCARD = "Journal supprimé";
|
||||
EVENTTRACE_LOG_HEADER = "Journal";
|
||||
EVENTTRACE_LOG_PAUSE = "Enregistrement événement en pause";
|
||||
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Enregistrement événements en pause quand masqué";
|
||||
EVENTTRACE_LOG_START = "Enregistrement événement lancé";
|
||||
EVENTTRACE_LOG_WHEN_HIDDEN = "Enregistrement événements quand masqué";
|
||||
EVENTTRACE_MARKER = "Marqueur";
|
||||
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
|
||||
EVENTTRACE_OPTIONS = "Options";
|
||||
EVENTTRACE_RESULTS = "Résultats : %d";
|
||||
EVENTTRACE_SHOW_ARGUMENTS = "Afficher arguments événement";
|
||||
EVENTTRACE_SHOW_TIMESTAMP = "Afficher heure événement";
|
||||
EVENTTRACE_TIMESTAMP = "Heure :";
|
||||
CLEAR_FOCUS = "Effacer la focalisation";
|
||||
INTERACT_ICONS_DEFAULT = "PNJ uniquement (Par défaut)";
|
||||
INTERACT_ICONS_OPTION = "Icônes de commande d’interaction";
|
||||
@@ -139,6 +163,10 @@ SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/équipens";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/equipempl";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_EVENTTRACE1 = "/eventtrace";
|
||||
SLASH_EVENTTRACE2 = "/suiviévènement";
|
||||
SLASH_EVENTTRACE3 = "/etrace";
|
||||
SLASH_EVENTTRACE4 = "/suiviev";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focal";
|
||||
SLASH_PET_AGGRESSIVE1 = "/famagresse";
|
||||
@@ -187,6 +215,7 @@ SLASH_TARGET_NEAREST_RAID1 = "/ciblerraid";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/utiliser";
|
||||
SOCIAL_LABEL = "Social";
|
||||
LINK_TRADESKILL_TOOLTIP = "Cliquez ici pour créer un lien vers votre profession.";
|
||||
TEXT_TO_SPEECH = "Narrateur";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Modifie la vitesse de la voix";
|
||||
|
||||
@@ -34,6 +34,30 @@ EQUIPSET_EQUIP = "Надеть";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Не удалось заменить комплект обмундирования. Инвентарь переполнен.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Некоторые предметы из набора \"%s\" не будут экипированы, поскольку вы участвуете в бою.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Недостает одного или более предметов из набора \"%s\".";
|
||||
EVENTTRACE_APPLY_DEFAULT_FILTER = "Применить фильтр по умолчанию";
|
||||
EVENTTRACE_ARG_FMT = "Арг. %d:";
|
||||
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Снять выделение";
|
||||
EVENTTRACE_BUTTON_DISCARD_FILTER = "Сбросить все";
|
||||
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Выделить все";
|
||||
EVENTTRACE_BUTTON_MARKER = "Маркер";
|
||||
EVENTTRACE_BUTTON_PAUSE = "Пауза";
|
||||
EVENTTRACE_BUTTON_PLAY = "Играть";
|
||||
EVENTTRACE_FILTER_HEADER = "Фильтр";
|
||||
EVENTTRACE_HEADER = "Журнал события";
|
||||
EVENTTRACE_LOG_CR_EVENTS = "Отображать глобальные события по регистрации";
|
||||
EVENTTRACE_LOG_DISCARD = "Журнал сброшен";
|
||||
EVENTTRACE_LOG_HEADER = "Журнал";
|
||||
EVENTTRACE_LOG_PAUSE = "Регистрация участников события приостановлена";
|
||||
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Фиксация событий прекращается в свернутом состоянии";
|
||||
EVENTTRACE_LOG_START = "Регистрация участников события началась";
|
||||
EVENTTRACE_LOG_WHEN_HIDDEN = "Фиксировать события в свернутом состоянии";
|
||||
EVENTTRACE_MARKER = "Маркер";
|
||||
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
|
||||
EVENTTRACE_OPTIONS = "Параметры";
|
||||
EVENTTRACE_RESULTS = "Результаты: %d";
|
||||
EVENTTRACE_SHOW_ARGUMENTS = "Отображать аргументы события";
|
||||
EVENTTRACE_SHOW_TIMESTAMP = "Отображать метку времени события";
|
||||
EVENTTRACE_TIMESTAMP = "Время:";
|
||||
CLEAR_FOCUS = "Отменить запоминание цели";
|
||||
INTERACT_ICONS_DEFAULT = "Только NPC (по умолчанию)";
|
||||
INTERACT_ICONS_OPTION = "Значки клавиш взаимодействия";
|
||||
@@ -139,6 +163,10 @@ SLASH_EQUIP_SET1 = "/комплект";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/надеть";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_EVENTTRACE1 = "/отслежсобыт";
|
||||
SLASH_EVENTTRACE2 = "/eventtrace";
|
||||
SLASH_EVENTTRACE3 = "/etrace";
|
||||
SLASH_EVENTTRACE4 = "/отслежс";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/фокус";
|
||||
SLASH_PET_AGGRESSIVE1 = "/питомецагрессивный";
|
||||
@@ -187,6 +215,7 @@ SLASH_TARGET_NEAREST_RAID1 = "/цельрейд";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/использовать";
|
||||
SOCIAL_LABEL = "Общение";
|
||||
LINK_TRADESKILL_TOOLTIP = "Щелкните здесь, чтобы создать ссылку на вашу профессию.";
|
||||
TEXT_TO_SPEECH = "Текст в речь";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Изменить скорость речи.";
|
||||
|
||||
@@ -34,6 +34,30 @@ EQUIPSET_EQUIP = "装备";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "装备更换失败。背包已满。";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "装备方案%s中的某些物品不能在战斗中切换。";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "未找到套装方案%s中的一件或多件物品,无法装备。";
|
||||
EVENTTRACE_APPLY_DEFAULT_FILTER = "应用默认条件";
|
||||
EVENTTRACE_ARG_FMT = "参数 %d:";
|
||||
EVENTTRACE_BUTTON_DISABLE_FILTERS = "撤选所有";
|
||||
EVENTTRACE_BUTTON_DISCARD_FILTER = "丢弃所有";
|
||||
EVENTTRACE_BUTTON_ENABLE_FILTERS = "勾选所有";
|
||||
EVENTTRACE_BUTTON_MARKER = "标记";
|
||||
EVENTTRACE_BUTTON_PAUSE = "暂停";
|
||||
EVENTTRACE_BUTTON_PLAY = "播放";
|
||||
EVENTTRACE_FILTER_HEADER = "过滤器";
|
||||
EVENTTRACE_HEADER = "事件记录";
|
||||
EVENTTRACE_LOG_CR_EVENTS = "显示全局注册事件";
|
||||
EVENTTRACE_LOG_DISCARD = "丢弃记录";
|
||||
EVENTTRACE_LOG_HEADER = "日志";
|
||||
EVENTTRACE_LOG_PAUSE = "暂停事件记录";
|
||||
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "隐藏时暂停记录事件";
|
||||
EVENTTRACE_LOG_START = "开始事件记录";
|
||||
EVENTTRACE_LOG_WHEN_HIDDEN = "隐藏时记录事件";
|
||||
EVENTTRACE_MARKER = "标记";
|
||||
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
|
||||
EVENTTRACE_OPTIONS = "设置选项";
|
||||
EVENTTRACE_RESULTS = "结果:%d";
|
||||
EVENTTRACE_SHOW_ARGUMENTS = "显示事件参数";
|
||||
EVENTTRACE_SHOW_TIMESTAMP = "显示事件时间戳";
|
||||
EVENTTRACE_TIMESTAMP = "时间戳:";
|
||||
CLEAR_FOCUS = "清除焦点";
|
||||
INTERACT_ICONS_DEFAULT = "仅限NPC(默认)";
|
||||
INTERACT_ICONS_OPTION = "交互按键图标";
|
||||
@@ -139,6 +163,10 @@ SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/equipslot";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_EVENTTRACE1 = "/eventtrace";
|
||||
SLASH_EVENTTRACE2 = "/eventtrace";
|
||||
SLASH_EVENTTRACE3 = "/etrace";
|
||||
SLASH_EVENTTRACE4 = "/etrace";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
SLASH_PET_AGGRESSIVE1 = "/petaggressive";
|
||||
@@ -187,6 +215,7 @@ SLASH_TARGET_NEAREST_RAID1 = "/targetraid";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/use";
|
||||
SOCIAL_LABEL = "社交";
|
||||
LINK_TRADESKILL_TOOLTIP = "点击这里以创建一个链接到你的专业的快捷方式。";
|
||||
TEXT_TO_SPEECH = "文本转语音";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "调节讲话速度";
|
||||
|
||||
@@ -34,6 +34,30 @@ EQUIPSET_EQUIP = "裝備";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "裝備切換失敗。物品欄已滿。";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "%s設定中的某些物品必須等到你脫離戰鬥後才能更換。";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "%s設定中的一個或多個物品未尋獲,因此無法裝備。";
|
||||
EVENTTRACE_APPLY_DEFAULT_FILTER = "套用預設篩選條件";
|
||||
EVENTTRACE_ARG_FMT = "Arg %d:";
|
||||
EVENTTRACE_BUTTON_DISABLE_FILTERS = "全部取消";
|
||||
EVENTTRACE_BUTTON_DISCARD_FILTER = "全部移除";
|
||||
EVENTTRACE_BUTTON_ENABLE_FILTERS = "全部選取";
|
||||
EVENTTRACE_BUTTON_MARKER = "標記";
|
||||
EVENTTRACE_BUTTON_PAUSE = "暫停";
|
||||
EVENTTRACE_BUTTON_PLAY = "播放";
|
||||
EVENTTRACE_FILTER_HEADER = "篩選條件";
|
||||
EVENTTRACE_HEADER = "事件紀錄";
|
||||
EVENTTRACE_LOG_CR_EVENTS = "顯示全球登錄事件";
|
||||
EVENTTRACE_LOG_DISCARD = "紀錄已移除。";
|
||||
EVENTTRACE_LOG_HEADER = "紀錄";
|
||||
EVENTTRACE_LOG_PAUSE = "事件紀錄功能已暫停。";
|
||||
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "隱藏時暫停記錄事件";
|
||||
EVENTTRACE_LOG_START = "事件紀錄功能已開始。";
|
||||
EVENTTRACE_LOG_WHEN_HIDDEN = "隱藏時繼續記錄事件";
|
||||
EVENTTRACE_MARKER = "標記";
|
||||
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
|
||||
EVENTTRACE_OPTIONS = "選項";
|
||||
EVENTTRACE_RESULTS = "結果:%d";
|
||||
EVENTTRACE_SHOW_ARGUMENTS = "顯示事件提要";
|
||||
EVENTTRACE_SHOW_TIMESTAMP = "顯示事件時間標記";
|
||||
EVENTTRACE_TIMESTAMP = "時間標記:";
|
||||
CLEAR_FOCUS = "清除專注目標";
|
||||
INTERACT_ICONS_DEFAULT = "僅限NPC(預設)";
|
||||
INTERACT_ICONS_OPTION = "互動鍵圖示";
|
||||
@@ -139,6 +163,10 @@ SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/裝備套裝";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/裝備欄位";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_EVENTTRACE1 = "/eventtrace";
|
||||
SLASH_EVENTTRACE2 = "/事件追蹤";
|
||||
SLASH_EVENTTRACE3 = "/etrace";
|
||||
SLASH_EVENTTRACE4 = "/事件追蹤";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/專注目標";
|
||||
SLASH_PET_AGGRESSIVE1 = "/寵物攻擊型";
|
||||
@@ -187,6 +215,7 @@ SLASH_TARGET_NEAREST_RAID1 = "/目標團隊";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/使用";
|
||||
SLASH_USE2 = "/use";
|
||||
SOCIAL_LABEL = "社交";
|
||||
LINK_TRADESKILL_TOOLTIP = "點擊這裡以製造一個你專業技能的連結。";
|
||||
TEXT_TO_SPEECH = "文字轉語音";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "調整語速";
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
## Special Thanks: Iriel, Kirov, Esamynn
|
||||
## Version: DEV
|
||||
## LoadOnDemand: 1
|
||||
## SavedVariables: DebugToolsSavedVars
|
||||
Dump.lua
|
||||
DebugTools.lua
|
||||
DebugTools.xml
|
||||
@@ -13,3 +12,13 @@ TableInspectorDataProvider.lua
|
||||
TableInspectorAnchorDataProvider.xml
|
||||
TableInspectorAttributeDataProvider.xml
|
||||
TableInspector.xml
|
||||
Profiler\Profiler.xml
|
||||
Profiler\Core.lua
|
||||
Profiler\Track.lua
|
||||
Profiler\Widget.lua
|
||||
Profiler\Window.lua
|
||||
Profiler\AddonsTab.lua
|
||||
Profiler\FramesTab.lua
|
||||
Profiler\EventsTab.lua
|
||||
Profiler\GlobalsTab.lua
|
||||
Profiler\FrameStack.lua
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,402 +1,5 @@
|
||||
<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">
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- EventTrace -->
|
||||
<!-- ================================================================ -->
|
||||
|
||||
<Button name="EventTraceEventTemplate" virtual="true">
|
||||
<Size x="280" y="16"/>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<FontString name="$parentTime" inherits="NumberFontNormalSmall" justifyH="RIGHT">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"/>
|
||||
<Anchor point="TOPRIGHT" relativePoint="TOPLEFT">
|
||||
<Offset x="62" y="0"/>
|
||||
</Anchor>
|
||||
<Anchor point="BOTTOM"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentEvent" inherits="GameFontHighlightSmall" justifyH="LEFT">
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT"/>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset x="70" y="0"/>
|
||||
</Anchor>
|
||||
<Anchor point="BOTTOM"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this:GetHighlightTexture():SetAlpha(.15);
|
||||
</OnLoad>
|
||||
<OnEnter>EventTraceFrameEvent_OnEnter(this);</OnEnter>
|
||||
<OnLeave>EventTraceFrameEvent_OnLeave(this);</OnLeave>
|
||||
<OnClick>EventTraceFrameEvent_OnClick(this);</OnClick>
|
||||
</Scripts>
|
||||
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" setAllPoints="true" alphaMode="ADD">
|
||||
<Color r=".8" g=".8" b="1" a="1"/>
|
||||
</HighlightTexture>
|
||||
</Button>
|
||||
|
||||
<Frame name="EventTraceFrame" parent="UIParent" movable="true" hidden="true" frameStrata="MEDIUM" toplevel="true">
|
||||
<Size x="306" y="484"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT">
|
||||
<Offset x="64" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
|
||||
<BackgroundInsets>
|
||||
<AbsInset left="5" right="5" top="5" bottom="5"/>
|
||||
</BackgroundInsets>
|
||||
<TileSize>
|
||||
<AbsValue val="16"/>
|
||||
</TileSize>
|
||||
<EdgeSize>
|
||||
<AbsValue val="16"/>
|
||||
</EdgeSize>
|
||||
</Backdrop>
|
||||
<Layers>
|
||||
<!-- ARTWORK, not BACKGROUND: the Backdrop draws its own
|
||||
background into BACKGROUND after the textures declared here,
|
||||
which would bury this bar. The title text is in OVERLAY, so
|
||||
it still reads over the top. -->
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="$parentTitleBG" file="Interface\QuestFrame\UI-QuestLogTitleHighlight">
|
||||
<Size x="280" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP">
|
||||
<Offset x="0" y="-6"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Color r=".6" g=".6" b=".6" a=".4"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentTitle" inherits="GameFontNormal" text="Events">
|
||||
<Anchors>
|
||||
<Anchor point="TOP">
|
||||
<Offset x="0" y="-8"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="$parentCloseButton" inherits="UIPanelCloseButton">
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT">
|
||||
<Offset x="-2" y="-2"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</Button>
|
||||
<Frame name="$parentTitleButton">
|
||||
<Size x="280" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP">
|
||||
<Offset x="0" y="-6"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this:RegisterForDrag("LeftButton");
|
||||
</OnLoad>
|
||||
<OnDragStart>
|
||||
EventTraceFrame.moving = true;
|
||||
EventTraceFrame:StartMoving();
|
||||
</OnDragStart>
|
||||
<OnDragStop>
|
||||
EventTraceFrame.moving = nil;
|
||||
EventTraceFrame:StopMovingOrSizing();
|
||||
</OnDragStop>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<!-- Pushed down 26px (was y=-28) to make room for the
|
||||
toolbar row of Pause + Args/Time toggles + Search. -->
|
||||
<Slider name="$parentScroll" orientation="VERTICAL">
|
||||
<Size x="16" y="0"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT">
|
||||
<Offset x="-7" y="-54"/>
|
||||
</Anchor>
|
||||
<Anchor point="BOTTOMRIGHT">
|
||||
<Offset x="-7" y="10"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentBG" setAllPoints="true">
|
||||
<Color r=".8" g=".8" b="1" a="0.1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this:SetFrameLevel(this:GetFrameLevel() + 1);
|
||||
this:SetMinMaxValues(0, 1);
|
||||
this:SetValue(0);
|
||||
this:SetValueStep(1);
|
||||
</OnLoad>
|
||||
<OnValueChanged>
|
||||
EventTraceFrame_Update();
|
||||
</OnValueChanged>
|
||||
</Scripts>
|
||||
<ThumbTexture name="$parentThumb" file="Interface\Buttons\UI-ScrollBar-Knob">
|
||||
<Size x="16" y="16"/>
|
||||
<TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/>
|
||||
</ThumbTexture>
|
||||
</Slider>
|
||||
<!-- ===== Toolbar (Pause + Args/Time toggles + Search) ===== -->
|
||||
<Button name="$parentPauseButton" inherits="UIPanelButtonTemplate" text="Pause">
|
||||
<Size x="60" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset x="12" y="-30"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>EventTraceFrame_TogglePause();</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<!-- `UICheckButtonTemplate`'s `$parentText` FontString is
|
||||
auto-positioned at the right of the box. Vanilla 1.12
|
||||
doesn't read a `text=` XML attribute (that came in
|
||||
TBC); set it from OnLoad via the global lookup
|
||||
instead. The earlier approach of separate sibling
|
||||
FontStrings in the parent's `<Layers>` block fell
|
||||
over because anchors that reference later-defined
|
||||
siblings ($parentArgsCheck etc.) resolve to (0,0)
|
||||
when the anchor is computed before the target exists. -->
|
||||
<!-- Initial checked state is set here from defaults so the
|
||||
first paint matches `_EventTraceFrame.showArguments`
|
||||
/ `showTimestamp`. `_ApplySavedVars` later overrides
|
||||
from `DebugToolsSavedVars` once ADDON_LOADED fires;
|
||||
if the user toggled the box to a non-default value
|
||||
in a prior session, the second SetChecked call wins. -->
|
||||
<CheckButton name="$parentArgsCheck" inherits="UICheckButtonTemplate">
|
||||
<Size x="20" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentPauseButton" relativePoint="RIGHT">
|
||||
<Offset x="4" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
getglobal(this:GetName() .. "Text"):SetText("Args");
|
||||
this:SetChecked(nil); -- showArguments default = false
|
||||
</OnLoad>
|
||||
<OnClick>EventTraceFrame_ToggleArgs(this:GetChecked());</OnClick>
|
||||
</Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="$parentTimeCheck" inherits="UICheckButtonTemplate">
|
||||
<Size x="20" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentArgsCheck" relativePoint="RIGHT">
|
||||
<Offset x="40" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
getglobal(this:GetName() .. "Text"):SetText("Time");
|
||||
this:SetChecked(1); -- showTimestamp default = true
|
||||
</OnLoad>
|
||||
<OnClick>EventTraceFrame_ToggleTimestamp(this:GetChecked());</OnClick>
|
||||
</Scripts>
|
||||
</CheckButton>
|
||||
<EditBox name="$parentSearch" inherits="InputBoxTemplate" autoFocus="false">
|
||||
<Size x="100" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentTimeCheck" relativePoint="RIGHT">
|
||||
<Offset x="48" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnTextChanged>EventTraceFrame_OnSearchChanged(this);</OnTextChanged>
|
||||
<!-- Escape unfocuses but preserves text — clearing is
|
||||
the X button's job. Modern WoW behaves the same. -->
|
||||
<OnEscapePressed>this:ClearFocus();</OnEscapePressed>
|
||||
</Scripts>
|
||||
<Frames>
|
||||
<!-- Embedded clear button: anchored inside the editbox
|
||||
at the right edge, visible only while there's text.
|
||||
Declared as a child so sibling z-order puts it
|
||||
above the editbox's own backdrop textures. -->
|
||||
<Button name="$parentClear" hidden="true">
|
||||
<Size x="14" y="14"/>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT">
|
||||
<Offset x="-4" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentLabel" inherits="GameFontNormalSmall" text="X">
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
local eb = getglobal("EventTraceFrameSearch");
|
||||
eb:SetText("");
|
||||
eb:ClearFocus();
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" setAllPoints="true" alphaMode="ADD">
|
||||
<Color r="1" g="1" b="1" a=".5"/>
|
||||
</HighlightTexture>
|
||||
</Button>
|
||||
</Frames>
|
||||
</EditBox>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>EventTraceFrame_OnLoad(this);</OnLoad>
|
||||
<OnEvent>EventTraceFrame_OnEvent(this, event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19);</OnEvent>
|
||||
<OnUpdate>EventTraceFrame_OnUpdate(this, arg1);</OnUpdate>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- ScriptErrorsFrame -->
|
||||
<!-- ================================================================ -->
|
||||
|
||||
<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>
|
||||
</Layer>
|
||||
<Layer level="ARTWORK">
|
||||
<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">
|
||||
<Offset x="0" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this:RegisterForDrag("LeftButton");
|
||||
</OnLoad>
|
||||
<OnDragStart>ScriptErrorsFrame_TitleButton_OnDragStart();</OnDragStart>
|
||||
<OnDragStop>ScriptErrorsFrame_TitleButton_OnDragStop();</OnDragStop>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<!-- Width leaves room for the scroll bar inside the border.
|
||||
UIPanelScrollFrameTemplate hangs its bar off this frame's
|
||||
right edge (+6, 16 wide, button art a little wider), so the
|
||||
bar ends at about right + 23. The frame's interior margin is
|
||||
368, where the Close button ends, which puts the widest this
|
||||
can be at 328. Blizzard's own numbers do not carry over here:
|
||||
its ScriptErrorsFrame inherits UIPanelDialogTemplate, a thin
|
||||
GearManager border, not this 32px dialog backdrop. -->
|
||||
<ScrollFrame name="$parentScrollFrame" inherits="UIPanelScrollFrameTemplate">
|
||||
<Size x="328" y="170"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset x="16" y="-32"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<ScrollChild>
|
||||
<EditBox name="$parentText" multiLine="true" letters="4000" autoFocus="false">
|
||||
<Size x="328" y="170"/>
|
||||
<Scripts>
|
||||
<OnEscapePressed>this:ClearFocus();</OnEscapePressed>
|
||||
<OnEditFocusGained>this:HighlightText(0);</OnEditFocusGained>
|
||||
</Scripts>
|
||||
<FontString inherits="GameFontHighlightSmall"/>
|
||||
</EditBox>
|
||||
</ScrollChild>
|
||||
</ScrollFrame>
|
||||
<Button name="$parentPrevious" inherits="UIPanelButtonTemplate" text="Previous" id="2">
|
||||
<Size x="96" y="24"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMLEFT">
|
||||
<Offset x="16" y="16"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>ScriptErrorsFrameButton_OnClick(this);</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<Button name="$parentNext" inherits="UIPanelButtonTemplate" text="Next" id="1">
|
||||
<Size x="96" y="24"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMLEFT">
|
||||
<Offset x="116" y="16"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>ScriptErrorsFrameButton_OnClick(this);</OnClick>
|
||||
</Scripts>
|
||||
</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_OnShow(this);</OnShow>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- Frame highlight (retail Blizzard_DebugTools.xml) -->
|
||||
<!-- ================================================================ -->
|
||||
@@ -440,10 +43,4 @@
|
||||
</Scripts>
|
||||
</GameTooltip>
|
||||
|
||||
<GameTooltip name="EventTraceTooltip" frameStrata="TOOLTIP" hidden="true" parent="EventTraceFrame" inherits="GameTooltipTemplate">
|
||||
<Scripts>
|
||||
<OnLoad>DebugTooltip_OnLoad(this);</OnLoad>
|
||||
</Scripts>
|
||||
</GameTooltip>
|
||||
|
||||
</Ui>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
-- Addons tab: the memory, allocation rate and CPU time of each addon.
|
||||
local _, ns = ...
|
||||
|
||||
local stats = ns.stats
|
||||
|
||||
local function Profiled(fn)
|
||||
return function(addon)
|
||||
if ns.profiling then return fn(addon) end
|
||||
end
|
||||
end
|
||||
|
||||
local tab = ns.AddTab("Addons")
|
||||
|
||||
tab.list = ns.CreateList(tab.frame, {
|
||||
sort = 3,
|
||||
columns = {
|
||||
{
|
||||
title = "Addon",
|
||||
text = function(addon) return addon.title end,
|
||||
value = function(addon) return strlower(addon.name) end,
|
||||
},
|
||||
{
|
||||
title = "Memory", width = 70,
|
||||
tip = "The Lua memory that the addon's objects use now.",
|
||||
text = function(addon) return ns.KB(addon.mem) end,
|
||||
value = function(addon) return addon.mem end,
|
||||
},
|
||||
{
|
||||
title = "Alloc/s", width = 76,
|
||||
tip = "The Lua memory the addon allocated in the last second. Allocations become garbage that the collector must free. A second with a garbage collection keeps the previous value.",
|
||||
text = function(addon) return ns.KBRate(addon.memRate) end,
|
||||
value = function(addon) return addon.memRate end,
|
||||
},
|
||||
{
|
||||
title = "Peak/s", width = 76,
|
||||
tip = "The highest Alloc/s since the last reset.",
|
||||
text = function(addon) return ns.KBRate(addon.peakRate) end,
|
||||
value = function(addon) return addon.peakRate end,
|
||||
},
|
||||
{
|
||||
title = "CPU/s", width = 64,
|
||||
tip = "The CPU time of the addon's own functions in the last second. Needs CPU profiling.",
|
||||
text = Profiled(function(addon) return ns.MS(addon.cpuRate) end),
|
||||
value = Profiled(function(addon) return addon.cpuRate end),
|
||||
},
|
||||
{
|
||||
title = "CPU %", width = 50,
|
||||
tip = "The addon's share of the CPU time of all addons in the last second.",
|
||||
text = Profiled(function(addon) return addon.cpuShare and format("%.1f%%", addon.cpuShare) end),
|
||||
value = Profiled(function(addon) return addon.cpuShare end),
|
||||
},
|
||||
{
|
||||
title = "CPU Total", width = 70,
|
||||
tip = "The CPU time of the addon's own functions since the last reset.",
|
||||
text = Profiled(function(addon) return ns.MS(addon.cpu) end),
|
||||
value = Profiled(function(addon) return addon.cpu end),
|
||||
},
|
||||
},
|
||||
|
||||
tooltip = function(addon)
|
||||
GameTooltip:AddLine(addon.title)
|
||||
GameTooltip:AddLine(addon.name, .6, .6, .6)
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddDoubleLine("Memory", ns.KB(addon.mem), 1, 1, 1, 1, 1, 1)
|
||||
GameTooltip:AddDoubleLine("Allocated per second", ns.KBRate(addon.memRate), 1, 1, 1, 1, 1, 1)
|
||||
GameTooltip:AddDoubleLine("Peak per second", ns.KBRate(addon.peakRate), 1, 1, 1, 1, 1, 1)
|
||||
if ns.profiling then
|
||||
GameTooltip:AddDoubleLine("CPU per second", ns.MS(addon.cpuRate), 1, 1, 1, 1, 1, 1)
|
||||
GameTooltip:AddDoubleLine("CPU since reset", ns.MS(addon.cpu), 1, 1, 1, 1, 1, 1)
|
||||
else
|
||||
GameTooltip:AddLine("Turn on CPU profiling to see CPU times.", .6, .6, .6)
|
||||
end
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("Click: show its frames. Right-click: more.", .6, .6, .6)
|
||||
end,
|
||||
|
||||
click = function(addon)
|
||||
ns.ShowIn("Frames", addon.name)
|
||||
end,
|
||||
|
||||
menu = function(addon, root)
|
||||
root:CreateTitle(addon.name)
|
||||
root:CreateButton("Show frames", function() ns.ShowIn("Frames", addon.name) end)
|
||||
root:CreateButton("Show globals", function() ns.ShowIn("Globals", addon.name) end)
|
||||
root:CreateButton("Copy name", function() ns.Copy(addon.name) end)
|
||||
end,
|
||||
})
|
||||
tab.list:SetAllPoints()
|
||||
|
||||
function tab:OnSelect()
|
||||
self.list:SetEntries(stats.list)
|
||||
end
|
||||
|
||||
function tab:OnSample()
|
||||
self.list:SetEntries(stats.list)
|
||||
end
|
||||
|
||||
function tab:Status()
|
||||
local text = format("%d addons Lua %s / %s", #stats.list, ns.KB(stats.mem or 0), ns.KB(stats.threshold or 0))
|
||||
if ns.profiling and stats.scriptRate then
|
||||
text = text .. format(" scripts %s/s events %s/s (%.0f fired/s)",
|
||||
ns.MS(stats.scriptRate), ns.MS(stats.eventRate), stats.eventCountRate)
|
||||
end
|
||||
return text
|
||||
end
|
||||
@@ -0,0 +1,283 @@
|
||||
-- Profiler core: shared helpers, the once-a-second sampler and the per-addon
|
||||
-- memory and CPU statistics every other profiler file reads.
|
||||
--
|
||||
-- Based on pfDebug (https://github.com/shagu/pfDebug):
|
||||
--
|
||||
-- MIT License
|
||||
--
|
||||
-- Copyright (c) 2018 Shagu
|
||||
--
|
||||
-- Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
-- of this software and associated documentation files (the "Software"), to deal
|
||||
-- in the Software without restriction, including without limitation the rights
|
||||
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
-- copies of the Software, and to permit persons to whom the Software is
|
||||
-- furnished to do so, subject to the following conditions:
|
||||
--
|
||||
-- The above copyright notice and this permission notice shall be included in all
|
||||
-- copies or substantial portions of the Software.
|
||||
--
|
||||
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
-- SOFTWARE.
|
||||
local _, ns = ...
|
||||
|
||||
ns.INTERVAL = 1 -- seconds between samples
|
||||
|
||||
-- whether CPU profiling runs in this session. scriptProfile takes effect at
|
||||
-- a reload and these tools load on demand, so the cvar can already hold the
|
||||
-- next session's value: ask the profiler whether it counted a call instead.
|
||||
local function Probe() end
|
||||
Probe()
|
||||
ns.profiling = select(2, GetFunctionCPUUsage(Probe)) > 0
|
||||
|
||||
-- [[ styling ]]
|
||||
function ns.CreateBackdrop(frame)
|
||||
frame:SetBackdrop({
|
||||
bgFile = "Interface\\BUTTONS\\WHITE8X8", tile = false, tileSize = 0,
|
||||
edgeFile = "Interface\\BUTTONS\\WHITE8X8", edgeSize = 1,
|
||||
insets = {left = -1, right = -1, top = -1, bottom = -1},
|
||||
})
|
||||
frame:SetBackdropColor(0, 0, 0, .75)
|
||||
frame:SetBackdropBorderColor(.1, .1, .1, 1)
|
||||
end
|
||||
|
||||
-- [[ formatting ]]
|
||||
ns.ACCENT = "|cff33ffcc"
|
||||
ns.GOLD = "|cffffcc00"
|
||||
ns.GREY = "|cffaaaaaa"
|
||||
ns.WHITE = "|cffffffff"
|
||||
ns.RED = "|cffff5555"
|
||||
|
||||
function ns.Print(msg)
|
||||
DEFAULT_CHAT_FRAME:AddMessage(ns.ACCENT .. "Profiler|r: " .. msg)
|
||||
end
|
||||
|
||||
function ns.KB(kb)
|
||||
if math.abs(kb) >= 1024 then return format("%.1f MB", kb / 1024) end
|
||||
return format("%.0f KB", kb)
|
||||
end
|
||||
|
||||
function ns.KBRate(kbps)
|
||||
if not kbps then return "-" end
|
||||
if math.abs(kbps) >= 1024 then return format("%.2f MB/s", kbps / 1024) end
|
||||
return format("%.1f KB/s", kbps)
|
||||
end
|
||||
|
||||
function ns.MS(ms)
|
||||
if not ms then return "-" end
|
||||
if ms >= 10000 then return format("%.1f s", ms / 1000) end
|
||||
if ms >= 100 then return format("%.0f ms", ms) end
|
||||
if ms >= 1 then return format("%.1f ms", ms) end
|
||||
return format("%.3f ms", ms)
|
||||
end
|
||||
|
||||
function ns.Count(n)
|
||||
if not n then return "-" end
|
||||
return BreakUpLargeNumbers(floor(n + .5))
|
||||
end
|
||||
|
||||
-- [[ ownership: which addon made a table, function or frame ]]
|
||||
-- nil for objects the game's own code made (FrameXML or the engine), "script"
|
||||
-- for objects a macro or /run made.
|
||||
function ns.Owner(value)
|
||||
local tag = _classicapi_GcTagOf(value)
|
||||
if tag and strsub(tag, 1, 2) == "0x" then return "script" end
|
||||
return tag
|
||||
end
|
||||
|
||||
function ns.OwnerLabel(owner)
|
||||
return owner or "FrameXML"
|
||||
end
|
||||
|
||||
-- a frame's name as the debug tools show it
|
||||
function ns.FrameName(frame)
|
||||
local name = frame:GetDebugName()
|
||||
if name == "" then return "<" .. frame:GetObjectType() .. ">" end
|
||||
return name
|
||||
end
|
||||
|
||||
-- [[ events of this module: sample and reset listeners ]]
|
||||
local sampleListeners, resetListeners = {}, {}
|
||||
|
||||
function ns.OnSample(fn) tinsert(sampleListeners, fn) end
|
||||
function ns.OnReset(fn) tinsert(resetListeners, fn) end
|
||||
|
||||
-- [[ per-addon statistics ]]
|
||||
-- addon rates are deltas between two samples. A garbage collection between
|
||||
-- the samples frees memory of every addon, so a window with a collection
|
||||
-- holds the previous allocation rate.
|
||||
local stats = {
|
||||
addons = {}, -- [name] = record
|
||||
list = {}, -- loaded addon records, in load order
|
||||
}
|
||||
ns.stats = stats
|
||||
|
||||
local gc = { count = 0 }
|
||||
ns.gc = gc
|
||||
|
||||
local collected = false
|
||||
local lastMem, lastThreshold = gcinfo()
|
||||
|
||||
local function NewAddon(index, name, title)
|
||||
local addon = { index = index, name = name, title = title, mem = 0, cpu = 0, peakRate = 0 }
|
||||
addon.search = strlower(name .. " " .. title)
|
||||
stats.addons[name] = addon
|
||||
tinsert(stats.list, addon)
|
||||
return addon
|
||||
end
|
||||
|
||||
-- a cumulative counter's growth since `last`, where a drop means a reset
|
||||
local function Delta(now, last)
|
||||
if not last or now < last then return now end
|
||||
return now - last
|
||||
end
|
||||
ns.Delta = Delta
|
||||
|
||||
local lastScript, lastEventTime, lastEventCount
|
||||
|
||||
local function Sample(dt)
|
||||
local mem, threshold = gcinfo()
|
||||
stats.collected = collected
|
||||
stats.memRate = not collected and stats.mem and (mem - stats.mem) / dt or nil
|
||||
stats.mem, stats.threshold = mem, threshold
|
||||
collected = false
|
||||
|
||||
UpdateAddOnMemoryUsage()
|
||||
if ns.profiling then UpdateAddOnCPUUsage() end
|
||||
|
||||
local cpuSum, topMem, topCpu = 0, nil, nil
|
||||
for i = 1, GetNumAddOns() do
|
||||
if IsAddOnLoaded(i) then
|
||||
local name, title = GetAddOnInfo(i)
|
||||
local addon = stats.addons[name] or NewAddon(i, name, title)
|
||||
|
||||
local addonMem = GetAddOnMemoryUsage(i)
|
||||
if not stats.collected and addon.sampled then
|
||||
addon.memRate = (addonMem - addon.mem) / dt
|
||||
addon.peakRate = max(addon.peakRate, addon.memRate)
|
||||
end
|
||||
addon.mem, addon.sampled = addonMem, true
|
||||
|
||||
if ns.profiling then
|
||||
local cpu = GetAddOnCPUUsage(i)
|
||||
addon.cpuRate = Delta(cpu, addon.cpu) / dt
|
||||
addon.cpu = cpu
|
||||
cpuSum = cpuSum + addon.cpuRate
|
||||
if not topCpu or addon.cpuRate > topCpu.cpuRate then topCpu = addon end
|
||||
end
|
||||
|
||||
if addon.memRate and (not topMem or addon.memRate > topMem.memRate) then topMem = addon end
|
||||
end
|
||||
end
|
||||
|
||||
for _, addon in ipairs(stats.list) do
|
||||
addon.cpuShare = cpuSum > 0 and addon.cpuRate and addon.cpuRate / cpuSum * 100 or nil
|
||||
end
|
||||
stats.cpuSum, stats.topMem, stats.topCpu = cpuSum, topMem, topCpu
|
||||
|
||||
if ns.profiling then
|
||||
local script = GetScriptCPUUsage()
|
||||
local eventTime, eventCount = GetEventCPUUsage()
|
||||
stats.scriptRate = Delta(script, lastScript) / dt
|
||||
stats.eventRate = Delta(eventTime, lastEventTime) / dt
|
||||
stats.eventCountRate = Delta(eventCount, lastEventCount) / dt
|
||||
lastScript, lastEventTime, lastEventCount = script, eventTime, eventCount
|
||||
end
|
||||
|
||||
for _, fn in ipairs(sampleListeners) do fn(dt) end
|
||||
end
|
||||
|
||||
-- the counters as they are now, so the first sample's rates cover only its
|
||||
-- own second, not the time since the UI loaded
|
||||
local function Baseline()
|
||||
stats.mem, stats.threshold = gcinfo()
|
||||
lastMem, lastThreshold = stats.mem, stats.threshold
|
||||
|
||||
UpdateAddOnMemoryUsage()
|
||||
if ns.profiling then UpdateAddOnCPUUsage() end
|
||||
for i = 1, GetNumAddOns() do
|
||||
if IsAddOnLoaded(i) then
|
||||
local name, title = GetAddOnInfo(i)
|
||||
local addon = stats.addons[name] or NewAddon(i, name, title)
|
||||
addon.mem, addon.sampled = GetAddOnMemoryUsage(i), true
|
||||
if ns.profiling then addon.cpu = GetAddOnCPUUsage(i) end
|
||||
end
|
||||
end
|
||||
|
||||
if ns.profiling then
|
||||
lastScript = GetScriptCPUUsage()
|
||||
lastEventTime, lastEventCount = GetEventCPUUsage()
|
||||
end
|
||||
end
|
||||
|
||||
function ns.Reset()
|
||||
ResetCPUUsage()
|
||||
lastScript, lastEventTime, lastEventCount = nil, nil, nil
|
||||
for _, addon in ipairs(stats.list) do
|
||||
addon.cpu, addon.peakRate = 0, 0
|
||||
end
|
||||
for _, fn in ipairs(resetListeners) do fn() end
|
||||
end
|
||||
|
||||
-- [[ the sampler: every frame, from the first open of the widget or the
|
||||
-- analyzer until /reload ]]
|
||||
-- The debug tools also load for /dump and the rest, so the profiler costs
|
||||
-- nothing until it is opened.
|
||||
local sampler = CreateFrame("Frame")
|
||||
sampler:Hide()
|
||||
local elapsedSinceSample = 0
|
||||
|
||||
-- spot garbage collections: Lua 5.0 sets a new threshold after each
|
||||
-- collection, so a changed threshold means a collection ran
|
||||
sampler:SetScript("OnUpdate", function(self, elapsed)
|
||||
local mem, threshold = gcinfo()
|
||||
if threshold ~= lastThreshold then
|
||||
collected = true
|
||||
gc.count = gc.count + 1
|
||||
gc.time, gc.clock, gc.freed = GetTime(), date("%H:%M:%S"), lastMem - mem
|
||||
end
|
||||
lastMem, lastThreshold = mem, threshold
|
||||
|
||||
elapsedSinceSample = elapsedSinceSample + elapsed
|
||||
if elapsedSinceSample >= ns.INTERVAL then
|
||||
Sample(elapsedSinceSample)
|
||||
elapsedSinceSample = 0
|
||||
end
|
||||
end)
|
||||
|
||||
function ns.StartSampling()
|
||||
if sampler:IsShown() then return end
|
||||
Baseline()
|
||||
sampler:Show()
|
||||
end
|
||||
|
||||
-- [[ CPU profiling switch: the cvar takes effect after a reload ]]
|
||||
function ns.SetProfiling(enable)
|
||||
SetCVar("scriptProfile", enable and "1" or "0")
|
||||
ReloadUI()
|
||||
end
|
||||
|
||||
-- [[ /profiler ]]
|
||||
function ClassicAPIProfiler_HandleSlashCmd(msg)
|
||||
local cmd, arg = strmatch(strlower(msg or ""), "^(%S*)%s*(.-)$")
|
||||
if cmd == "" then
|
||||
ns.window:SetShown(not ns.window:IsShown())
|
||||
elseif cmd == "widget" then
|
||||
ns.widget:SetShown(not ns.widget:IsShown())
|
||||
elseif cmd == "reset" then
|
||||
ns.Reset()
|
||||
ns.Print("CPU and allocation counters reset.")
|
||||
elseif cmd == "profile" and (arg == "on" or arg == "off") then
|
||||
ns.SetProfiling(arg == "on")
|
||||
else
|
||||
ns.Print("/profiler - toggle the analyzer")
|
||||
ns.Print("/profiler widget - toggle the memory widget")
|
||||
ns.Print("/profiler reset - reset CPU and allocation counters")
|
||||
ns.Print("/profiler profile on|off - CPU profiling (reloads the UI)")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,124 @@
|
||||
-- Events tab: how often each event fires while the analyzer is open, how
|
||||
-- many frames listen to it, and (deep tracking) what its handlers cost.
|
||||
local _, ns = ...
|
||||
|
||||
local track = ns.track
|
||||
|
||||
local function Tracked(fn)
|
||||
return function(ev)
|
||||
if track.deep then return fn(ev) end
|
||||
end
|
||||
end
|
||||
|
||||
local function Line(left, right)
|
||||
GameTooltip:AddDoubleLine(left, right, 1, 1, 1, 1, 1, 1)
|
||||
end
|
||||
|
||||
local function Tooltip(ev)
|
||||
GameTooltip:AddLine(ev.name)
|
||||
Line("Fired", ns.Count(ev.fires) .. ns.GREY .. " (" .. format("%.1f", ev.fireRate or 0) .. "/s)")
|
||||
if track.deep then
|
||||
Line("Handlers ran", ns.Count(ev.dispatches))
|
||||
if ns.profiling then Line("Handler CPU", ns.MS(ev.cpu)) end
|
||||
Line("Allocated", ns.KB(ev.alloc))
|
||||
end
|
||||
|
||||
local listeners = {}
|
||||
for _, frame in ipairs({ GetFramesRegisteredForEvent(ev.name) }) do
|
||||
if frame ~= track.capture then
|
||||
local rec = track.frames[frame]
|
||||
tinsert(listeners, { frame = frame, stat = rec and rec.events[ev.name] })
|
||||
end
|
||||
end
|
||||
if #listeners == 0 then return end
|
||||
|
||||
table.sort(listeners, function(a, b)
|
||||
local ca, cb = a.stat and a.stat.cpu or -1, b.stat and b.stat.cpu or -1
|
||||
return ca > cb
|
||||
end)
|
||||
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("Listeners", 1, .8, 0)
|
||||
for i = 1, min(#listeners, 15) do
|
||||
local frame, stat = listeners[i].frame, listeners[i].stat
|
||||
local text = ns.GREY .. ns.OwnerLabel(ns.Owner(frame:GetScript("OnEvent") or frame))
|
||||
if stat then
|
||||
if ns.profiling then text = text .. " " .. ns.WHITE .. ns.MS(stat.cpu) end
|
||||
text = text .. " " .. ns.WHITE .. ns.KB(stat.alloc)
|
||||
end
|
||||
Line(ns.FrameName(frame), text)
|
||||
end
|
||||
if #listeners > 15 then
|
||||
GameTooltip:AddLine("and " .. (#listeners - 15) .. " more", .6, .6, .6)
|
||||
end
|
||||
end
|
||||
|
||||
local tab = ns.AddTab("Events")
|
||||
|
||||
tab.list = ns.CreateList(tab.frame, {
|
||||
sort = 2,
|
||||
columns = {
|
||||
{
|
||||
title = "Event",
|
||||
text = function(ev) return ev.name end,
|
||||
value = function(ev) return ev.search end,
|
||||
},
|
||||
{
|
||||
title = "Fires/s", width = 60,
|
||||
tip = "How often the event fired in the last second.",
|
||||
text = function(ev) return ev.fireRate and format("%.1f", ev.fireRate) end,
|
||||
value = function(ev) return ev.fireRate end,
|
||||
},
|
||||
{
|
||||
title = "Listeners", width = 60,
|
||||
tip = "The frames registered for the event.",
|
||||
text = function(ev) return ev.listeners end,
|
||||
value = function(ev) return ev.listeners end,
|
||||
},
|
||||
{
|
||||
title = "CPU/s", width = 64,
|
||||
tip = "The CPU time of the event's OnEvent handlers in the last second. Needs deep tracking and CPU profiling.",
|
||||
text = Tracked(function(ev) return ns.profiling and ns.MS(ev.cpuRate) end),
|
||||
value = Tracked(function(ev) return ns.profiling and ev.cpuRate or nil end),
|
||||
},
|
||||
{
|
||||
title = "Alloc/s", width = 72,
|
||||
tip = "The Lua memory the event's OnEvent handlers allocated in the last second. Needs deep tracking.",
|
||||
text = Tracked(function(ev) return ns.KBRate(ev.allocRate) end),
|
||||
value = Tracked(function(ev) return ev.allocRate end),
|
||||
},
|
||||
{
|
||||
title = "Fired", width = 64,
|
||||
tip = "How often the event fired since the analyzer opened or the last reset.",
|
||||
text = function(ev) return ns.Count(ev.fires) end,
|
||||
value = function(ev) return ev.fires end,
|
||||
},
|
||||
},
|
||||
|
||||
tooltip = Tooltip,
|
||||
|
||||
menu = function(ev, root)
|
||||
root:CreateTitle(ev.name)
|
||||
root:CreateButton("Copy name", function() ns.Copy(ev.name) end)
|
||||
end,
|
||||
})
|
||||
tab.list:SetAllPoints()
|
||||
|
||||
local function Update(self)
|
||||
local entries = {}
|
||||
for _, ev in pairs(track.events) do tinsert(entries, ev) end
|
||||
self.list:SetEntries(entries)
|
||||
end
|
||||
|
||||
tab.OnSelect = Update
|
||||
tab.OnSample = Update
|
||||
|
||||
function tab:Status()
|
||||
local fires = 0
|
||||
for _, ev in pairs(track.events) do fires = fires + (ev.fireRate or 0) end
|
||||
local text = format("%d events seen %.0f fired/s", #self.list.entries, fires)
|
||||
if not track.deep then
|
||||
return text .. " " .. ns.GOLD .. "Turn on deep tracking for the CPU time and memory of each event."
|
||||
end
|
||||
return text
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
-- /fstack: the owner, script CPU time and allocations of the highlighted
|
||||
-- frame, which FrameStackTooltip_OnTooltipSetFrameStack adds to the tooltip.
|
||||
local _, ns = ...
|
||||
|
||||
local function Line(tooltip, left, right)
|
||||
tooltip:AddDoubleLine(left, right, .2, 1, .8, 1, 1, 1)
|
||||
end
|
||||
|
||||
function ns.AddFrameStackLines(tooltip, region)
|
||||
if not region then return end
|
||||
-- a texture or font string runs no scripts: report its frame's
|
||||
local frame = region.GetScript and region or region:GetParent()
|
||||
|
||||
tooltip:AddLine(" ")
|
||||
Line(tooltip, "Owner", ns.OwnerLabel(ns.Owner(region)))
|
||||
if not frame then return end
|
||||
|
||||
for _, script in ipairs({ "OnUpdate", "OnEvent" }) do
|
||||
local fn = frame:GetScript(script)
|
||||
if fn then
|
||||
local text = ns.OwnerLabel(ns.Owner(fn))
|
||||
if ns.profiling then
|
||||
local ms, calls = GetFunctionCPUUsage(fn)
|
||||
text = text .. ns.GREY .. " " .. ns.MS(ms) .. " in " .. ns.Count(calls) .. " calls"
|
||||
end
|
||||
Line(tooltip, script, text)
|
||||
end
|
||||
end
|
||||
|
||||
if ns.profiling then
|
||||
local ms, calls = GetFrameCPUUsage(frame, false)
|
||||
Line(tooltip, "All scripts", ns.MS(ms) .. ns.GREY .. " in " .. ns.Count(calls) .. " calls")
|
||||
end
|
||||
|
||||
local rec = ns.track.frames[frame]
|
||||
if rec and (rec.hookedUpdate or rec.hookedEvent) then
|
||||
Line(tooltip, "Allocated", ns.KB(rec.alloc) .. ns.GREY .. " (" .. ns.KBRate(rec.allocRate) .. ")")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,166 @@
|
||||
-- Frames tab: the frames that run scripts, with the CPU time of all their
|
||||
-- scripts (CPU profiling) and the memory they allocate (deep tracking).
|
||||
local _, ns = ...
|
||||
|
||||
local track = ns.track
|
||||
|
||||
local function Profiled(fn)
|
||||
return function(rec)
|
||||
if ns.profiling then return fn(rec) end
|
||||
end
|
||||
end
|
||||
|
||||
local function Tracked(fn)
|
||||
return function(rec)
|
||||
if rec.hookedUpdate or rec.hookedEvent then return fn(rec) end
|
||||
end
|
||||
end
|
||||
|
||||
local function Line(left, right)
|
||||
GameTooltip:AddDoubleLine(left, right, 1, 1, 1, 1, 1, 1)
|
||||
end
|
||||
|
||||
-- the events of a frame, most expensive first
|
||||
local function SortedEvents(rec)
|
||||
local list = {}
|
||||
for name, stat in pairs(rec.events) do
|
||||
tinsert(list, { name = name, stat = stat })
|
||||
end
|
||||
local key = ns.profiling and "cpu" or "alloc"
|
||||
table.sort(list, function(a, b) return a.stat[key] > b.stat[key] end)
|
||||
return list
|
||||
end
|
||||
|
||||
local function Tooltip(rec)
|
||||
local frame = rec.frame
|
||||
GameTooltip:AddLine(rec.name)
|
||||
Line("Type", frame:GetObjectType())
|
||||
Line("Owner", ns.OwnerLabel(rec.owner))
|
||||
Line("State", frame:IsVisible() and "visible" or frame:IsShown() and "shown, parent hidden" or "hidden")
|
||||
|
||||
local source = frame:GetSourceLocation()
|
||||
if source then
|
||||
GameTooltip:AddLine(source, .6, .6, .6, true)
|
||||
end
|
||||
|
||||
GameTooltip:AddLine(" ")
|
||||
for _, script in ipairs({ "OnUpdate", "OnEvent" }) do
|
||||
local fn = frame:GetScript(script)
|
||||
if fn then
|
||||
local text = ns.OwnerLabel(ns.Owner(fn))
|
||||
if ns.profiling then
|
||||
local ms, calls = GetFunctionCPUUsage(fn)
|
||||
text = text .. ns.GREY .. " " .. ns.MS(ms) .. " in " .. ns.Count(calls) .. " calls"
|
||||
end
|
||||
Line(script, text)
|
||||
end
|
||||
end
|
||||
|
||||
if ns.profiling then
|
||||
Line("All scripts", ns.MS(rec.cpu) .. " in " .. ns.Count(rec.calls) .. " calls")
|
||||
end
|
||||
if rec.hookedUpdate or rec.hookedEvent then
|
||||
Line("Allocated", ns.KB(rec.alloc))
|
||||
end
|
||||
|
||||
local events = SortedEvents(rec)
|
||||
if #events > 0 then
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("Events", 1, .8, 0)
|
||||
for i = 1, min(#events, 12) do
|
||||
local stat = events[i].stat
|
||||
local text = ns.Count(stat.count) .. "x"
|
||||
if ns.profiling then text = text .. " " .. ns.MS(stat.cpu) end
|
||||
Line(events[i].name, text .. " " .. ns.KB(stat.alloc))
|
||||
end
|
||||
end
|
||||
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("Right-click: highlight, inspect or copy.", .6, .6, .6)
|
||||
end
|
||||
|
||||
local tab = ns.AddTab("Frames")
|
||||
|
||||
tab.list = ns.CreateList(tab.frame, {
|
||||
sort = 3,
|
||||
columns = {
|
||||
{
|
||||
title = "Frame",
|
||||
text = function(rec) return rec.name end,
|
||||
value = function(rec) return strlower(rec.name) end,
|
||||
},
|
||||
{
|
||||
title = "Owner", width = 100, justify = "LEFT",
|
||||
tip = "The addon that made the frame's OnUpdate or OnEvent handler, or the frame itself.",
|
||||
text = function(rec) return ns.OwnerLabel(rec.owner) end,
|
||||
value = function(rec) return strlower(ns.OwnerLabel(rec.owner)) end,
|
||||
},
|
||||
{
|
||||
title = "CPU/s", width = 64,
|
||||
tip = "The CPU time of all the frame's scripts in the last second. Needs CPU profiling.",
|
||||
text = Profiled(function(rec) return ns.MS(rec.cpuRate) end),
|
||||
value = Profiled(function(rec) return rec.cpuRate end),
|
||||
},
|
||||
{
|
||||
title = "Calls/s", width = 60,
|
||||
tip = "How often the frame's scripts ran in the last second. Needs CPU profiling.",
|
||||
text = Profiled(function(rec) return ns.Count(rec.callRate) end),
|
||||
value = Profiled(function(rec) return rec.callRate end),
|
||||
},
|
||||
{
|
||||
title = "Per Call", width = 64,
|
||||
tip = "The average CPU time of one script call since the last reset.",
|
||||
text = Profiled(function(rec) return rec.calls > 0 and ns.MS(rec.cpu / rec.calls) end),
|
||||
value = Profiled(function(rec) return rec.calls > 0 and rec.cpu / rec.calls or nil end),
|
||||
},
|
||||
{
|
||||
title = "Alloc/s", width = 72,
|
||||
tip = "The Lua memory the frame's OnUpdate and OnEvent scripts allocated in the last second. Needs deep tracking.",
|
||||
text = Tracked(function(rec) return ns.KBRate(rec.allocRate) end),
|
||||
value = Tracked(function(rec) return rec.allocRate end),
|
||||
},
|
||||
{
|
||||
title = "CPU Total", width = 64,
|
||||
tip = "The CPU time of all the frame's scripts since the last reset.",
|
||||
text = Profiled(function(rec) return ns.MS(rec.cpu) end),
|
||||
value = Profiled(function(rec) return rec.cpu end),
|
||||
},
|
||||
},
|
||||
|
||||
tooltip = Tooltip,
|
||||
|
||||
menu = function(rec, root)
|
||||
root:CreateTitle(rec.name)
|
||||
root:CreateButton("Highlight", function() ns.Highlight(rec.frame) end)
|
||||
root:CreateButton("Inspect", function() ns.Inspect(rec.frame) end)
|
||||
root:CreateButton("Copy name", function() ns.Copy(rec.name) end)
|
||||
local source = rec.frame:GetSourceLocation()
|
||||
if source then
|
||||
root:CreateButton("Copy source location", function() ns.Copy(source) end)
|
||||
end
|
||||
root:CreateButton("Show " .. ns.OwnerLabel(rec.owner) .. " frames", function()
|
||||
ns.ShowIn("Frames", ns.OwnerLabel(rec.owner))
|
||||
end)
|
||||
end,
|
||||
})
|
||||
tab.list:SetAllPoints()
|
||||
|
||||
local function Update(self)
|
||||
local entries = {}
|
||||
for _, rec in pairs(track.frames) do
|
||||
if rec.calls > 0 or rec.alloc > 0 then tinsert(entries, rec) end
|
||||
end
|
||||
self.list:SetEntries(entries)
|
||||
end
|
||||
|
||||
tab.OnSelect = Update
|
||||
tab.OnSample = Update
|
||||
|
||||
function tab:Status()
|
||||
local text = format("%d frames %d listed", track.frameCount, #self.list.shown)
|
||||
if not ns.profiling and not track.deep then
|
||||
return text .. " " .. ns.GOLD .. "Turn on CPU profiling or deep tracking to measure frames."
|
||||
end
|
||||
if track.deep then text = text .. " deep tracking on" end
|
||||
return text
|
||||
end
|
||||
@@ -0,0 +1,177 @@
|
||||
-- Globals tab: the global tables, functions and frames each addon made, the
|
||||
-- ClassicAPI functions that something replaced or hooked, and (CPU
|
||||
-- profiling) the time of every global function, game API included.
|
||||
local _, ns = ...
|
||||
|
||||
-- namespaces whose functions are listed as Name.key
|
||||
local LIBRARIES = { string = true, table = true, math = true, coroutine = true }
|
||||
|
||||
local function Kind(value)
|
||||
local t = type(value)
|
||||
if t == "table" and type(rawget(value, 0)) == "userdata" and value.GetObjectType then
|
||||
return value:GetObjectType()
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
local function Line(left, right)
|
||||
GameTooltip:AddDoubleLine(left, right, 1, 1, 1, 1, 1, 1)
|
||||
end
|
||||
|
||||
local function Tooltip(entry)
|
||||
local value = entry.value
|
||||
GameTooltip:AddLine(entry.name)
|
||||
Line("Type", entry.kind)
|
||||
Line("Owner", ns.OwnerLabel(entry.owner))
|
||||
if entry.override then
|
||||
GameTooltip:AddLine("Not ClassicAPI's function: something replaced or hooked it.", 1, .3, .3, true)
|
||||
end
|
||||
|
||||
if entry.kind == "function" and ns.profiling then
|
||||
Line("CPU", ns.MS(entry.cpu))
|
||||
Line("Calls", ns.Count(entry.calls))
|
||||
if entry.calls > 0 then Line("Per call", ns.MS(entry.cpu / entry.calls)) end
|
||||
elseif entry.kind == "table" then
|
||||
Line("Entries", ns.Count(table.count(value)))
|
||||
elseif type(value) == "table" then
|
||||
Line("Debug name", ns.FrameName(value))
|
||||
local source = value:GetSourceLocation()
|
||||
if source then GameTooltip:AddLine(source, .6, .6, .6, true) end
|
||||
elseif type(value) ~= "function" and value ~= nil then
|
||||
Line("Value", strsub(tostring(value), 1, 60))
|
||||
end
|
||||
end
|
||||
|
||||
local tab = ns.AddTab("Globals")
|
||||
tab.untagged = false
|
||||
|
||||
tab.list = ns.CreateList(tab.frame, {
|
||||
sort = 3,
|
||||
columns = {
|
||||
{
|
||||
title = "Name",
|
||||
text = function(entry)
|
||||
if entry.override then return ns.RED .. entry.name .. ns.GREY .. " (replaced)" end
|
||||
return entry.name
|
||||
end,
|
||||
value = function(entry) return strlower(entry.name) end,
|
||||
},
|
||||
{
|
||||
title = "Type", width = 80, justify = "LEFT",
|
||||
text = function(entry) return entry.kind end,
|
||||
value = function(entry) return entry.kind end,
|
||||
},
|
||||
{
|
||||
title = "Owner", width = 100, justify = "LEFT",
|
||||
tip = "The addon that made the value. Strings, numbers and booleans have no owner.",
|
||||
text = function(entry) return ns.OwnerLabel(entry.owner) end,
|
||||
value = function(entry) return strlower(ns.OwnerLabel(entry.owner)) end,
|
||||
},
|
||||
{
|
||||
title = "CPU", width = 64,
|
||||
tip = "The CPU time of the function since the last reset. Needs CPU profiling.",
|
||||
text = function(entry) return entry.cpu and ns.MS(entry.cpu) end,
|
||||
value = function(entry) return entry.cpu end,
|
||||
},
|
||||
{
|
||||
title = "Calls", width = 70,
|
||||
tip = "How often the function ran since the last reset. Needs CPU profiling.",
|
||||
text = function(entry) return entry.calls and ns.Count(entry.calls) end,
|
||||
value = function(entry) return entry.calls end,
|
||||
},
|
||||
},
|
||||
|
||||
tooltip = Tooltip,
|
||||
|
||||
menu = function(entry, root)
|
||||
local value = entry.value
|
||||
root:CreateTitle(entry.name)
|
||||
if type(value) == "table" then
|
||||
root:CreateButton("Inspect", function() ns.Inspect(value) end)
|
||||
if entry.kind ~= "table" then
|
||||
root:CreateButton("Highlight", function() ns.Highlight(value) end)
|
||||
end
|
||||
end
|
||||
root:CreateButton("Copy name", function() ns.Copy(entry.name) end)
|
||||
end,
|
||||
})
|
||||
tab.list:SetPoint("TOPLEFT", 0, -26)
|
||||
tab.list:SetPoint("BOTTOMRIGHT")
|
||||
|
||||
-- [[ scan ]]
|
||||
local function Scan()
|
||||
-- the ClassicAPI functions whose global is no longer ClassicAPI's
|
||||
local overrides = {}
|
||||
for name, fn in pairs(ClassicAPI) do
|
||||
if type(fn) == "function" then
|
||||
if _G[name] ~= fn then overrides[name] = true end
|
||||
elseif type(fn) == "table" and type(_G[name]) == "table" then
|
||||
for key, sub in pairs(fn) do
|
||||
if type(sub) == "function" and _G[name][key] ~= sub then overrides[name .. "." .. key] = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local entries = {}
|
||||
local function Add(name, value)
|
||||
local kind = Kind(value)
|
||||
local owner = (kind ~= "string" and kind ~= "number" and kind ~= "boolean" and kind ~= "nil") and ns.Owner(value) or nil
|
||||
local override = overrides[name]
|
||||
if not owner and not override and not tab.untagged then return end
|
||||
|
||||
local entry = { name = name, value = value, kind = kind, owner = owner, override = override }
|
||||
entry.search = strlower(name .. " " .. ns.OwnerLabel(owner))
|
||||
if kind == "function" and ns.profiling then
|
||||
entry.cpu, entry.calls = GetFunctionCPUUsage(value)
|
||||
end
|
||||
tinsert(entries, entry)
|
||||
overrides[name] = nil
|
||||
end
|
||||
|
||||
for name, value in pairs(_G) do
|
||||
if type(name) == "string" then
|
||||
Add(name, value)
|
||||
if type(value) == "table" and (LIBRARIES[name] or strfind(name, "^C_")) then
|
||||
for key, fn in pairs(value) do
|
||||
if type(key) == "string" and type(fn) == "function" then Add(name .. "." .. key, fn) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- overridden names that are gone or live outside the scanned namespaces
|
||||
for name in pairs(overrides) do
|
||||
local space, key = strmatch(name, "^([^.]+)%.(.+)$")
|
||||
Add(name, space and _G[space][key] or _G[name])
|
||||
end
|
||||
|
||||
tab.count = #entries
|
||||
tab.overrides = 0
|
||||
for _, entry in ipairs(entries) do
|
||||
if entry.override then tab.overrides = tab.overrides + 1 end
|
||||
end
|
||||
tab.list:SetEntries(entries)
|
||||
end
|
||||
|
||||
local rescan = ns.CreateButton("ClassicAPIProfilerWindowRescan", tab.frame, 80, "Rescan", Scan)
|
||||
rescan:SetPoint("TOPRIGHT", -4, -2)
|
||||
ns.Tooltip(rescan, "Read the global namespace again. Also updates the CPU times.")
|
||||
|
||||
local untagged = CreateFrame("CheckButton", "ClassicAPIProfilerWindowUntagged", tab.frame, "UICheckButtonTemplate")
|
||||
untagged:SetSize(20, 20)
|
||||
untagged:SetPoint("RIGHT", rescan, "LEFT", -110, 0)
|
||||
ClassicAPIProfilerWindowUntaggedText:SetText("Show FrameXML")
|
||||
untagged:SetScript("OnClick", function(self)
|
||||
tab.untagged = self:GetChecked() and true or false
|
||||
Scan()
|
||||
end)
|
||||
ns.Tooltip(untagged, "Also list the values the game's own code made, and plain strings, numbers and booleans. With CPU profiling this shows the cost of every game API function.")
|
||||
|
||||
function tab:OnSelect()
|
||||
if not self.count then Scan() end
|
||||
end
|
||||
|
||||
function tab:Status()
|
||||
if not self.count then return "" end
|
||||
return format("%d globals listed %d ClassicAPI functions replaced or hooked", #self.list.shown, self.overrides)
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<!--
|
||||
A thin scroll bar for ClassicAPI's ScrollBox. ScrollBarMixin needs Back and
|
||||
Forward steppers, so they exist but stay hidden.
|
||||
-->
|
||||
<EventFrame name="ClassicAPIProfilerScrollBarTemplate" inherits="VerticalScrollBarTemplate" virtual="true">
|
||||
<Size x="7"/>
|
||||
<KeyValues>
|
||||
<KeyValue key="hideIfUnscrollable" value="true" type="boolean"/>
|
||||
</KeyValues>
|
||||
<Frames>
|
||||
<Frame parentKey="Track" setAllPoints="true" enableMouse="true">
|
||||
<Frames>
|
||||
<EventButton parentKey="Thumb">
|
||||
<Size x="7"/>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<Texture setAllPoints="true">
|
||||
<Color r=".2" g="1" b=".8" a=".5"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</EventButton>
|
||||
</Frames>
|
||||
</Frame>
|
||||
<EventButton parentKey="Back" hidden="true"/>
|
||||
<EventButton parentKey="Forward" hidden="true"/>
|
||||
</Frames>
|
||||
</EventFrame>
|
||||
|
||||
<!--
|
||||
A sortable list: a column header above a WowScrollBoxList, with the scroll
|
||||
bar on the right. Window.lua fills the header and the view.
|
||||
-->
|
||||
<Frame name="ClassicAPIProfilerListTemplate" virtual="true">
|
||||
<Frames>
|
||||
<Frame parentKey="Header">
|
||||
<Size y="18"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="5" y="-5"/>
|
||||
<Anchor point="TOPRIGHT" x="-15" y="-5"/>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
<EventFrame parentKey="ScrollBar" inherits="ClassicAPIProfilerScrollBarTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT" x="-5" y="-25"/>
|
||||
<Anchor point="BOTTOMRIGHT" x="-5" y="5"/>
|
||||
</Anchors>
|
||||
</EventFrame>
|
||||
<Frame parentKey="ScrollBox" inherits="WowScrollBoxList">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="5" y="-25"/>
|
||||
<Anchor point="BOTTOMRIGHT" relativeKey="$parent.ScrollBar" relativePoint="BOTTOMLEFT" x="-3"/>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
</Frames>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,212 @@
|
||||
-- Profiler track: per-frame and per-event statistics for the Frames and
|
||||
-- Events tabs. The analyzer runs Scan once a sample while it is open.
|
||||
--
|
||||
-- Deep tracking hooks the intrinsic precall and postcall of every OnUpdate
|
||||
-- and OnEvent script. The hooks never replace a handler, so GetScript and
|
||||
-- SetScript behave as before, and they stay until /reload. Around each
|
||||
-- script they measure the Lua memory it allocated and, with CPU profiling,
|
||||
-- the time of each OnEvent handler per event.
|
||||
local addonName, ns = ...
|
||||
|
||||
local PRECALL = LE_SCRIPT_BINDING_TYPE_INTRINSIC_PRECALL
|
||||
local POSTCALL = LE_SCRIPT_BINDING_TYPE_INTRINSIC_POSTCALL
|
||||
local Delta = ns.Delta
|
||||
|
||||
local track = {
|
||||
frames = {}, -- [frame] = record: frames that ran scripts, or are hooked
|
||||
events = {}, -- [event] = record: events fired while the analyzer is open
|
||||
frameCount = 0, -- frames in the last scan
|
||||
deep = false,
|
||||
}
|
||||
ns.track = track
|
||||
|
||||
-- the frame that counts every event while the analyzer is open
|
||||
local capture = CreateFrame("Frame")
|
||||
track.capture = capture
|
||||
|
||||
-- the addon that runs a frame's scripts: the owner of its handlers, or the
|
||||
-- frame's creator when the game's own code made the handlers
|
||||
local function OwnerOf(frame)
|
||||
local fn = frame:GetScript("OnUpdate") or frame:GetScript("OnEvent")
|
||||
return fn and ns.Owner(fn) or ns.Owner(frame)
|
||||
end
|
||||
|
||||
-- cpu and calls are the frame's totals now, so its first rate covers only
|
||||
-- the time after the record exists, not the whole session
|
||||
local function FrameRecord(frame, cpu, calls)
|
||||
local rec = {
|
||||
frame = frame, name = ns.FrameName(frame), owner = OwnerOf(frame),
|
||||
cpu = cpu or 0, calls = calls or 0, alloc = 0, events = {},
|
||||
}
|
||||
rec.search = strlower(rec.name .. " " .. ns.OwnerLabel(rec.owner))
|
||||
track.frames[frame] = rec
|
||||
return rec
|
||||
end
|
||||
|
||||
local function EventRecord(name)
|
||||
local rec = { name = name, search = strlower(name), fires = 0, dispatches = 0, cpu = 0, alloc = 0 }
|
||||
track.events[name] = rec
|
||||
return rec
|
||||
end
|
||||
|
||||
capture:SetScript("OnEvent", function(self, event)
|
||||
local rec = track.events[event] or EventRecord(event)
|
||||
rec.fires = rec.fires + 1
|
||||
end)
|
||||
|
||||
-- [[ deep tracking ]]
|
||||
-- a stack, because a script can run inside another script
|
||||
local depth = 0
|
||||
local memAt, gcAt, fnAt, cpuAt, eventAt = {}, {}, {}, {}, {}
|
||||
|
||||
local function Enter(fn, event)
|
||||
depth = depth + 1
|
||||
memAt[depth], gcAt[depth] = gcinfo()
|
||||
fnAt[depth], eventAt[depth] = fn, event
|
||||
if fn and ns.profiling then cpuAt[depth] = GetFunctionCPUUsage(fn) end
|
||||
end
|
||||
|
||||
-- the KB allocated since the matching Enter (nil when a garbage collection
|
||||
-- ran in between), the handler's CPU time in ms, and the event
|
||||
local function Leave()
|
||||
local d = depth
|
||||
if d == 0 then return end
|
||||
depth = d - 1
|
||||
|
||||
local mem, threshold = gcinfo()
|
||||
local alloc = threshold == gcAt[d] and max(mem - memAt[d], 0) or nil
|
||||
local fn, cpu = fnAt[d], nil
|
||||
if fn and ns.profiling then cpu = max(GetFunctionCPUUsage(fn) - cpuAt[d], 0) end
|
||||
fnAt[d] = nil
|
||||
return alloc, cpu, eventAt[d]
|
||||
end
|
||||
|
||||
local function EnterUpdate() Enter() end
|
||||
|
||||
local function LeaveEvent(rec)
|
||||
local alloc, cpu, name = Leave()
|
||||
if not name then return end
|
||||
|
||||
local ev = track.events[name] or EventRecord(name)
|
||||
local fe = rec.events[name]
|
||||
if not fe then
|
||||
fe = { count = 0, cpu = 0, alloc = 0 }
|
||||
rec.events[name] = fe
|
||||
end
|
||||
|
||||
fe.count = fe.count + 1
|
||||
ev.dispatches = ev.dispatches + 1
|
||||
if alloc then
|
||||
rec.alloc, fe.alloc, ev.alloc = rec.alloc + alloc, fe.alloc + alloc, ev.alloc + alloc
|
||||
end
|
||||
if cpu then
|
||||
fe.cpu, ev.cpu = fe.cpu + cpu, ev.cpu + cpu
|
||||
end
|
||||
end
|
||||
|
||||
local function Hook(frame, rec)
|
||||
if not rec.hookedUpdate and frame:GetScript("OnUpdate") then
|
||||
rec.hookedUpdate = true
|
||||
frame:HookScript("OnUpdate", EnterUpdate, PRECALL)
|
||||
frame:HookScript("OnUpdate", function()
|
||||
local alloc = Leave()
|
||||
if alloc then rec.alloc = rec.alloc + alloc end
|
||||
end, POSTCALL)
|
||||
end
|
||||
|
||||
if not rec.hookedEvent and frame:GetScript("OnEvent") then
|
||||
rec.hookedEvent = true
|
||||
-- the engine sets the `event` global before it runs the script
|
||||
frame:HookScript("OnEvent", function() Enter(frame:GetScript("OnEvent"), event) end, PRECALL)
|
||||
frame:HookScript("OnEvent", function() LeaveEvent(rec) end, POSTCALL)
|
||||
end
|
||||
end
|
||||
|
||||
function track.EnableDeep()
|
||||
track.deep = true
|
||||
end
|
||||
|
||||
-- [[ sampling ]]
|
||||
-- the frames registered for an event, without the capture frame
|
||||
local function CountListeners(...)
|
||||
local n = select("#", ...)
|
||||
for i = 1, n do
|
||||
if select(i, ...) == capture then return n - 1 end
|
||||
end
|
||||
return n
|
||||
end
|
||||
track.CountListeners = CountListeners
|
||||
|
||||
function track.Scan(dt)
|
||||
local profiling, deep = ns.profiling, track.deep
|
||||
local count = 0
|
||||
|
||||
local frame = EnumerateFrames()
|
||||
while frame do
|
||||
count = count + 1
|
||||
local rec = track.frames[frame]
|
||||
|
||||
if profiling then
|
||||
local cpu, calls = GetFrameCPUUsage(frame, false)
|
||||
if calls > 0 and not rec then rec = FrameRecord(frame, cpu, calls) end
|
||||
if rec then
|
||||
rec.cpuRate = Delta(cpu, rec.cpu) / dt
|
||||
rec.callRate = Delta(calls, rec.calls) / dt
|
||||
rec.cpu, rec.calls = cpu, calls
|
||||
end
|
||||
end
|
||||
|
||||
if deep and not (rec and rec.hookedUpdate and rec.hookedEvent) then
|
||||
if frame:GetScript("OnUpdate") or frame:GetScript("OnEvent") then
|
||||
rec = rec or FrameRecord(frame)
|
||||
-- the profiler does not measure its own addon
|
||||
if rec.owner ~= addonName then Hook(frame, rec) end
|
||||
end
|
||||
end
|
||||
|
||||
frame = EnumerateFrames(frame)
|
||||
end
|
||||
track.frameCount = count
|
||||
|
||||
for _, rec in pairs(track.frames) do
|
||||
rec.allocRate = Delta(rec.alloc, rec.lastAlloc) / dt
|
||||
rec.lastAlloc = rec.alloc
|
||||
end
|
||||
|
||||
for name, ev in pairs(track.events) do
|
||||
ev.fireRate = Delta(ev.fires, ev.lastFires) / dt
|
||||
ev.cpuRate = Delta(ev.cpu, ev.lastCpu) / dt
|
||||
ev.allocRate = Delta(ev.alloc, ev.lastAlloc) / dt
|
||||
ev.lastFires, ev.lastCpu, ev.lastAlloc = ev.fires, ev.cpu, ev.alloc
|
||||
ev.listeners = CountListeners(GetFramesRegisteredForEvent(name))
|
||||
end
|
||||
end
|
||||
|
||||
-- the frame totals and the deep hooks keep counting while the analyzer is
|
||||
-- closed, so an open takes the counters as they are now, and the first scan
|
||||
-- covers only its own second
|
||||
function track.Start()
|
||||
for frame, rec in pairs(track.frames) do
|
||||
if ns.profiling then rec.cpu, rec.calls = GetFrameCPUUsage(frame, false) end
|
||||
rec.lastAlloc = rec.alloc
|
||||
end
|
||||
for _, ev in pairs(track.events) do
|
||||
ev.lastCpu, ev.lastAlloc = ev.cpu, ev.alloc
|
||||
end
|
||||
capture:RegisterAllEvents()
|
||||
end
|
||||
|
||||
function track.Stop()
|
||||
capture:UnregisterAllEvents()
|
||||
end
|
||||
|
||||
ns.OnReset(function()
|
||||
for _, rec in pairs(track.frames) do
|
||||
rec.cpu, rec.calls, rec.alloc, rec.lastAlloc = 0, 0, 0, 0
|
||||
table.wipe(rec.events)
|
||||
end
|
||||
for _, ev in pairs(track.events) do
|
||||
ev.fires, ev.dispatches, ev.cpu, ev.alloc = 0, 0, 0, 0
|
||||
ev.lastFires, ev.lastCpu, ev.lastAlloc = 0, 0, 0
|
||||
end
|
||||
end)
|
||||
@@ -0,0 +1,180 @@
|
||||
-- Profiler widget: Lua memory, garbage collection, frame rate and the addons
|
||||
-- that make the most garbage and use the most CPU.
|
||||
local _, ns = ...
|
||||
|
||||
local LINE_HEIGHT = 14
|
||||
local stats, gc = ns.stats, ns.gc
|
||||
|
||||
local widget = CreateFrame("Button", "ClassicAPIProfilerWidget", UIParent)
|
||||
ns.widget = widget
|
||||
widget:Hide()
|
||||
widget:SetPoint("CENTER", 0, 0)
|
||||
widget:SetWidth(240)
|
||||
widget:SetMovable(true)
|
||||
widget:SetClampedToScreen(true)
|
||||
widget:EnableMouse(true)
|
||||
widget:RegisterForDrag("LeftButton")
|
||||
widget:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
ns.CreateBackdrop(widget)
|
||||
|
||||
local lines = {}
|
||||
local function AddLine(label)
|
||||
local y = -4 - #lines * LINE_HEIGHT
|
||||
|
||||
local left = widget:CreateFontString(nil, "LOW", "GameFontWhite")
|
||||
left:SetPoint("TOPLEFT", 5, y)
|
||||
left:SetText(ns.ACCENT .. label)
|
||||
|
||||
local right = widget:CreateFontString(nil, "LOW", "GameFontWhite")
|
||||
right:SetPoint("TOPRIGHT", -5, y)
|
||||
right:SetJustifyH("RIGHT")
|
||||
|
||||
tinsert(lines, right)
|
||||
return right
|
||||
end
|
||||
|
||||
local memory = AddLine("Memory")
|
||||
local rate = AddLine("Rate")
|
||||
local cleanup = AddLine("Last Cleanup")
|
||||
local framerate = AddLine("Framerate")
|
||||
local cpu = AddLine("Lua CPU")
|
||||
local topMem = AddLine("Top Alloc")
|
||||
local topCpu = AddLine("Top CPU")
|
||||
|
||||
-- the largest allocation in a tenth of a second, draining over 5 seconds
|
||||
local DRAIN = .1 / 5
|
||||
local bar = CreateFrame("StatusBar", nil, widget)
|
||||
bar:SetColorFill(1, .3, .3)
|
||||
bar:SetPoint("BOTTOMLEFT", 1, 1)
|
||||
bar:SetPoint("BOTTOMRIGHT", -1, 1)
|
||||
bar:SetHeight(18)
|
||||
bar:SetMinMaxValues(0, 1)
|
||||
bar:SetValue(0)
|
||||
|
||||
local barText = bar:CreateFontString(nil, "OVERLAY", "GameFontWhite")
|
||||
barText:SetPoint("LEFT", 4, 0)
|
||||
|
||||
widget:SetHeight(#lines * LINE_HEIGHT + 8 + 20)
|
||||
|
||||
-- lastMem is taken on the first tick while shown, so the growth while hidden
|
||||
-- or while the other addons load never counts as a peak
|
||||
local lastMem, peakElapsed = nil, 0
|
||||
widget:SetScript("OnShow", function() ns.StartSampling() end)
|
||||
widget:SetScript("OnHide", function() lastMem = nil end)
|
||||
widget:SetScript("OnUpdate", function(self, elapsed)
|
||||
peakElapsed = peakElapsed + elapsed
|
||||
if peakElapsed < .1 then return end
|
||||
peakElapsed = 0
|
||||
|
||||
local mem = gcinfo()
|
||||
local grown = lastMem and mem - lastMem or 0
|
||||
lastMem = mem
|
||||
|
||||
local value, peak = bar:GetValue(), select(2, bar:GetMinMaxValues())
|
||||
if grown > value then
|
||||
bar:SetMinMaxValues(0, grown)
|
||||
bar:SetValue(grown)
|
||||
barText:SetText(ns.ACCENT .. "Last Peak (100ms):|r " .. grown .. ns.GREY .. " KB")
|
||||
else
|
||||
bar:SetValue(max(value - peak * DRAIN, 0))
|
||||
end
|
||||
bar:SetColorFill(1, .3, .3, min(bar:GetValue() / 10, 1))
|
||||
end)
|
||||
|
||||
local function Unit(value, unit)
|
||||
return ns.WHITE .. value .. " " .. ns.GREY .. unit
|
||||
end
|
||||
|
||||
ns.OnSample(function()
|
||||
if not widget:IsShown() then return end
|
||||
|
||||
memory:SetText(ns.WHITE .. ns.KB(stats.mem) .. ns.GREY .. " / " .. ns.WHITE .. ns.KB(stats.threshold))
|
||||
|
||||
if stats.collected then
|
||||
rate:SetText(ns.ACCENT .. "collected " .. ns.KB(gc.freed or 0))
|
||||
elseif stats.memRate then
|
||||
rate:SetText(ns.WHITE .. ns.KBRate(stats.memRate))
|
||||
end
|
||||
|
||||
if gc.time then
|
||||
cleanup:SetText(ns.WHITE .. gc.clock .. ns.GREY .. " (" .. SecondsToTime(GetTime() - gc.time) .. " ago)")
|
||||
else
|
||||
cleanup:SetText(ns.GREY .. "none yet")
|
||||
end
|
||||
|
||||
local fps = GetFramerate()
|
||||
framerate:SetText(Unit(format("%.0f", fps), "fps") .. ns.GREY .. " (" .. ns.WHITE .. format("%.1f", 1000 / fps) .. ns.GREY .. " ms)")
|
||||
|
||||
if ns.profiling then
|
||||
cpu:SetText(ns.WHITE .. ns.MS(stats.scriptRate) .. ns.GREY .. "/s (" .. ns.WHITE .. format("%.1f%%", stats.scriptRate / 10) .. ns.GREY .. ")")
|
||||
topCpu:SetText(stats.topCpu and ns.WHITE .. stats.topCpu.name .. " " .. ns.GREY .. ns.MS(stats.topCpu.cpuRate) .. "/s" or "")
|
||||
else
|
||||
cpu:SetText(ns.GREY .. "profiling off")
|
||||
topCpu:SetText(ns.GREY .. "-")
|
||||
end
|
||||
|
||||
local top = stats.topMem
|
||||
if top and top.memRate > 0 then
|
||||
topMem:SetText(ns.WHITE .. top.name .. " " .. ns.GREY .. ns.KBRate(top.memRate))
|
||||
else
|
||||
topMem:SetText(ns.GREY .. "-")
|
||||
end
|
||||
|
||||
if GameTooltip:IsOwned(widget) then widget:GetScript("OnEnter")(widget) end
|
||||
end)
|
||||
|
||||
ns.OnReset(function()
|
||||
bar:SetValue(0)
|
||||
end)
|
||||
|
||||
-- the top `count` loaded addons by `key`, largest first
|
||||
local function Top(key, count)
|
||||
local sorted = {}
|
||||
for _, addon in ipairs(stats.list) do
|
||||
if (addon[key] or 0) > 0 then tinsert(sorted, addon) end
|
||||
end
|
||||
table.sort(sorted, function(a, b) return a[key] > b[key] end)
|
||||
for i = count + 1, #sorted do sorted[i] = nil end
|
||||
return sorted
|
||||
end
|
||||
|
||||
widget:SetScript("OnEnter", function(self)
|
||||
GameTooltip:SetOwner(self, "ANCHOR_BOTTOMLEFT")
|
||||
GameTooltip:ClearLines()
|
||||
GameTooltip:AddLine(ns.ACCENT .. "Profiler")
|
||||
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("Allocated per second", 1, .8, 0)
|
||||
for _, addon in ipairs(Top("memRate", 8)) do
|
||||
GameTooltip:AddDoubleLine(addon.name, ns.KBRate(addon.memRate), 1, 1, 1, 1, 1, 1)
|
||||
end
|
||||
|
||||
if ns.profiling then
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("CPU per second", 1, .8, 0)
|
||||
for _, addon in ipairs(Top("cpuRate", 8)) do
|
||||
GameTooltip:AddDoubleLine(addon.name, ns.MS(addon.cpuRate), 1, 1, 1, 1, 1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
GameTooltip:AddLine(" ")
|
||||
GameTooltip:AddLine("Left-click: reset the peak", .6, .6, .6)
|
||||
GameTooltip:AddLine("Right-click: open the analyzer", .6, .6, .6)
|
||||
GameTooltip:AddLine("Drag: move", .6, .6, .6)
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
|
||||
widget:SetScript("OnLeave", function()
|
||||
GameTooltip:Hide()
|
||||
end)
|
||||
|
||||
widget:SetScript("OnClick", function(self, button)
|
||||
if button == "RightButton" then
|
||||
ns.window:SetShown(not ns.window:IsShown())
|
||||
else
|
||||
bar:SetValue(0)
|
||||
end
|
||||
end)
|
||||
|
||||
widget:SetScript("OnDragStart", function(self) self:StartMoving() end)
|
||||
widget:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end)
|
||||
@@ -0,0 +1,392 @@
|
||||
-- Profiler window: the analyzer. Its tabs share a toolbar (search, deep
|
||||
-- tracking, reset, copy) and the sortable list below.
|
||||
local _, ns = ...
|
||||
|
||||
local ROW_HEIGHT = 18
|
||||
local WHITE8X8 = "Interface\\BUTTONS\\WHITE8X8"
|
||||
|
||||
ns.search = ""
|
||||
|
||||
local window = CreateFrame("Frame", "ClassicAPIProfilerWindow", UIParent)
|
||||
ns.window = window
|
||||
window:Hide()
|
||||
window:SetSize(760, 480)
|
||||
window:SetPoint("CENTER")
|
||||
window:SetFrameStrata("DIALOG")
|
||||
window:SetMovable(true)
|
||||
window:EnableMouse(true)
|
||||
window:SetClampedToScreen(true)
|
||||
window:RegisterForDrag("LeftButton")
|
||||
window:SetScript("OnDragStart", window.StartMoving)
|
||||
window:SetScript("OnDragStop", window.StopMovingOrSizing)
|
||||
ns.CreateBackdrop(window)
|
||||
tinsert(UISpecialFrames, "ClassicAPIProfilerWindow")
|
||||
|
||||
local title = window:CreateFontString(nil, "OVERLAY", "GameFontWhite")
|
||||
title:SetFont(STANDARD_TEXT_FONT, 14)
|
||||
title:SetPoint("TOPLEFT", 10, -10)
|
||||
title:SetText(ns.ACCENT .. "Profiler|r: " .. ns.GOLD .. "Analyzer")
|
||||
|
||||
local close = CreateFrame("Button", "ClassicAPIProfilerWindowClose", window, "UIPanelCloseButton")
|
||||
close:SetSize(20, 20)
|
||||
close:SetPoint("TOPRIGHT", 0, 0)
|
||||
close:SetScript("OnClick", function() window:Hide() end)
|
||||
|
||||
local status = window:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
||||
status:SetPoint("BOTTOMLEFT", 10, 8)
|
||||
status:SetPoint("BOTTOMRIGHT", -10, 8)
|
||||
status:SetJustifyH("LEFT")
|
||||
|
||||
-- [[ widgets ]]
|
||||
local function Tooltip(frame, text)
|
||||
frame:SetScript("OnEnter", function(self)
|
||||
GameTooltip:SetOwner(self, "ANCHOR_BOTTOMLEFT")
|
||||
GameTooltip:SetText(type(text) == "function" and text() or text, 1, 1, 1, 1, true)
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
frame:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
end
|
||||
ns.Tooltip = Tooltip
|
||||
|
||||
function ns.CreateButton(name, parent, width, text, onClick)
|
||||
local button = CreateFrame("Button", name, parent, "UIPanelButtonTemplate")
|
||||
button:SetSize(width, 20)
|
||||
button:SetText(text)
|
||||
button:SetScript("OnClick", onClick)
|
||||
return button
|
||||
end
|
||||
|
||||
-- [[ actions the tabs offer in their context menus ]]
|
||||
function ns.Copy(text)
|
||||
local bytes = CopyToClipboard(text, true)
|
||||
ns.Print("Copied " .. bytes .. " bytes to the clipboard.")
|
||||
end
|
||||
|
||||
function ns.Inspect(value)
|
||||
DisplayTableInspectorWindow(value)
|
||||
end
|
||||
|
||||
local highlight = CreateFrame("Frame", nil, UIParent)
|
||||
highlight:SetFrameStrata("TOOLTIP")
|
||||
highlight:SetBackdrop({ bgFile = WHITE8X8, edgeFile = WHITE8X8, edgeSize = 2 })
|
||||
highlight:SetBackdropColor(.2, 1, .8, .15)
|
||||
highlight:SetBackdropBorderColor(.2, 1, .8, 1)
|
||||
highlight:Hide()
|
||||
|
||||
function ns.Highlight(frame)
|
||||
if not frame:IsVisible() then
|
||||
ns.Print(ns.FrameName(frame) .. " is hidden.")
|
||||
return
|
||||
end
|
||||
highlight:ClearAllPoints()
|
||||
highlight:SetAllPoints(frame)
|
||||
highlight:Show()
|
||||
if highlight.timer then highlight.timer:Cancel() end
|
||||
highlight.timer = C_Timer.NewTimer(3, function() highlight:Hide() end)
|
||||
end
|
||||
|
||||
-- [[ list: a sortable, searchable ScrollBox ]]
|
||||
-- columns = { { title, width, justify, tip, text = fn(entry), value = fn(entry) } }
|
||||
-- The first column takes the space the others leave. `value` sorts the
|
||||
-- column and sizes the row bars; it is a number, a lowercase string or nil.
|
||||
local function LayoutColumns(parent, cells, columns)
|
||||
local right = -4
|
||||
for i = #columns, 2, -1 do
|
||||
cells[i]:SetPoint("RIGHT", parent, "RIGHT", right, 0)
|
||||
cells[i]:SetWidth(columns[i].width)
|
||||
right = right - columns[i].width - 8
|
||||
end
|
||||
cells[1]:SetPoint("LEFT", parent, "LEFT", 4, 0)
|
||||
cells[1]:SetPoint("RIGHT", parent, "RIGHT", right, 0)
|
||||
end
|
||||
|
||||
-- green to yellow to red, as `perc` goes from 0 to 1
|
||||
local function Gradient(perc)
|
||||
if perc <= .5 then return perc * 2, 1, 0 end
|
||||
return 1, 2 - perc * 2, 0
|
||||
end
|
||||
|
||||
local function RowOnEnter(row)
|
||||
local tooltip = row.list.def.tooltip
|
||||
if not tooltip then return end
|
||||
GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
|
||||
GameTooltip:ClearLines()
|
||||
tooltip(row.entry)
|
||||
GameTooltip:Show()
|
||||
end
|
||||
|
||||
local function RowOnLeave()
|
||||
GameTooltip:Hide()
|
||||
end
|
||||
|
||||
local function RowOnClick(row, button)
|
||||
local def = row.list.def
|
||||
if button == "RightButton" and def.menu then
|
||||
MenuUtil.CreateContextMenu(row, function(owner, root)
|
||||
def.menu(row.entry, root)
|
||||
end)
|
||||
elseif button == "LeftButton" and def.click then
|
||||
def.click(row.entry)
|
||||
end
|
||||
end
|
||||
|
||||
local function InitRow(list, row, entry)
|
||||
local columns = list.def.columns
|
||||
|
||||
if not row.cells then
|
||||
row:EnableMouse(true)
|
||||
row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
row:SetHighlightTexture(WHITE8X8)
|
||||
row:GetHighlightTexture():SetVertexColor(1, 1, 1, .1)
|
||||
row:SetScript("OnEnter", RowOnEnter)
|
||||
row:SetScript("OnLeave", RowOnLeave)
|
||||
row:SetScript("OnClick", RowOnClick)
|
||||
|
||||
row.bar = CreateFrame("StatusBar", nil, row)
|
||||
row.bar:SetAllPoints()
|
||||
row.bar:SetMinMaxValues(0, 1)
|
||||
|
||||
row.cells = {}
|
||||
for i, column in ipairs(columns) do
|
||||
local cell = row.bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
||||
cell:SetHeight(ROW_HEIGHT)
|
||||
cell:SetWordWrap(false)
|
||||
cell:SetJustifyH(column.justify or (i == 1 and "LEFT" or "RIGHT"))
|
||||
row.cells[i] = cell
|
||||
end
|
||||
LayoutColumns(row, row.cells, columns)
|
||||
end
|
||||
|
||||
row.list, row.entry = list, entry
|
||||
for i, column in ipairs(columns) do
|
||||
row.cells[i]:SetText(column.text(entry) or "")
|
||||
end
|
||||
|
||||
local value = columns[list.sortColumn].value
|
||||
local v = value and value(entry)
|
||||
local perc = type(v) == "number" and list.maxValue > 0 and max(v, 0) / list.maxValue or 0
|
||||
local r, g, b = Gradient(perc)
|
||||
row.bar:SetColorFill(r, g, b, .2)
|
||||
row.bar:SetValue(perc)
|
||||
|
||||
if GameTooltip:IsOwned(row) then RowOnEnter(row) end
|
||||
end
|
||||
|
||||
local ListMixin = {}
|
||||
|
||||
function ListMixin:UpdateHeader()
|
||||
for i, column in ipairs(self.def.columns) do
|
||||
local text = column.title
|
||||
if i == self.sortColumn then
|
||||
text = ns.ACCENT .. text .. (self.sortDesc and " v" or " ^")
|
||||
end
|
||||
self.headers[i].text:SetText(text)
|
||||
end
|
||||
end
|
||||
|
||||
function ListMixin:SortBy(index)
|
||||
if self.sortColumn == index then
|
||||
self.sortDesc = not self.sortDesc
|
||||
else
|
||||
self.sortColumn, self.sortDesc = index, true
|
||||
end
|
||||
self:UpdateHeader()
|
||||
self:Refresh()
|
||||
end
|
||||
|
||||
function ListMixin:SetEntries(entries)
|
||||
self.entries = entries
|
||||
self:Refresh()
|
||||
end
|
||||
|
||||
-- filter by the search text, sort, and show
|
||||
function ListMixin:Refresh()
|
||||
local value = self.def.columns[self.sortColumn].value
|
||||
local needle, desc = ns.search, self.sortDesc
|
||||
local shown, keys, maxValue = {}, {}, 0
|
||||
|
||||
for _, entry in ipairs(self.entries) do
|
||||
if needle == "" or strfind(entry.search, needle, 1, true) then
|
||||
tinsert(shown, entry)
|
||||
local v = value and value(entry)
|
||||
keys[entry] = v
|
||||
if type(v) == "number" and v > maxValue then maxValue = v end
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(shown, function(a, b)
|
||||
local va, vb = keys[a], keys[b]
|
||||
if va == vb then return a.search < b.search end
|
||||
if va == nil then return false end
|
||||
if vb == nil then return true end
|
||||
if desc then return va > vb end
|
||||
return va < vb
|
||||
end)
|
||||
|
||||
self.shown, self.maxValue = shown, maxValue
|
||||
self.ScrollBox:SetDataProvider(CreateDataProvider(shown), ScrollBoxConstants.RetainScrollPosition)
|
||||
end
|
||||
|
||||
-- the shown rows as plain text
|
||||
function ListMixin:Export()
|
||||
local columns = self.def.columns
|
||||
local function Line(texts)
|
||||
local parts = {}
|
||||
for i, text in ipairs(texts) do
|
||||
text = gsub(gsub(text, "|c%x%x%x%x%x%x%x%x", ""), "|r", "")
|
||||
parts[i] = format(i == 1 and "%-48s" or "%14s", text)
|
||||
end
|
||||
return table.concat(parts, " ")
|
||||
end
|
||||
|
||||
local titles = {}
|
||||
for i, column in ipairs(columns) do titles[i] = column.title end
|
||||
local lines = { Line(titles) }
|
||||
|
||||
for _, entry in ipairs(self.shown) do
|
||||
local texts = {}
|
||||
for i, column in ipairs(columns) do texts[i] = column.text(entry) or "" end
|
||||
tinsert(lines, Line(texts))
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
function ns.CreateList(parent, def)
|
||||
local list = CreateFrame("Frame", nil, parent, "ClassicAPIProfilerListTemplate")
|
||||
Mixin(list, ListMixin)
|
||||
list.def = def
|
||||
list.entries, list.shown, list.maxValue = {}, {}, 0
|
||||
list.sortColumn, list.sortDesc = def.sort or 1, true
|
||||
ns.CreateBackdrop(list)
|
||||
|
||||
list.headers = {}
|
||||
for i, column in ipairs(def.columns) do
|
||||
local header = CreateFrame("Button", nil, list.Header)
|
||||
header:SetHeight(18)
|
||||
header:EnableMouse(true)
|
||||
header.text = header:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
||||
header.text:SetAllPoints()
|
||||
header.text:SetJustifyH(column.justify or (i == 1 and "LEFT" or "RIGHT"))
|
||||
header:SetScript("OnClick", function() list:SortBy(i) end)
|
||||
if column.tip then Tooltip(header, column.tip) end
|
||||
list.headers[i] = header
|
||||
end
|
||||
LayoutColumns(list.Header, list.headers, def.columns)
|
||||
list:UpdateHeader()
|
||||
|
||||
local view = CreateScrollBoxListLinearView(0, 0, 0, 0, 1)
|
||||
view:SetElementExtent(ROW_HEIGHT)
|
||||
view:SetElementInitializer("Button", function(row, entry) InitRow(list, row, entry) end)
|
||||
ScrollUtil.InitScrollBoxListWithScrollBar(list.ScrollBox, list.ScrollBar, view)
|
||||
return list
|
||||
end
|
||||
|
||||
-- [[ tabs ]]
|
||||
-- a tab has `frame`, `list`, and optionally OnSelect(), OnSample(dt) and
|
||||
-- Status() for the line at the bottom
|
||||
local tabs = {}
|
||||
|
||||
local function UpdateStatus()
|
||||
local tab = window.active
|
||||
status:SetText(tab and tab.Status and tab:Status() or "")
|
||||
end
|
||||
|
||||
function ns.SelectTab(tab)
|
||||
for _, other in ipairs(tabs) do
|
||||
other.frame:SetShown(other == tab)
|
||||
if other == tab then other.button:LockHighlight() else other.button:UnlockHighlight() end
|
||||
end
|
||||
window.active = tab
|
||||
if tab.OnSelect then tab:OnSelect() end
|
||||
UpdateStatus()
|
||||
end
|
||||
|
||||
function ns.AddTab(name)
|
||||
local tab = { name = name }
|
||||
local index = #tabs + 1
|
||||
|
||||
tab.frame = CreateFrame("Frame", nil, window)
|
||||
tab.frame:SetPoint("TOPLEFT", 6, -84)
|
||||
tab.frame:SetPoint("BOTTOMRIGHT", -6, 24)
|
||||
tab.frame:Hide()
|
||||
|
||||
tab.button = ns.CreateButton("ClassicAPIProfilerWindowTab" .. index, window, 90, name, function() ns.SelectTab(tab) end)
|
||||
tab.button:SetPoint("TOPLEFT", 8 + (index - 1) * 94, -32)
|
||||
|
||||
tabs[index] = tab
|
||||
tabs[name] = tab
|
||||
return tab
|
||||
end
|
||||
|
||||
-- show `text` in the search box and switch to tab `name`
|
||||
function ns.ShowIn(name, text)
|
||||
window.search:SetText(text)
|
||||
ns.SelectTab(tabs[name])
|
||||
end
|
||||
|
||||
-- [[ toolbar ]]
|
||||
local search = CreateFrame("EditBox", "ClassicAPIProfilerWindowSearch", window, "InputBoxTemplate")
|
||||
window.search = search
|
||||
search:SetSize(200, 20)
|
||||
search:SetPoint("TOPLEFT", 14, -58)
|
||||
search:SetAutoFocus(false)
|
||||
search:SetScript("OnTextChanged", function(self)
|
||||
ns.search = strlower(self:GetText())
|
||||
self.placeholder:SetShown(self:GetText() == "")
|
||||
if window.active then window.active.list:Refresh() end
|
||||
end)
|
||||
|
||||
search.placeholder = search:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
|
||||
search.placeholder:SetPoint("LEFT", 2, 0)
|
||||
search.placeholder:SetText("Search name, owner or source")
|
||||
|
||||
local copy = ns.CreateButton("ClassicAPIProfilerWindowCopy", window, 70, "Copy", function()
|
||||
ns.Copy(window.active.list:Export())
|
||||
end)
|
||||
copy:SetPoint("TOPRIGHT", -8, -58)
|
||||
Tooltip(copy, "Copy the shown rows to the clipboard as plain text.")
|
||||
|
||||
local reset = ns.CreateButton("ClassicAPIProfilerWindowReset", window, 70, "Reset", function()
|
||||
ns.Reset()
|
||||
UpdateStatus()
|
||||
end)
|
||||
reset:SetPoint("RIGHT", copy, "LEFT", -4, 0)
|
||||
Tooltip(reset, "Set every CPU time, call count, allocation and event count to zero.")
|
||||
|
||||
local deep = ns.CreateButton("ClassicAPIProfilerWindowDeep", window, 150, "", function(self)
|
||||
ns.track.EnableDeep()
|
||||
self:SetText("Deep Tracking: " .. ns.ACCENT .. "ON")
|
||||
self:Disable()
|
||||
end)
|
||||
deep:SetPoint("RIGHT", reset, "LEFT", -4, 0)
|
||||
deep:SetText("Deep Tracking: " .. ns.GREY .. "OFF")
|
||||
Tooltip(deep, "Hooks the OnUpdate and OnEvent scripts of every frame to measure the Lua memory each frame allocates, and the CPU time and memory of each event. The hooks never replace a handler, but they add a little time to every script call. They stay until /reload.")
|
||||
|
||||
local profiling = ns.CreateButton("ClassicAPIProfilerWindowProfiling", window, 150, "", function()
|
||||
ns.SetProfiling(not ns.profiling)
|
||||
end)
|
||||
profiling:SetPoint("TOPRIGHT", -24, -32)
|
||||
profiling:SetText("CPU Profiling: " .. (ns.profiling and ns.ACCENT .. "ON" or ns.GREY .. "OFF"))
|
||||
Tooltip(profiling, function()
|
||||
return (ns.profiling and "Turn CPU profiling off" or "Turn CPU profiling on") ..
|
||||
" and reload the UI.\n\nProfiling measures the CPU time of every addon, frame, function and event. It makes each Lua call slower, so turn it off when you are done."
|
||||
end)
|
||||
|
||||
-- [[ sampling while open ]]
|
||||
ns.OnSample(function(dt)
|
||||
if not window:IsShown() then return end
|
||||
ns.track.Scan(dt)
|
||||
local tab = window.active
|
||||
if tab.OnSample then tab:OnSample(dt) end
|
||||
UpdateStatus()
|
||||
end)
|
||||
|
||||
window:SetScript("OnShow", function()
|
||||
ns.StartSampling()
|
||||
ns.track.Start()
|
||||
ns.SelectTab(window.active or tabs[1])
|
||||
end)
|
||||
|
||||
window:SetScript("OnHide", function()
|
||||
ns.track.Stop()
|
||||
end)
|
||||
@@ -0,0 +1,8 @@
|
||||
## Interface: 11200
|
||||
## Title: ClassicAPI Event Trace
|
||||
## Notes: Event log for addon development (backport of retail's Blizzard_EventTrace to 1.12.1)
|
||||
## Author: Blizzard Entertainment, backported
|
||||
## Version: DEV
|
||||
## LoadOnDemand: 1
|
||||
## SavedVariables: EventTraceSavedVars
|
||||
EventTrace.xml
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,413 @@
|
||||
<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">
|
||||
<!--
|
||||
Backport of retail's Blizzard_EventTrace/Blizzard_EventTrace.xml.
|
||||
- Textures without <Anchors> get setAllPoints="true": retail's loader
|
||||
fills such a texture to its parent, this client's does not.
|
||||
- The menu buttons' auctionhouse-nav-button atlas is art this client does
|
||||
not have. They are drawn like the Options dropdown beside them: the
|
||||
panel button art in three slices, its highlight on mouseover.
|
||||
- The title bar is a Frame, which this client does not make mouse-enabled
|
||||
for its drag scripts, so it has enableMouse.
|
||||
- The scroll bars are this client's bar, not retail's thin one. Its frame
|
||||
is 25 wide but its art 31, drawn from the frame's left. Each scroll box
|
||||
leaves 32 at its right and the bar sits 4 to its right, so the art ends
|
||||
3 past the log, at the Inset's edge (6 from the window's, the log's 9).
|
||||
- The search box inherits ClassicAPISearchBoxTemplate, retail's
|
||||
SearchBoxTemplate under the name that is ClassicAPI's on every client
|
||||
(Turtle's FrameXML has a SearchBoxTemplate of its own).
|
||||
-->
|
||||
<Script file="EventTrace.lua"/>
|
||||
|
||||
<CheckButton name="EventTraceCheckButtonTemplate" virtual="true">
|
||||
<Size x="15" y="15"/>
|
||||
<HitRectInsets>
|
||||
<AbsInset left="0" right="-100" top="0" bottom="0"/>
|
||||
</HitRectInsets>
|
||||
<NormalTexture file="Interface\Buttons\UI-CheckBox-Up"/>
|
||||
<PushedTexture file="Interface\Buttons\UI-CheckBox-Down"/>
|
||||
<HighlightTexture file="Interface\Buttons\UI-CheckBox-Highlight" alphaMode="ADD"/>
|
||||
<CheckedTexture file="Interface\Buttons\UI-CheckBox-Check"/>
|
||||
<DisabledCheckedTexture file="Interface\Buttons\UI-CheckBox-Check-Disabled"/>
|
||||
</CheckButton>
|
||||
|
||||
<Frame name="EventTraceButtonBehaviorTemplate" mixin="EventTraceButtonBehaviorMixin" virtual="true">
|
||||
<Scripts>
|
||||
<OnEnter method="OnEnter"/>
|
||||
<OnLeave method="OnLeave"/>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
<Button name="EventTraceMenuButtonTemplate" inherits="EventTraceButtonBehaviorTemplate" virtual="true">
|
||||
<Size x="132" y="20"/>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture parentKey="Left" file="Interface\Buttons\UI-Panel-Button-Up">
|
||||
<Size x="12" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT"/>
|
||||
</Anchors>
|
||||
<TexCoords left="0" right="0.09375" top="0" bottom="0.6875"/>
|
||||
</Texture>
|
||||
<Texture parentKey="Right" file="Interface\Buttons\UI-Panel-Button-Up">
|
||||
<Size x="12" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT"/>
|
||||
</Anchors>
|
||||
<TexCoords left="0.53125" right="0.625" top="0" bottom="0.6875"/>
|
||||
</Texture>
|
||||
<Texture parentKey="Middle" file="Interface\Buttons\UI-Panel-Button-Up">
|
||||
<Size x="12" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeKey="$parent.Left" relativePoint="RIGHT"/>
|
||||
<Anchor point="RIGHT" relativeKey="$parent.Right" relativePoint="LEFT"/>
|
||||
</Anchors>
|
||||
<TexCoords left="0.09375" right="0.53125" top="0" bottom="0.6875"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture parentKey="MouseoverOverlay" file="Interface\Buttons\UI-Panel-Button-Highlight" hidden="true" alphaMode="ADD" setAllPoints="true">
|
||||
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
|
||||
</Texture>
|
||||
<FontString parentKey="Label" inherits="GameFontHighlight">
|
||||
<Size x="180" y="16"/>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Button>
|
||||
|
||||
<Button name="EventTraceScrollBoxButtonTemplate" inherits="EventTraceButtonBehaviorTemplate" mixin="EventTraceScrollBoxButtonMixin" virtual="true">
|
||||
<Size y="20"/>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture setAllPoints="true">
|
||||
<Color r=".1" g=".1" b=".1" a="1"/>
|
||||
</Texture>
|
||||
<Texture parentKey="Alternate" hidden="true" alphaMode="ADD" setAllPoints="true">
|
||||
<Color r=".02" g=".02" b=".02" a="1"/>
|
||||
</Texture>
|
||||
<Texture parentKey="MouseoverOverlay" hidden="true" alphaMode="ADD" setAllPoints="true">
|
||||
<Color r=".1" g=".1" b=".1" a="1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture parentKey="FlashOverlay" setAllPoints="true">
|
||||
<Color r="1" b="1" g="1" a=".5"/>
|
||||
<Animations>
|
||||
<AnimationGroup parentKey="Anim" setToFinalAlpha="true" inherits="VisibleWhilePlayingAnimGroupTemplate">
|
||||
<Alpha fromAlpha="0.5" toAlpha="1" duration="0.5" order="1" smoothing="IN"/>
|
||||
<Alpha fromAlpha="1" toAlpha="0.0" duration="0.5" order="2" smoothing="IN"/>
|
||||
</AnimationGroup>
|
||||
</Animations>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Button>
|
||||
|
||||
<Button name="EventTraceLogEventButtonTemplate" inherits="EventTraceScrollBoxButtonTemplate" registerForClicks="AnyUp" mixin="EventTraceLogEventButtonMixin" virtual="true">
|
||||
<Size y="20"/>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString parentKey="RightLabel" inherits="GameFontHighlightSmall" justifyH="RIGHT">
|
||||
<Size y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" x="-5"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString parentKey="LeftLabel" inherits="GameFontHighlightSmall" wordwrap="false" justifyH="LEFT">
|
||||
<Size y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" x="24"/>
|
||||
<Anchor point="RIGHT" relativeKey="$parent.RightLabel" relativePoint="LEFT" x="-5"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button parentKey="HideButton" inherits="UIPanelCloseButtonNoScripts">
|
||||
<Size x="20" y="20"/>
|
||||
</Button>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad"/>
|
||||
<OnEnter method="OnEnter"/>
|
||||
<OnLeave method="OnLeave"/>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<Button name="EventTraceLogMessageButtonTemplate" inherits="EventTraceScrollBoxButtonTemplate" mixin="EventTraceLogMessageButtonMixin" virtual="true">
|
||||
<Size x="100" y="20"/>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString parentKey="RightLabel" inherits="GameFontHighlightSmall" justifyH="RIGHT">
|
||||
<Size y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" x="-5"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString parentKey="LeftLabel" inherits="GameFontHighlightSmall" wordwrap="false" justifyH="LEFT">
|
||||
<Size y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" x="24"/>
|
||||
<Anchor point="RIGHT" relativeKey="$parent.RightLabel" relativePoint="LEFT" x="-5"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnEnter method="OnEnter"/>
|
||||
<OnLeave method="OnLeave"/>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<Button name="EventTraceFilterButtonTemplate" inherits="EventTraceScrollBoxButtonTemplate" mixin="EventTraceFilterButtonMixin" virtual="true">
|
||||
<Size x="100" y="20"/>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString parentKey="Label" inherits="GameFontHighlightSmall" justifyH="LEFT">
|
||||
<Size y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" x="24"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button parentKey="HideButton" inherits="UIPanelCloseButtonNoScripts">
|
||||
<Size x="20" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" x="3"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
<CheckButton parentKey="CheckButton" inherits="EventTraceCheckButtonTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" x="-3"/>
|
||||
</Anchors>
|
||||
</CheckButton>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnDoubleClick method="OnDoubleClick"/>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<Frame name="EventTrace" mixin="EventTracePanelMixin" inherits="ButtonFrameTemplate" parent="UIParent" toplevel="true" movable="true" resizable="true" enableMouse="true" frameStrata="DIALOG">
|
||||
<Size x="600" y="400"/>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<Frame parentKey="TitleBar" inherits="PanelDragBarTemplate" enableMouse="true">
|
||||
<Size y="32"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"/>
|
||||
<Anchor point="TOPRIGHT"/>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
</Layers>
|
||||
</Frame>
|
||||
<Button parentKey="ResizeButton" inherits="PanelResizeButtonTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMRIGHT" x="-4" y="4"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
<Frame parentKey="SubtitleBar">
|
||||
<Size y="32"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"/>
|
||||
<Anchor point="TOPRIGHT"/>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<Button parentKey="ViewLog" inherits="EventTraceMenuButtonTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativePoint="BOTTOMLEFT" x="5"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
<Button parentKey="ViewFilter" inherits="EventTraceMenuButtonTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeKey="$parent.ViewLog" relativePoint="TOPRIGHT" x="3"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
<DropdownButton parentKey="OptionsDropdown" inherits="WowStyle1FilterDropdownTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT" relativePoint="BOTTOMRIGHT" x="-20"/>
|
||||
</Anchors>
|
||||
</DropdownButton>
|
||||
</Frames>
|
||||
</Frame>
|
||||
<Frame parentKey="Log">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeKey="$parent.SubtitleBar" relativePoint="BOTTOMLEFT" x="8" y="-32"/>
|
||||
<Anchor point="BOTTOMRIGHT" x="-9" y="28"/>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<Frame parentKey="Bar">
|
||||
<Size y="24"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"/>
|
||||
<Anchor point="TOPRIGHT"/>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString parentKey="Label" inherits="GameFontHighlight" justifyH="LEFT">
|
||||
<Size x="135" y="10"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" x="12" y="0"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button parentKey="MarkButton" inherits="EventTraceMenuButtonTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" x="-3" y="0"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
<Button parentKey="PlaybackButton" inherits="EventTraceMenuButtonTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" relativeKey="$parent.MarkButton" relativePoint="LEFT" x="-3"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
<Button parentKey="DiscardAllButton" inherits="EventTraceMenuButtonTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" relativeKey="$parent.PlaybackButton" relativePoint="LEFT" x="-3"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
<EditBox parentKey="SearchBox" autoFocus="false" historyLines="1" bytes="64" inherits="ClassicAPISearchBoxTemplate">
|
||||
<Size x="180" y="22"/>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" relativeKey="$parent.DiscardAllButton" relativePoint="LEFT" x="-3"/>
|
||||
</Anchors>
|
||||
</EditBox>
|
||||
</Frames>
|
||||
</Frame>
|
||||
<Frame parentKey="Events">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeKey="$parent.Bar" relativePoint="BOTTOMLEFT" y="-2"/>
|
||||
<Anchor point="BOTTOMRIGHT"/>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<Frame parentKey="ScrollBox" inherits="WowScrollBoxList">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"/>
|
||||
<Anchor point="BOTTOMRIGHT" x="-32"/>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture setAllPoints="true">
|
||||
<Color r=".03" g=".03" b=".03" a="1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
<EventFrame parentKey="ScrollBar" inherits="MinimalScrollBar">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeKey="$parent.ScrollBox" relativePoint="TOPRIGHT" x="4" y="0"/>
|
||||
<Anchor point="BOTTOMLEFT" relativeKey="$parent.ScrollBox" relativePoint="BOTTOMRIGHT" x="4" y="0"/>
|
||||
</Anchors>
|
||||
</EventFrame>
|
||||
</Frames>
|
||||
</Frame>
|
||||
<Frame parentKey="Search">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeKey="$parent.Bar" relativePoint="BOTTOMLEFT" y="-2"/>
|
||||
<Anchor point="BOTTOMRIGHT"/>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<Frame parentKey="ScrollBox" inherits="WowScrollBoxList">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"/>
|
||||
<Anchor point="BOTTOMRIGHT" x="-32"/>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture setAllPoints="true">
|
||||
<Color r=".03" g=".03" b=".03" a="1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
<EventFrame parentKey="ScrollBar" inherits="MinimalScrollBar">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeKey="$parent.ScrollBox" relativePoint="TOPRIGHT" x="4" y="0"/>
|
||||
<Anchor point="BOTTOMLEFT" relativeKey="$parent.ScrollBox" relativePoint="BOTTOMRIGHT" x="4" y="0"/>
|
||||
</Anchors>
|
||||
</EventFrame>
|
||||
</Frames>
|
||||
</Frame>
|
||||
</Frames>
|
||||
</Frame>
|
||||
<Frame parentKey="Filter" hidden="true">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeKey="$parent.SubtitleBar" relativePoint="BOTTOMLEFT" x="8" y="-32"/>
|
||||
<Anchor point="BOTTOMRIGHT" x="-9" y="28"/>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<Frame parentKey="Bar">
|
||||
<Size y="24"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"/>
|
||||
<Anchor point="TOPRIGHT"/>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString parentKey="Label" inherits="GameFontHighlight" justifyH="LEFT">
|
||||
<Size x="135" y="10"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" x="12" y="0"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button parentKey="CheckAllButton" inherits="EventTraceMenuButtonTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" x="-3"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
<Button parentKey="UncheckAllButton" inherits="EventTraceMenuButtonTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" relativeKey="$parent.CheckAllButton" relativePoint="LEFT" x="-3"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
<Button parentKey="DiscardAllButton" inherits="EventTraceMenuButtonTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" relativeKey="$parent.UncheckAllButton" relativePoint="LEFT" x="-3"/>
|
||||
</Anchors>
|
||||
</Button>
|
||||
</Frames>
|
||||
</Frame>
|
||||
|
||||
<Frame parentKey="ScrollBox" inherits="WowScrollBoxList">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeKey="$parent.Bar" relativePoint="BOTTOMLEFT" y="-2"/>
|
||||
<Anchor point="BOTTOMRIGHT" x="-32"/>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture setAllPoints="true">
|
||||
<Color r=".03" g=".03" b=".03" a="1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
<EventFrame parentKey="ScrollBar" inherits="MinimalScrollBar">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeKey="$parent.ScrollBox" relativePoint="TOPRIGHT" x="4" y="0"/>
|
||||
<Anchor point="BOTTOMLEFT" relativeKey="$parent.ScrollBox" relativePoint="BOTTOMRIGHT" x="4" y="0"/>
|
||||
</Anchors>
|
||||
</EventFrame>
|
||||
</Frames>
|
||||
</Frame>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad"/>
|
||||
<OnEvent method="OnEvent"/>
|
||||
<OnShow method="OnShow"/>
|
||||
<OnHide method="OnHide"/>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
<GameTooltip name="EventTraceTooltip" frameStrata="TOOLTIP" hidden="true" parent="EventTrace" inherits="SharedTooltipTemplate"/>
|
||||
</Ui>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user