mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 12:27:35 +00:00
Compare commits
145
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
977eb3f60e | ||
|
|
091cfeea89 | ||
|
|
57edfe76c9 | ||
|
|
4fd56166cd | ||
|
|
60405941db | ||
|
|
89e2c6ba8f | ||
|
|
4ce271c7eb | ||
|
|
7cc6f45790 | ||
|
|
dccf6050ca | ||
|
|
33a8000579 | ||
|
|
1aa60be6f6 | ||
|
|
c8f9c23fa4 | ||
|
|
3b1a0f849e | ||
|
|
8d01e5e30a | ||
|
|
80de069b41 | ||
|
|
493e80548e | ||
|
|
be3c8a0a42 | ||
|
|
7b9fc5f076 | ||
|
|
11fb22f964 | ||
|
|
f9ac402e98 | ||
|
|
c0cc787925 | ||
|
|
90061e73a4 | ||
|
|
01367b9b26 | ||
|
|
248249c089 | ||
|
|
dc0f929050 | ||
|
|
25292427d6 | ||
|
|
78b4ece8a6 | ||
|
|
c5785b8579 | ||
|
|
7e7f6f0bf6 | ||
|
|
a6b5982b64 | ||
|
|
398b773fc4 | ||
|
|
aed68f24b8 | ||
|
|
8f08220346 | ||
|
|
3bbead72c8 | ||
|
|
316bb0dc95 | ||
|
|
6a88c2ee37 | ||
|
|
3616990a2f | ||
|
|
c6e76306ab | ||
|
|
846aa44330 | ||
|
|
683864246b | ||
|
|
8ff1549b83 | ||
|
|
e4c2da3be3 | ||
|
|
1148be8cdc | ||
|
|
4294a2bbfa | ||
|
|
5bc3110818 | ||
|
|
87e8d06ef6 | ||
|
|
01b7bf3f12 | ||
|
|
62ac523be3 | ||
|
|
6f6beca9c7 | ||
|
|
ae03c22180 | ||
|
|
d68a7418dc | ||
|
|
e309f6d32e | ||
|
|
14e43af371 | ||
|
|
3cbef80a2b | ||
|
|
800d90d386 | ||
|
|
81ffc12766 | ||
|
|
b2c7d63085 | ||
|
|
b48ffe42fb | ||
|
|
0e27295854 | ||
|
|
708226a929 | ||
|
|
230913519e | ||
|
|
ce96c56c4a | ||
|
|
e2ae028492 | ||
|
|
1260ce3f64 | ||
|
|
32c04f8126 | ||
|
|
5fe9ecfbdb | ||
|
|
8a17b86478 | ||
|
|
e0207ddd09 | ||
|
|
6b78320aa8 | ||
|
|
1a84d01d4a | ||
|
|
d22d10316b | ||
|
|
f58cccc86f | ||
|
|
b08d2fe813 | ||
|
|
f0f784bd70 | ||
|
|
72fc61dc6b | ||
|
|
e68f68ca47 | ||
|
|
627c86f5ba | ||
|
|
b644567cf9 | ||
|
|
891ef4f1e6 | ||
|
|
9142325a9e | ||
|
|
561c03cbe9 | ||
|
|
0296267f0d | ||
|
|
cb459acc28 | ||
|
|
b30735ecf9 | ||
|
|
0e681855b7 | ||
|
|
8bb6055e05 | ||
|
|
e7918018a2 | ||
|
|
1e1989d230 | ||
|
|
be93a6f969 | ||
|
|
640ebd0ab0 | ||
|
|
4bb31eb98d | ||
|
|
be21ff50f6 | ||
|
|
d7103de564 | ||
|
|
c00eb24a8d | ||
|
|
b65c9e0319 | ||
|
|
de033896b1 | ||
|
|
dc8d4caf12 | ||
|
|
20069f3815 | ||
|
|
f2fa112f0a | ||
|
|
fca967c827 | ||
|
|
24404b5a23 | ||
|
|
a106d86969 | ||
|
|
fac3be3a0b | ||
|
|
b49c107778 | ||
|
|
53b7f79440 | ||
|
|
7b21453469 | ||
|
|
ebfd9d034d | ||
|
|
d825bcbdda | ||
|
|
6b36bb10e6 | ||
|
|
6628dcca19 | ||
|
|
55d94c80d6 | ||
|
|
c0b403ba05 | ||
|
|
f6b98bc3ec | ||
|
|
8f4f574174 | ||
|
|
43a989dd56 | ||
|
|
0caddb8fe6 | ||
|
|
8e603a7a0b | ||
|
|
4726fd7c85 | ||
|
|
3e0c40184b | ||
|
|
b3e8d92b03 | ||
|
|
0a3f8324ea | ||
|
|
6f3a8ffc1d | ||
|
|
3862408e1a | ||
|
|
12a81f7f89 | ||
|
|
05fa5845b7 | ||
|
|
6ed0edc947 | ||
|
|
1645e6b43c | ||
|
|
efcdc54a50 | ||
|
|
6a24271cd1 | ||
|
|
a7ce7555f0 | ||
|
|
04790cb6c1 | ||
|
|
e7ecae56da | ||
|
|
65c6b0b271 | ||
|
|
5f94fb98bd | ||
|
|
426f6b6ec9 | ||
|
|
3a4d291748 | ||
|
|
1cdd46958f | ||
|
|
72f0e5d98f | ||
|
|
ceb6aaf46b | ||
|
|
84c936faa4 | ||
|
|
957d66af49 | ||
|
|
3849b7513b | ||
|
|
1cdd443e53 | ||
|
|
ada33825ad | ||
|
|
dd584703a2 |
@@ -10,3 +10,4 @@
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ico binary
|
||||
*.blp binary
|
||||
|
||||
+9
-7
@@ -76,13 +76,14 @@ endif()
|
||||
add_library(${PROJECT_NAME} SHARED
|
||||
${SOURCES} ${HEADERS} ${TINYCBOR_SOURCES})
|
||||
|
||||
# Embed the addons under `src/embed/AddOns/` and `src/bindings/Bindings.xml` as RCDATA
|
||||
# resources. The file-read hook in `src/addons/Embedded.cpp` serves the
|
||||
# addon files when the engine asks for paths under
|
||||
# `Interface\AddOns\<addon>\`, so the addons load even when the user
|
||||
# doesn't have them on disk. `src/bindings/Inject.cpp` splices the bindings
|
||||
# XML (our TARGETING-group additions) into the engine's
|
||||
# `Interface\FrameXML\Bindings.xml` at load time.
|
||||
# Embed the addons under `src/embed/AddOns/`, `src/bindings/Bindings.xml` and
|
||||
# the textures under `src/embed/Textures/` as RCDATA resources. The file-read
|
||||
# hook in `src/addons/Embedded.cpp` serves the addon files when the engine
|
||||
# asks for paths under `Interface\AddOns\<addon>\`, so the addons load even
|
||||
# when the user doesn't have them on disk. `src/bindings/Inject.cpp` splices
|
||||
# the bindings XML (our TARGETING-group additions) into the engine's
|
||||
# `Interface\FrameXML\Bindings.xml` at load time. `src/texture/Builtin.cpp`
|
||||
# serves each texture at its game path when no file has it.
|
||||
include(cmake/embed_resources.cmake)
|
||||
embed_resources(
|
||||
TARGET ${PROJECT_NAME}
|
||||
@@ -90,6 +91,7 @@ embed_resources(
|
||||
ADDONS "!!!ClassicAPI" "ClassicAPI_DebugTools" "ClassicAPI_EventTrace"
|
||||
ADDON_VERSION "${CLASSICAPI_TOC_VERSION}"
|
||||
BINDINGS_XML "${CMAKE_CURRENT_SOURCE_DIR}/src/bindings/Bindings.xml"
|
||||
TEXTURES_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/src/embed/Textures"
|
||||
OUT_DIR "${CMAKE_BINARY_DIR}/generated")
|
||||
|
||||
target_compile_definitions(
|
||||
|
||||
@@ -22,6 +22,7 @@ reference is in **[docs/API.md](docs/API.md)**.
|
||||
| Inline textures | Draws inline texture markup (`\|T…\|t`) and atlas markup (`\|A…\|a`) as icons in FontStrings, chat, and tooltips. This covers item and spell icons, raid-target markers, and the coin icons in money strings. `GetStringWidth` and `GetStringHeight` count the icons, so measured width and text wrapping stay correct. Done in pure C++ by hooking the engine's text pipeline — no addon. |
|
||||
| [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. |
|
||||
| [Model scenes](docs/API.md#modelscene) | The `ModelScene` frame type shows many models in one 3D scene, with its own camera, light and fog. Each model is an actor that you place, turn, scale and animate. An actor shows a creature display, a model file or a copy of a unit. It can also seat a rider, show spell visual kits and pause on one frame. A scene can show a WMO, for example a dungeon, around the actors, with its props and their sounds. An actor can move on the WMO as a character moves in the world: it runs, jumps, falls, swims and walks to a point. You hear the sounds of a scene from its camera, or from an actor. A follow camera follows an actor as the camera of the world follows your character, and stops in front of walls. An actor can glow, show a selection circle and show a name over its head, and a scene can show the click-to-move marker. |
|
||||
| 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. 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`. |
|
||||
@@ -51,7 +52,7 @@ 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`, `IsStackableAction`, `HasBonusActionBar`, `C_ActionBar.EnableActionRangeCheck`, `C_ActionBar.GetSpell`, `C_ActionBar.GetItemActionOnEquipSpellID`, `C_ActionBar.IsStackableAction`, `C_ActionBar.IsMacroActionWithShowTooltip`, `C_ActionBar.FindSpellActionButtons`, `C_ActionBar.HasSpellActionButtons`, `C_ActionBar.IsOnBarOrSpecialBar` |
|
||||
| [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 |
|
||||
@@ -88,6 +89,8 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation`, `: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: ModelScene](docs/API.md#modelscene) | `CreateFrame("ModelScene")`, `<ModelScene>` (`drawLayer`, `fogNear`, `fogFar`, `<FogColor>`, `<ViewInsets>`), `:CreateActor`, `:GetNumActors`, `:GetActorAtIndex`, `:SetCameraPosition` / `:GetCameraPosition`, `:SetCameraOrientationByYawPitchRoll`, `:SetCameraOrientationByAxisVectors`, `:GetCameraForward`, `:GetCameraRight`, `:GetCameraUp`, `:SetCameraFieldOfView` / `:GetCameraFieldOfView`, `:SetCameraNearClip` / `:GetCameraNearClip`, `:SetCameraFarClip` / `:GetCameraFarClip`, `:SetViewInsets` / `:GetViewInsets`, `:SetViewTranslation` / `:GetViewTranslation`, `:Project3DPointTo2D`, `"FollowCamera"` (`:SetFollowActor` / `:GetFollowActor`), `:SetLightType` / `:GetLightType`, `:SetLightVisible` / `:IsLightVisible`, `:SetLightDirection` / `:GetLightDirection`, `:SetLightPosition` / `:GetLightPosition`, `:SetLightAmbientColor` / `:GetLightAmbientColor`, `:SetLightDiffuseColor` / `:GetLightDiffuseColor`, `:SetFogColor` / `:GetFogColor`, `:SetFogNear` / `:GetFogNear`, `:SetFogFar` / `:GetFogFar`, `:ClearFog`, `:SetDrawLayer` / `:GetDrawLayer`, `:SetPaused`, `:GetSceneTime`, `:After`, `:NewTimer`, `:NewTicker`, `:SetAllowOverlappedModels` / `:GetAllowOverlappedModels`, `:SetGroundTerrain` / `:GetGroundTerrain`, `:SetWorldModel`, `:SetWorldModelByMapID`, `:ClearWorldModel`, `:SetPlayWorldModelSounds` / `:GetPlayWorldModelSounds`, `:TraceSegment`, `:GetLiquidAtPoint`, `:SweepCollider`, `:SetSoundListenerActor` / `:GetSoundListenerActor`, `:GetActorAtCursor`, `:GetCursorRay`, `:GetActorsInRadius`, `:SetMoveMarker` / `:ClearMoveMarker` / `:GetMoveMarker`, `OnActorEnter` / `OnActorLeave` |
|
||||
| [Frame: ModelSceneActor](docs/API.md#actorsetmodelbycreaturedisplayiddisplayid--useactiveplayercustomizations) | `:SetModelByCreatureDisplayID`, `:SetModelByPath`, `:SetModelByUnit`, `:SetModelByUnitCreatureDisplayID`, `:GetModelUnitGUID`, `:Dress`, `:Undress`, `:UndressSlot`, `:TryOn`, `:DressPlayerSlot`, `:IsSlotAllowed` / `:IsSlotVisible`, `:GetItemTransmogInfo`, `:GetItemTransmogInfoList`, `:SetItemTransmogInfo`, `:SetSheathed` / `:GetSheathed`, `:SetSheathedCategory`, `:UseUnitSheatheCategory`, `:SetAutoDress` / `:GetAutoDress`, `:IsGeoReady`, `:ClearModel`, `:IsLoaded`, `:GetModelPath`, `:SetPosition` / `:GetPosition`, `:SetYaw` / `:GetYaw`, `:SetPitch` / `:GetPitch`, `:SetRoll` / `:GetRoll`, `:SetScale` / `:GetScale`, `:SetUseCenterForOrigin` / `:IsUsingCenterForOrigin`, `:GetActiveBoundingBox`, `:GetMaxBoundingBox`, `:Show` / `:Hide` / `:SetShown` / `:IsShown` / `:IsVisible`, `:SetAlpha` / `:GetAlpha`, `:SetAnimation` / `:GetAnimation`, `:GetAnimationVariation`, `:SetAnimationBlendOperation` / `:GetAnimationBlendOperation`, `:SetPaused` / `:GetPaused`, `:SetSpellVisualKit` / `:GetSpellVisualKit`, `:ApplySpellVisualKit`, `:PlayAnimationKit` / `:StopAnimationKit`, `:AttachToMount`, `:DetachFromMount`, `:CalculateMountScale`, `:GetAttachmentPosition`, `:AttachToActor`, `:DetachFromActor`, `:PlaySpellVisual`, `:StopSpellVisual`, `:PlaySound`, `:SetPlayAnimationSounds` / `:GetPlayAnimationSounds`, `:SetShadowShown` / `:IsShadowShown`, `:SetHighlight` / `:IsHighlighted`, `:SetSelectionCircle` / `:ClearSelectionCircle` / `:GetSelectionCircle`, `:SetNameText` / `:ClearNameText` / `:GetNameText`, `:GetNamePosition`, `:SetMovementEnabled` / `:IsMovementEnabled`, `:StartMovingForward`, `:StartMovingBackward`, `:StopMoving`, `:StartStrafingLeft`, `:StartStrafingRight`, `:StopStrafing`, `:StartTurningLeft`, `:StartTurningRight`, `:StopTurning`, `:Jump`, `:StartPitchingUp`, `:StartPitchingDown`, `:StopPitching`, `:IsFalling`, `:IsSwimming`, `:MoveTo`, `:SetWalking` / `:IsWalking`, `:GetCameraPivotHeight`, `OnLoad`, `OnUpdate`, `OnModelLoading`, `OnModelLoaded`, `OnModelCleared`, `OnAnimFinished`, `OnSpellVisualImpact`, `OnMoveFinished`, `<Actor>` templates |
|
||||
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled`, `:GetMessageInfo` (with the extra `:AddMessage` values), `:BackFillMessage` (adds a message as the oldest), `:RemoveMessagesByPredicate`, `:SetScrollOffset` / `:GetScrollOffset`, `:GetMaxScrollRange`, `:ScrollByAmount`, `:SetInsertMode` / `:GetInsertMode`, `:AddOnDisplayRefreshedCallback` |
|
||||
| [Frame: scripts](docs/API.md#onenable-and-ondisable-scripts) | `OnEnable`, `OnDisable`, `OnMinMaxChanged`, `OnAttributeChanged`, `OnCharComposition`, `OnTextChanged` (`userInput`), `PreClick`, `PostClick`, `SetModernScriptArgs` / `GetModernScriptArgs` |
|
||||
| [FriendList](docs/API.md#friendlist) | `AddOrRemoveFriend`, `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
|
||||
@@ -96,7 +99,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:AddSpellByID`, `GameTooltip:AddTexture`, `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetMinimumWidth`, `GameTooltip:GetOwner`, `GameTooltip:GetPadding`, `GameTooltip:GetSpell`, `GameTooltip:GetUnit`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:IsUnit`, `GameTooltip:SetAnchorType`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetFrameStack`, `GameTooltip:SetHyperlink` (full links, `spell:`, `quest:`), `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetTotem`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem`, `OnTooltipSetSpell`, `OnTooltipSetUnit`, `OnTooltipSetGameObject`, `OnTooltipSetFrameStack` (scripts) |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetPoiForUiMapID`, `C_GossipInfo.GetPoiInfo`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex`, `GetNumGossipActiveQuests`, `GetNumGossipAvailableQuests`, `GetNumGossipOptions` |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `GetMouseButtonName`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Input](docs/API.md#input) | `GetCursorDelta`, `GetMouseButtonClicked`, `GetMouseButtonName`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo`, `IsActiveBattlefieldArena` |
|
||||
| [Item: C_Item](docs/API.md#item) | `C_Item.DoesItemExist` / `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemClassInfo`, `C_Item.GetItemCount`, `C_Item.GetItemData` / `C_Item.GetItemDataByID`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon` / `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfo`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType` / `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize` / `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName` / `C_Item.GetItemNameByID`, `C_Item.GetItemQuality` / `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice` / `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID` / `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemTempEnchantInfo`, `C_Item.GetItemUniqueness` / `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached` / `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.IsUsableItem`, `C_Item.ItemHasRange`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockItem`, `C_Item.UnlockAllItems`, `C_Item.PickupItem`, `C_Item.RequestLoadItemData` / `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName` |
|
||||
| [Item: info](docs/API.md#getitemclassinfoclassid) | `GetItemClassInfo`, `GetItemSubClassInfo`, `GetItemIcon`, `IsConsumableItem`, `IsCurrentItem`, `IsDressableItem`, `IsEquippedItemType`, `IsHarmfulItem`, `IsHelpfulItem`, `IsUsableItem` |
|
||||
@@ -106,13 +109,15 @@ 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`, `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) |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `debuglocals`, `debugstack` (coroutine argument), `difftime`, `GetCallstackHeight`, `GetErrorCallstackHeight`, `gmatch`, `math.cosh`, `math.fmod`, `math.huge`, `math.modf`, `math.sinh`, `math.tanh`, `Mixin`, `newproxy`, `scrub`, `select`, `SetErrorCallstackHeight`, `strbyte` (range args), `strcmputf8i`, `strconcat`, `string.byte` (range args), `string.concat`, `string.format` (two-digit `%n$`), `string.gmatch`, `string.gsub` (table replacement, `%0`), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strlenutf8`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strsplittable`, `strtrim`, `table.count`, `table.maxn`, `table.removemulti`, `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` |
|
||||
| [MapExplorationInfo](docs/API.md#mapexplorationinfo) | `C_MapExplorationInfo.GetExploredAreaIDsAtPosition`, `C_MapExplorationInfo.GetExploredMapTextures`, `C_MapExplorationInfo.GetUnexploredMapTextures` |
|
||||
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems`, `GetBuybackItemLink` |
|
||||
| [Model](docs/API.md#model) | `DressUpModel:TryOn` (links, recipes, names), `DressUpModel:UndressSlot`, `Model:SetGlow`, `PlayerModel:RefreshCamera`, `PlayerModel:SetCamDistanceScale`, `PlayerModel:SetCreature`, `PlayerModel:SetDisplayInfo`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`) |
|
||||
| [Model](docs/API.md#model) | `DressUpModel:TryOn` (links, recipes, names, hand, enchant glow, result), `DressUpModel:UndressSlot`, `DressUpModel:IsGeoReady`, `DressUpModel:SetSheathed` / `GetSheathed`, `DressUpModel:SetAutoDress` / `GetAutoDress`, `DressUpModel:GetItemTransmogInfo` / `GetItemTransmogInfoList` / `SetItemTransmogInfo`, `DressUpModel:IsSlotAllowed` / `IsSlotVisible`, `Model:SetGlow`, `PlayerModel:RefreshCamera`, `PlayerModel:SetCamDistanceScale`, `PlayerModel:SetCreature` (`displayID`), `PlayerModel:SetDisplayInfo`, `PlayerModel:SetItem`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`), `PlayerModel:SetUnit` (result), `PlayerModel:CanSetUnit`, `PlayerModel:GetDisplayInfo`, `PlayerModel:SetAnimation`, `PlayerModel:FreezeAnimation`, `PlayerModel:HasAnimation`, `PlayerModel:SetDoBlend` / `GetDoBlend`, `PlayerModel:SetKeepModelOnHide` / `GetKeepModelOnHide`, `PlayerModel:ApplySpellVisualKit` |
|
||||
| [ModelInfo](docs/API.md#c_modelinfogetmodelsceneinfobyidmodelsceneid) | `C_ModelInfo.GetModelSceneInfoByID`, `C_ModelInfo.GetModelSceneCameraInfoByID`, `C_ModelInfo.GetModelSceneActorInfoByID`, `C_ModelInfo.GetModelSceneActorDisplayInfoByID` |
|
||||
| [Mount](docs/API.md#mount) | `C_MountJournal.GetMountFromItem`, `C_MountJournal.GetMountFromSpell`, `C_MountJournal.GetMountInfoExtraByID` |
|
||||
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` |
|
||||
@@ -131,7 +136,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetDirectionFinished`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer`, `InteractUnit`, `C_PlayerInteractionManager.InteractUnit`, `IsTargetLoose` |
|
||||
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes`, `C_TaxiMap.GetTaxiPaths`, `C_TaxiMap.GetTaxiPathWaypoints`, `C_TaxiMap.GetTaxiRoute` |
|
||||
| [Texture](docs/API.md#texture) | `C_Texture.GetAtlasElementID`, `C_Texture.GetAtlasElements`, `C_Texture.GetAtlasExists`, `C_Texture.GetAtlasID`, `C_Texture.GetAtlasInfo`, `C_Texture.RegisterAtlas` |
|
||||
| [Texture](docs/API.md#texture) | `C_Texture.GetAtlasElementID`, `C_Texture.GetAtlasElements`, `C_Texture.GetAtlasExists`, `C_Texture.GetAtlasID`, `C_Texture.GetAtlasInfo`, `C_Texture.RegisterAtlas`, `SetPortraitTextureFromCreatureDisplayID` |
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime`, `GetTickTime`, `GetTimeCached` |
|
||||
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
|
||||
| [Tracking](docs/API.md#tracking) | `GetNumTrackingTypes`, `GetTrackingInfo`, `SetTracking` |
|
||||
@@ -161,6 +166,7 @@ the glue state because GlueXML had no way to reach them otherwise.
|
||||
| [CharacterList](docs/API.md#characterlist) | `GetSavedCharacterOrder`, `SetSavedCharacterOrder` (persist to `WTF\Account\...\ClassicAPI.txt`) |
|
||||
| CVar | `GetCVar`, `SetCVar`, `RegisterCVar`, `GetCVarDefault`, `C_CVar.GetCVarBool`, `C_CVar.SetTempCVar`, `C_CVar.RemoveTempCVar` (storage is process-global — writes from glue are visible in-world and vice versa) |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsFirstLoadThisSession`, `C_Glue.IsOnGlueScreen` |
|
||||
| [Input](docs/API.md#getcursordelta) | `GetCursorDelta` |
|
||||
| Script | `RunScript` (compile and run a Lua chunk in the glue state's globals — useful for slash-command-style helpers in GlueXML) |
|
||||
| State | `IsLoggedIn` |
|
||||
|
||||
@@ -211,20 +217,21 @@ for what each command does, and
|
||||
|
||||
Each setting is saved in `WTF\Config.wtf`. Read and write them with `GetCVar`
|
||||
/ `SetCVar` or `/console`. Click a group name for the full rules. To change
|
||||
these settings and the player cache in the game, open the game menu and
|
||||
click AddOn Options, then ClassicAPI.
|
||||
these settings, the player cache and the ClassicAPI key bindings in the game,
|
||||
open the game menu and click AddOn Options, then ClassicAPI.
|
||||
|
||||
| Group | CVar | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| [Bindings](docs/API.md#actionbuttonusekeydown) | `ActionButtonUseKeyDown` | `0` | `1` uses an action-bar key binding when you press the key. `0` uses it when you release the key. |
|
||||
| [Combat](docs/API.md#stopautoattackontargetchange) | `stopAutoAttackOnTargetChange` | `0` | `1` stops melee auto-attack, Auto Shot and wand shots when you change target. |
|
||||
| [Combat log](docs/API.md#kept-entries) | `combatLogRetentionTime` | `300` | The number of seconds that the combat log keeps an entry. |
|
||||
| [Cast settings](docs/API.md#cast-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when a spell cannot be cast in it. |
|
||||
| [Stand, dismount and form](docs/API.md#stand-dismount-and-form-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast, loot or open the flight map while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast, use an object, loot, attack or open the flight map while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when you cast a spell that cannot be cast in it, use an item, or open the flight map. It also lets you talk to NPCs and use objects in druid forms and Ghost Wolf. |
|
||||
| [Bag sorting](docs/API.md#c_containergetsortbagsrighttoleft--c_containersetsortbagsrighttoleftenable) | `sortBagsRightToLeft` | `0` | `1` makes a sort fill your bags from the last slot of the last bag. |
|
||||
| | `backpackAutosortDisabled` | `0` | `1` keeps the items in your backpack in place when you sort your bags. |
|
||||
| | `bankAutosortDisabled` | `0` | `1` keeps the items in the main bank slots in place when you sort your bank. |
|
||||
| [Expanded cursors](docs/API.md#expanded-cursors-expandedcursors) | `expandedCursors` | `0` | `1` makes the cursor show what you can do with an NPC or an object: an anvil for repairs, a hearthstone for an inn, a horseshoe for a stable, `!` or `?` for quests, and a fish hook for your fishing bobber. |
|
||||
| [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. |
|
||||
@@ -280,6 +287,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)) |
|
||||
@@ -395,6 +404,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| 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) |
|
||||
| Model blend operation | `Enum.ModelBlendOperation.{None,Anim}` (0–1) |
|
||||
| Autocomplete priority | `Enum.AutoCompletePriority.{Other,Interacted,InGroup,Guild,Friend,AccountCharacter,AccountCharacterSameRealm}` (0–6) |
|
||||
|
||||
</details>
|
||||
@@ -502,10 +512,10 @@ 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.
|
||||
window opens when an error occurs. It shows the message, the time, the count,
|
||||
the stack and the local variables 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
|
||||
|
||||
+52
-12
@@ -1,5 +1,5 @@
|
||||
# embed_resources.cmake — embed the addons and the bindings XML as Win32
|
||||
# RCDATA resources.
|
||||
# embed_resources.cmake — embed the addons, the bindings XML and the
|
||||
# built-in textures as Win32 RCDATA resources.
|
||||
#
|
||||
# Runs at configure time. It writes two files into OUT_DIR:
|
||||
#
|
||||
@@ -16,20 +16,27 @@
|
||||
# constexpr File kFiles[] = { ... };
|
||||
# constexpr size_t kFileCount = ...;
|
||||
# }
|
||||
# namespace Texture::Builtin::Manifest {
|
||||
# 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.
|
||||
# pack, in ADDONS order; then the textures, one pack. 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
|
||||
# Addon 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. A file's index
|
||||
# is its place in its addon's pack, which is in path order.
|
||||
# `Interface\AddOns\`. Texture paths are relative to TEXTURES_ROOT, where
|
||||
# each file sits at its game path (`Interface\Cursor\Quest.blp`), for
|
||||
# src/texture/Builtin.cpp. The DLL compares both ignoring case. A file's
|
||||
# index is its place in its 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
|
||||
@@ -46,7 +53,8 @@
|
||||
#
|
||||
# Usage:
|
||||
# embed_resources(TARGET <t> ADDONS_ROOT <dir> ADDONS <folder names...>
|
||||
# ADDON_VERSION <v> BINDINGS_XML <file> OUT_DIR <dir>)
|
||||
# ADDON_VERSION <v> BINDINGS_XML <file>
|
||||
# TEXTURES_ROOT <dir> OUT_DIR <dir>)
|
||||
|
||||
set(_EMBED_PACK_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/pack_files.cmake")
|
||||
|
||||
@@ -81,7 +89,8 @@ function(_embed_write_if_changed path content)
|
||||
endfunction()
|
||||
|
||||
function(embed_resources)
|
||||
cmake_parse_arguments(E "" "TARGET;ADDONS_ROOT;ADDON_VERSION;BINDINGS_XML;OUT_DIR"
|
||||
cmake_parse_arguments(E ""
|
||||
"TARGET;ADDONS_ROOT;ADDON_VERSION;BINDINGS_XML;TEXTURES_ROOT;OUT_DIR"
|
||||
"ADDONS" ${ARGN})
|
||||
|
||||
set(rc "// Generated by cmake/embed_resources.cmake - do not edit.\n\n")
|
||||
@@ -125,6 +134,22 @@ function(embed_resources)
|
||||
math(EXPR id "${id} + 1")
|
||||
endforeach()
|
||||
|
||||
file(GLOB_RECURSE textures CONFIGURE_DEPENDS RELATIVE
|
||||
"${E_TEXTURES_ROOT}" "${E_TEXTURES_ROOT}/*")
|
||||
list(SORT textures) # deterministic indexes
|
||||
set(texture_paths "")
|
||||
set(texture_manifest "")
|
||||
set(index 0)
|
||||
foreach(rel ${textures})
|
||||
list(APPEND texture_paths "${E_TEXTURES_ROOT}/${rel}")
|
||||
string(REPLACE "/" "\\\\" rel_bs "${rel}")
|
||||
string(APPEND texture_manifest " { \"${rel_bs}\", ${id}, ${index} },\n")
|
||||
math(EXPR index "${index} + 1")
|
||||
endforeach()
|
||||
_embed_pack("${E_OUT_DIR}" "Textures" ${texture_paths})
|
||||
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
|
||||
_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.
|
||||
@@ -159,6 +184,21 @@ ${manifest}};
|
||||
constexpr size_t kFileCount = sizeof(kFiles) / sizeof(kFiles[0]);
|
||||
|
||||
} // namespace Addons::Embedded::Manifest
|
||||
|
||||
namespace Texture::Builtin::Manifest {
|
||||
|
||||
struct File {
|
||||
const char *path; // the game path
|
||||
int resourceId; // the textures' pack
|
||||
int index; // its place in the pack
|
||||
};
|
||||
|
||||
constexpr File kFiles[] = {
|
||||
${texture_manifest}};
|
||||
|
||||
constexpr size_t kFileCount = sizeof(kFiles) / sizeof(kFiles[0]);
|
||||
|
||||
} // namespace Texture::Builtin::Manifest
|
||||
")
|
||||
|
||||
add_custom_target(${E_TARGET}_embed DEPENDS ${inputs})
|
||||
|
||||
+3583
-964
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -178,9 +178,9 @@ behaviour the comment describes (`push 0; mov edx, 1; mov ecx, L`) is
|
||||
| `0x006F4450` | `lua_next` | fastcall | calls `luaH_next` at `0x006FA2A0` | ★ |
|
||||
| `0x006F44E0` | `lua_concat` | fastcall | calls `luaV_concat` at `0x006F84D0` | ★ |
|
||||
| `0x006F4560` | `lua_newuserdata` | fastcall | tag 7; calls `luaS_newudata` at `0x006F9E30` | ★ |
|
||||
| `0x006F45B0` | `lua_getupvalue` | fastcall | reads upvalue from C closure | ◦ |
|
||||
| `0x006F4660` | `lua_setupvalue` | fastcall | | ◦ |
|
||||
| `0x006F46E0` | *internal upvalue helper*| | shared by get/set above | ◦ |
|
||||
| `0x006F45B0` | `lua_pushupvalues` | fastcall | pushes the running C closure's upvalues, returns how many | ★ |
|
||||
| `0x006F4660` | `lua_getupvalue` | fastcall | `(L, funcindex, n)`: pushes upvalue n, returns its name or null | ★ |
|
||||
| `0x006F46E0` | `aux_upvalue` | | finds upvalue n of a function, returns its name | ★ |
|
||||
|
||||
## Auxiliary library (`luaL_*`)
|
||||
|
||||
|
||||
@@ -64,6 +64,17 @@ bool RedirectJcc(uintptr_t site, uintptr_t expected, const void *target) {
|
||||
return PatchBytes(reinterpret_cast<uint8_t *>(site) + 2, &rel, sizeof rel);
|
||||
}
|
||||
|
||||
bool RedirectSlot(uintptr_t slot, uintptr_t expected, const void *target) {
|
||||
uintptr_t current = 0;
|
||||
std::memcpy(¤t, reinterpret_cast<const void *>(slot), sizeof current);
|
||||
const uintptr_t to = reinterpret_cast<uintptr_t>(target);
|
||||
if (current == to)
|
||||
return true;
|
||||
if (current != expected)
|
||||
return false;
|
||||
return PatchBytes(reinterpret_cast<void *>(slot), &to, sizeof to);
|
||||
}
|
||||
|
||||
bool ReplaceWithCall(uintptr_t site, const uint8_t *original, size_t len, const void *target) {
|
||||
uint8_t code[64];
|
||||
if (len < 5 || len > sizeof code)
|
||||
@@ -102,4 +113,20 @@ void EnsureParentDirs(const char *relPath) {
|
||||
}
|
||||
}
|
||||
|
||||
char FoldPathChar(char c) {
|
||||
if (c == '/')
|
||||
return '\\';
|
||||
if (c >= 'A' && c <= 'Z')
|
||||
return static_cast<char>(c + ('a' - 'A'));
|
||||
return c;
|
||||
}
|
||||
|
||||
bool SamePath(const char *a, const char *b) {
|
||||
for (; FoldPathChar(*a) == FoldPathChar(*b); ++a, ++b) {
|
||||
if (*a == '\0')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
|
||||
@@ -28,6 +28,10 @@ bool RedirectCall(uintptr_t site, uintptr_t expected, const void *target);
|
||||
// at `site` to `target` when it still reaches `expected`. Same contract.
|
||||
bool RedirectJcc(uintptr_t site, uintptr_t expected, const void *target);
|
||||
|
||||
// RedirectCall for a function-pointer slot (a vtable entry): points the slot
|
||||
// at `slot` to `target` when it still holds `expected`. Same contract.
|
||||
bool RedirectSlot(uintptr_t slot, uintptr_t expected, const void *target);
|
||||
|
||||
// Writes `call target` plus NOP padding over the `len` bytes (5 to 64) of
|
||||
// code at `site` when they still hold `original`; true when the site
|
||||
// now calls `target`. A site holding anything else (another DLL's patch, a
|
||||
@@ -45,4 +49,11 @@ void BoundedCopy(char *dst, const char *src, size_t n);
|
||||
// directory. Directories that already exist are a no-op.
|
||||
void EnsureParentDirs(const char *relPath);
|
||||
|
||||
// `c` as game paths compare it: '/' as '\', and A-Z as a-z.
|
||||
char FoldPathChar(char c);
|
||||
|
||||
// Whether `a` and `b` are the same game path: equal after FoldPathChar, so
|
||||
// any case and either slash.
|
||||
bool SamePath(const char *a, const char *b);
|
||||
|
||||
} // namespace Common
|
||||
|
||||
+7
-4
@@ -80,10 +80,13 @@ static void __stdcall LoadGlueScriptFunctions_h() {
|
||||
}
|
||||
|
||||
// Every Lua-side `frame:RegisterEvent(...)` is a chance to claim a slot
|
||||
// for any custom event still waiting. By this point the engine's table
|
||||
// is fully populated and SuperWoWhook / other DLLs have done their
|
||||
// post-rebuild writes, so the table state is settled and our walk from the
|
||||
// low end finds genuinely free slots.
|
||||
// for any custom event still waiting. By the first FrameXML registration
|
||||
// the engine's table is fully populated and SuperWoWhook / other DLLs have
|
||||
// done their post-rebuild writes, so the walk from the low end finds
|
||||
// genuinely free slots. A registration that arrives EARLIER (another DLL's
|
||||
// frame, between LoadScriptFunctions and the table build — issue #134)
|
||||
// still sees glue's 26-entry table; `RetryClaims` refuses that table and
|
||||
// re-validates its cached slots against the live one on every call.
|
||||
static void __fastcall FrameRegisterEvent_h(void *frame, void *edx,
|
||||
const char *eventName) {
|
||||
Event::Custom::RetryClaims();
|
||||
|
||||
+37
-16
@@ -15,6 +15,7 @@
|
||||
#include "MinHook.h"
|
||||
#include "Offsets.h"
|
||||
#include "api/Documentation.h"
|
||||
#include "compat/WowTurbo.h"
|
||||
|
||||
namespace Game {
|
||||
|
||||
@@ -29,15 +30,23 @@ float PixelToInternal(float px) {
|
||||
const float div = Read<float>(Offsets::VAR_COORD_ASPECT_COMPENSATION);
|
||||
if (div == 0.0f)
|
||||
return 0.0f;
|
||||
return px * mul / (div * static_cast<float>(Offsets::UI_COORD_SCALE_UNIT));
|
||||
return px * mul / (div * Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE));
|
||||
}
|
||||
|
||||
double InternalToPixel(float internal) {
|
||||
const float mul = Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE);
|
||||
const float div = Read<float>(Offsets::VAR_COORD_ASPECT_COMPENSATION);
|
||||
if (mul == 0.0f)
|
||||
using GetScaledScreenAspect_t = float(__cdecl *)();
|
||||
// Declared double so the quotient is read from ST0, as the engine's
|
||||
// pushes store it with `fstp qword`, not narrowed to float.
|
||||
using DivideByScreenScaleX_t = double(__stdcall *)(float);
|
||||
if (Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE) == 0.0f)
|
||||
return 0.0;
|
||||
return static_cast<double>(internal) * div * Offsets::UI_COORD_SCALE_UNIT / mul;
|
||||
// `aspect * 1024` is exact, so the float product rounds once, as the
|
||||
// engine's `fstp dword` does.
|
||||
const float aspect =
|
||||
reinterpret_cast<GetScaledScreenAspect_t>(Offsets::FUN_COORD_GET_SCALED_SCREEN_ASPECT)();
|
||||
const float scaled = aspect * Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE) * internal;
|
||||
return reinterpret_cast<DivideByScreenScaleX_t>(Offsets::FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X)(
|
||||
scaled);
|
||||
}
|
||||
|
||||
bool StringToFlag(const char *text) {
|
||||
@@ -72,12 +81,14 @@ namespace Lua {
|
||||
F(GetTable, lua_gettable, LUA_GET_TABLE) \
|
||||
F(RawGet, lua_rawget, LUA_RAW_GET) \
|
||||
F(RawGetI, lua_rawgeti, LUA_RAWGETI) \
|
||||
F(RawSetI, lua_rawseti, LUA_RAW_SET_I) \
|
||||
F(SetTable, lua_settable, LUA_SET_TABLE) \
|
||||
F(RawSet, lua_rawset, LUA_RAW_SET) \
|
||||
F(GetMetatable,lua_getmetatable,LUA_GET_METATABLE)\
|
||||
F(SetMetatable,lua_setmetatable,LUA_SET_METATABLE)\
|
||||
F(Insert, lua_insert, LUA_INSERT) \
|
||||
F(Remove, lua_remove, LUA_REMOVE) \
|
||||
F(Replace, lua_replace, LUA_REPLACE) \
|
||||
F(GetTop, lua_gettop, LUA_GET_TOP) \
|
||||
F(SetTop, lua_settop, LUA_SET_TOP) \
|
||||
F(Call, lua_call, LUA_CALL) \
|
||||
@@ -97,6 +108,9 @@ namespace Lua {
|
||||
F(Resume, lua_resume, LUA_RESUME) \
|
||||
F(Yield, lua_yield, LUA_YIELD) \
|
||||
F(ArgError, luaL_argerror, LUAL_ARG_ERROR) \
|
||||
F(CheckLString,luaL_checklstring,LUAL_CHECK_LSTRING)\
|
||||
F(CheckNumber, luaL_checknumber,LUAL_CHECK_NUMBER)\
|
||||
F(OptNumber, luaL_optnumber, LUAL_OPT_NUMBER) \
|
||||
F(SetN, luaL_setn, LUAL_SETN) \
|
||||
F(Ref, luaL_ref, LUA_REF_REF) \
|
||||
F(Unref, luaL_unref, LUA_REF_UNREF) \
|
||||
@@ -246,6 +260,12 @@ void *ResolveEditBox(void *L, int idx, bool raiseError) {
|
||||
void *ResolveModel(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_MODEL_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolvePlayerModel(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveDressUpModel(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_DRESSUPMODEL_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
|
||||
// Looks up `_G[name]`. If absent, creates a fresh table and binds it.
|
||||
// Leaves the resulting table on top of the stack.
|
||||
@@ -355,11 +375,11 @@ void RegisterGlueFunction(const char *name, CFunction func, const Doc::Function
|
||||
Api::Documentation::RecordGlobal(name, doc);
|
||||
}
|
||||
|
||||
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count,
|
||||
const Doc::Method *docs, int docCount) {
|
||||
void RegisterFrameMethods(void *context, ArrayRef<FrameMethodEntry> methods,
|
||||
Doc::MethodList docs) {
|
||||
auto fn = reinterpret_cast<RegisterFrameMethods_t>(Offsets::FUN_REGISTER_FRAME_METHODS);
|
||||
fn(table, count, context);
|
||||
Api::Documentation::RecordFrameMethods(context, table, count, docs, docCount);
|
||||
fn(methods.data, methods.count, context);
|
||||
Api::Documentation::RecordFrameMethods(context, methods, docs);
|
||||
}
|
||||
|
||||
// Registers `func` at `_G[tableName][methodName]`. If the namespace
|
||||
@@ -425,17 +445,16 @@ static bool SetIntegerTable(const char *parent, const char *sub,
|
||||
return true;
|
||||
}
|
||||
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub, ArrayRef<EnumIntegerEntry> entries,
|
||||
const char *docSystem) {
|
||||
if (SetIntegerTable(parent, sub, entries, count))
|
||||
Api::Documentation::RecordEnum(parent, sub, entries, count, docSystem);
|
||||
if (SetIntegerTable(parent, sub, entries.data, entries.count))
|
||||
Api::Documentation::RecordEnum(parent, sub, entries, docSystem);
|
||||
}
|
||||
|
||||
void RegisterConstants(const char *sub, const EnumIntegerEntry *entries, int count,
|
||||
void RegisterConstants(const char *sub, ArrayRef<EnumIntegerEntry> entries,
|
||||
const char *docSystem) {
|
||||
if (SetIntegerTable("Constants", sub, entries, count))
|
||||
Api::Documentation::RecordConstants(sub, entries, count, docSystem);
|
||||
if (SetIntegerTable("Constants", sub, entries.data, entries.count))
|
||||
Api::Documentation::RecordConstants(sub, entries, docSystem);
|
||||
}
|
||||
|
||||
void SetFieldNumber(void *L, const char *key, double value) {
|
||||
@@ -586,6 +605,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;
|
||||
|
||||
+63
-18
@@ -58,6 +58,18 @@ inline T &Ref(uintptr_t address) {
|
||||
return *reinterpret_cast<T *>(address);
|
||||
}
|
||||
|
||||
// An engine constant at a fixed VA, read each time it is used, so the value is
|
||||
// the one the engine's own code reads: a runtime patch shows, and so does a
|
||||
// global the engine's static initializers fill after this DLL loads. Converts
|
||||
// to T, so it stands in a float expression as a float would.
|
||||
//
|
||||
// const Game::Live<float> kSkin{Offsets::VAR_MOVER_SKIN};
|
||||
template <typename T>
|
||||
struct Live {
|
||||
uintptr_t address;
|
||||
operator T() const { return Read<T>(address); }
|
||||
};
|
||||
|
||||
// Calls `fn(object)` for each object of one of a frame's intrusive lists,
|
||||
// OFF_FRAME_REGION_LIST or OFF_FRAME_CHILD_LIST: node+4 next, node+8 object,
|
||||
// a NULL or low-bit-1 link ends it - the walk the engine's own frame code
|
||||
@@ -89,8 +101,10 @@ void *ResolveUnitToken(const char *token);
|
||||
|
||||
// UI pixels <-> internal layout (anchor) units. Script_SetPoint converts its
|
||||
// pixel offsets with `px * [VAR_COORD_DDC_WIDTH_SCALE] / ([VAR_COORD_ASPECT_COMPENSATION]
|
||||
// * UI_COORD_SCALE_UNIT)`; Script_GetLeft and Script_GetStringWidth push the
|
||||
// inverse. A region's LOCAL pixels must be multiplied by its effective scale
|
||||
// * [FLOAT_SCRIPT_COORD_SCALE])`; every script coordinate push (GetLeft,
|
||||
// GetStringWidth, GetFont's height, GetCursorPosition, OnCursorChanged) is the
|
||||
// inverse, `DivideByScreenScaleX(GetScaledScreenAspect() * 1024 * value)`,
|
||||
// which InternalToPixel calls in that order. A region's LOCAL pixels must be multiplied by its effective scale
|
||||
// (OFF_REGION_EFFECTIVE_SCALE) first. Both return 0 while the scale globals
|
||||
// are unset, so an early caller gets a zero offset rather than a wild one.
|
||||
float PixelToInternal(float px);
|
||||
@@ -147,6 +161,18 @@ int StrCmpI(const char *a, const char *b, int n = 0x7FFFFFFF);
|
||||
// literals + file-scope arrays): no heap, no dynamic init.
|
||||
// - Summaries are user-facing text: one sentence, plain words, present
|
||||
// tense, no patch versions.
|
||||
|
||||
// A view over a file-scope `const T[]` that takes its count from the array's
|
||||
// type, so no call site passes one; `{}` means none.
|
||||
template <typename T>
|
||||
struct ArrayRef {
|
||||
const T *data = nullptr;
|
||||
int count = 0;
|
||||
constexpr ArrayRef() = default;
|
||||
template <size_t N>
|
||||
constexpr ArrayRef(const T (&a)[N]) : data(a), count(static_cast<int>(N)) {}
|
||||
};
|
||||
|
||||
namespace Doc {
|
||||
|
||||
struct Field {
|
||||
@@ -172,14 +198,7 @@ constexpr Field Vararg(const char *type, const char *doc = nullptr) {
|
||||
return {"...", type, true, nullptr, doc};
|
||||
}
|
||||
|
||||
// A view over a file-scope `const Field[]`; `{}` means none.
|
||||
struct FieldList {
|
||||
const Field *data = nullptr;
|
||||
int count = 0;
|
||||
constexpr FieldList() = default;
|
||||
template <size_t N>
|
||||
constexpr FieldList(const Field (&a)[N]) : data(a), count(static_cast<int>(N)) {}
|
||||
};
|
||||
using FieldList = ArrayRef<Field>;
|
||||
|
||||
// A registered function. Positional init: `{summary, args, rets}` for a
|
||||
// table function, `{summary, args, rets, "XGlobals"}` for a global, and
|
||||
@@ -200,6 +219,7 @@ struct Method {
|
||||
const char *name;
|
||||
const Function *doc;
|
||||
};
|
||||
using MethodList = ArrayRef<Method>;
|
||||
|
||||
// A table shape a function returns or accepts → `Tables{Type="Structure"}`
|
||||
// in `system`. Self-registering: nothing binds a structure to Lua, so it
|
||||
@@ -240,7 +260,9 @@ constexpr int UpvalueIndex(int i) { return GLOBALS_INDEX - i; }
|
||||
// `lua_call` / `lua_pcall` nresults sentinel meaning "all".
|
||||
constexpr int MULTRET = -1;
|
||||
|
||||
// Type tag values returned by `Type()` (lua_type).
|
||||
// Type tag values returned by `Type()` (lua_type). TYPE_NONE is an absent
|
||||
// argument (past the top of the stack).
|
||||
constexpr int TYPE_NONE = -1;
|
||||
constexpr int TYPE_NIL = 0;
|
||||
constexpr int TYPE_BOOLEAN = 1;
|
||||
constexpr int TYPE_LIGHTUSERDATA = 2;
|
||||
@@ -270,6 +292,8 @@ using lua_rawget_t = void(__fastcall *)(void *L, int idx);
|
||||
// Pushes t[n] for the table at idx, raw (no metamethods). With idx =
|
||||
// REGISTRY_INDEX it pushes a value stored under an integer ref.
|
||||
using lua_rawgeti_t = void(__fastcall *)(void *L, int idx, int n);
|
||||
// Pops the top value into t[n] for the table at idx, raw.
|
||||
using lua_rawseti_t = void(__fastcall *)(void *L, int idx, int n);
|
||||
using lua_settable_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_rawset_t = void(__fastcall *)(void *L, int idx);
|
||||
// Pushes the metatable of the value at idx and returns 1, or pushes
|
||||
@@ -280,6 +304,8 @@ using lua_getmetatable_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_setmetatable_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_insert_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_remove_t = void(__fastcall *)(void *L, int idx);
|
||||
// Pops the top value into slot idx.
|
||||
using lua_replace_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_gettop_t = int(__fastcall *)(void *L);
|
||||
using lua_settop_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_call_t = void(__fastcall *)(void *L, int nargs, int nresults);
|
||||
@@ -313,6 +339,11 @@ using lua_newthread_t = void *(__fastcall *)(void *L);
|
||||
using lua_resume_t = int(__fastcall *)(void *L, int nargs);
|
||||
using lua_yield_t = int(__fastcall *)(void *L, int nresults);
|
||||
using luaL_argerror_t = void(__fastcall *)(void *L, int narg, const char *msg);
|
||||
// The auxlib argument readers: each raises the engine's own "bad argument"
|
||||
// error. CheckLString's `len` may be null.
|
||||
using luaL_checklstring_t = const char *(__fastcall *)(void *L, int narg, unsigned int *len);
|
||||
using luaL_checknumber_t = double(__fastcall *)(void *L, int narg);
|
||||
using luaL_optnumber_t = double(__fastcall *)(void *L, int narg, double def);
|
||||
using luaL_setn_t = void(__fastcall *)(void *L, int t, int n);
|
||||
// Pops the top value into the table at `t` under a fresh integer key and
|
||||
// returns the key (LUA_REFNIL, -1, for a nil). With REGISTRY_INDEX it anchors
|
||||
@@ -358,12 +389,14 @@ extern const lua_newtable_t NewTable;
|
||||
extern const lua_gettable_t GetTable;
|
||||
extern const lua_rawget_t RawGet;
|
||||
extern const lua_rawgeti_t RawGetI;
|
||||
extern const lua_rawseti_t RawSetI;
|
||||
extern const lua_settable_t SetTable;
|
||||
extern const lua_rawset_t RawSet;
|
||||
extern const lua_getmetatable_t GetMetatable;
|
||||
extern const lua_setmetatable_t SetMetatable;
|
||||
extern const lua_insert_t Insert;
|
||||
extern const lua_remove_t Remove;
|
||||
extern const lua_replace_t Replace;
|
||||
extern const lua_gettop_t GetTop;
|
||||
extern const lua_settop_t SetTop;
|
||||
extern const lua_call_t Call;
|
||||
@@ -383,6 +416,9 @@ extern const lua_newthread_t NewThread;
|
||||
extern const lua_resume_t Resume;
|
||||
extern const lua_yield_t Yield;
|
||||
extern const luaL_argerror_t ArgError;
|
||||
extern const luaL_checklstring_t CheckLString;
|
||||
extern const luaL_checknumber_t CheckNumber;
|
||||
extern const luaL_optnumber_t OptNumber;
|
||||
extern const luaL_setn_t SetN;
|
||||
extern const luaL_ref_t Ref;
|
||||
extern const luaL_unref_t Unref;
|
||||
@@ -465,6 +501,8 @@ void *ResolveTexture(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveFontString(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveEditBox(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveModel(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolvePlayerModel(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveDressUpModel(void *L, int idx = 1, bool raiseError = true);
|
||||
|
||||
// Every name these registrars bind is ALSO bound under `_G.ClassicAPI` —
|
||||
// `ClassicAPI.GetSpellInfo`, `ClassicAPI.C_Item.IsBound` — as an escape
|
||||
@@ -509,10 +547,11 @@ struct FrameMethodEntry {
|
||||
|
||||
// Registers a batch of methods on a per-frame-type registry (e.g.
|
||||
// GameTooltipMethodRegistry for `tooltip:Foo()` calls). `context` is the
|
||||
// registry address — see Offsets::VAR_*_METHOD_REGISTRY. `docs` pairs
|
||||
// descriptors to the entries by NAME (any order, any subset).
|
||||
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count,
|
||||
const Doc::Method *docs = nullptr, int docCount = 0);
|
||||
// registry address — see Offsets::VAR_*_METHOD_REGISTRY. `methods` and
|
||||
// `docs` are file-scope arrays; `docs` pairs descriptors to the entries by
|
||||
// NAME (any order, any subset).
|
||||
void RegisterFrameMethods(void *context, ArrayRef<FrameMethodEntry> methods,
|
||||
Doc::MethodList docs = {});
|
||||
|
||||
// Registers `func` at `_G[tableName][methodName]`, creating the namespace
|
||||
// table if it doesn't already exist. This is how modern WoW C_*-style APIs
|
||||
@@ -543,14 +582,14 @@ struct EnumIntegerEntry {
|
||||
// `Enum.AddOnSecurityStatus = { Secure=0, Insecure=1, ... }` shapes.
|
||||
// `docSystem` is the documentation System the enumeration belongs to
|
||||
// ("SpellBook", "Item", …); the entries themselves are the documentation.
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
// `entries` is a file-scope array.
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub, ArrayRef<EnumIntegerEntry> entries,
|
||||
const char *docSystem = nullptr);
|
||||
|
||||
// Registers `Constants[sub] = { entries }`, Blizzard's engine constant
|
||||
// tables (`Constants.MoneyFormattingConstants`). Documented as a "Constants"
|
||||
// table of `docSystem`.
|
||||
void RegisterConstants(const char *sub, const EnumIntegerEntry *entries, int count,
|
||||
void RegisterConstants(const char *sub, ArrayRef<EnumIntegerEntry> entries,
|
||||
const char *docSystem);
|
||||
|
||||
// Set `t[key] = value` on the table currently at stack[-1] (the most
|
||||
@@ -763,6 +802,12 @@ bool RunHookRegistrations();
|
||||
// issue #33, Frame::Attributes). A pure-C++ cache keyed by spellID/itemID is
|
||||
// NOT fragile and must not register.
|
||||
//
|
||||
// It runs AFTER the frames are gone: CGGameUI::Shutdown destroys every frame
|
||||
// (and each Model frame's scene with every model in it) before
|
||||
// CGGameUI::Initialize resets the Lua state. So a cleanup here only FORGETS
|
||||
// pointers; it never releases an engine object tied to a frame. Release those
|
||||
// when the frame dies (Model::Teardown for Model frames).
|
||||
//
|
||||
// Same lifetime + ordering rules as ModuleAutoRegister: static-init chaining,
|
||||
// undefined cross-TU order — fine, because each callback only clears its OWN
|
||||
// state. This is the "clear stale state" category only; a save-before-teardown
|
||||
|
||||
+2329
-74
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,178 @@
|
||||
// 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/>.
|
||||
|
||||
// Where a spell sits on the bars.
|
||||
//
|
||||
// FindSpellActionButtons(spell [, set]) the 1-based slots holding the
|
||||
// spell as a spell action, or nothing when none does.
|
||||
// HasSpellActionButtons(spell [, set]) whether any slot does.
|
||||
// IsOnBarOrSpecialBar(spell [, set]) the same, or the spell is on the
|
||||
// shapeshift bar or the pet bar.
|
||||
//
|
||||
// A slot matches on the spell ID it stores, as 4.3.4's PutActionInSlot
|
||||
// compares (IsSpell && GetSpell == spellId): a macro or an item that casts
|
||||
// the spell is not the spell, and a rank matches only itself. The special
|
||||
// bars are the engine's own lists — the shapeshift bar's spell array that
|
||||
// GetShapeshiftFormInfo reads, and the pet bar's spell entries
|
||||
// (GetPetActionInfo's types 1-5) while a pet bar is up.
|
||||
//
|
||||
// `set` is Enum.ActionBarSet, a mask whose Mkb bit is the one set of slots
|
||||
// this client has; Gamepad alone finds no slot.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "spell/Arg.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::Find {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int ACTION_BAR_SET_MKB = 1;
|
||||
constexpr int ACTION_BAR_SET_GAMEPAD = 2;
|
||||
constexpr int ACTION_BAR_SET_ALL = ACTION_BAR_SET_MKB | ACTION_BAR_SET_GAMEPAD;
|
||||
|
||||
// The spell at stack[1] and whether the set at stack[2] (default All)
|
||||
// covers this client's slots. 0 when the spell does not resolve.
|
||||
int Args(void *L, bool *searchSlots) {
|
||||
const int set = Game::Lua::IsNumber(L, 2) ? static_cast<int>(Game::Lua::ToNumber(L, 2))
|
||||
: ACTION_BAR_SET_ALL;
|
||||
*searchSlots = (set & ACTION_BAR_SET_MKB) != 0;
|
||||
return Spell::Arg::ResolveSpellID(L, 1);
|
||||
}
|
||||
|
||||
bool SlotHolds(int slot0, int spellID) {
|
||||
const uint32_t entry = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ACTION_TABLE))[slot0];
|
||||
return (entry & Offsets::ACTION_TYPE_MASK) == Offsets::ACTION_TYPE_SPELL &&
|
||||
entry == static_cast<uint32_t>(spellID);
|
||||
}
|
||||
|
||||
bool AnySlotHolds(int spellID) {
|
||||
for (int slot0 = 0; slot0 < Offsets::ACTION_TABLE_MAX_SLOTS; ++slot0) {
|
||||
if (SlotHolds(slot0, spellID))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OnShapeshiftBar(int spellID) {
|
||||
const int count = Game::Read<int>(Offsets::VAR_SHAPESHIFT_BAR_SPELL_COUNT);
|
||||
const auto *spells = Game::Read<const uint32_t *>(Offsets::VAR_SHAPESHIFT_BAR_SPELLS);
|
||||
if (spells == nullptr)
|
||||
return false;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (spells[i] == static_cast<uint32_t>(spellID))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OnPetBar(int spellID) {
|
||||
if (Game::Read<uint64_t>(Offsets::VAR_PET_GUID) == 0)
|
||||
return false;
|
||||
const auto *slots = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PET_ACTION_SLOTS));
|
||||
for (int i = 0; i < Offsets::PET_ACTION_SLOT_COUNT; ++i) {
|
||||
const uint32_t type = (slots[i] >> 24) & 0x3F;
|
||||
if (type >= Offsets::PET_ACTION_TYPE_SPELL_FIRST &&
|
||||
type <= Offsets::PET_ACTION_TYPE_SPELL_LAST &&
|
||||
(slots[i] & 0xFFFF) == static_cast<uint32_t>(spellID))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int __fastcall Script_FindSpellActionButtons(void *L) {
|
||||
bool searchSlots;
|
||||
const int spellID = Args(L, &searchSlots);
|
||||
if (spellID <= 0 || !searchSlots || !AnySlotHolds(spellID))
|
||||
return 0;
|
||||
Game::Lua::NewTable(L);
|
||||
int index = 0;
|
||||
for (int slot0 = 0; slot0 < Offsets::ACTION_TABLE_MAX_SLOTS; ++slot0) {
|
||||
if (!SlotHolds(slot0, spellID))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(++index));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slot0 + 1));
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_HasSpellActionButtons(void *L) {
|
||||
bool searchSlots;
|
||||
const int spellID = Args(L, &searchSlots);
|
||||
Game::Lua::PushBool(L, spellID > 0 && searchSlots && AnySlotHolds(spellID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsOnBarOrSpecialBar(void *L) {
|
||||
bool searchSlots;
|
||||
const int spellID = Args(L, &searchSlots);
|
||||
Game::Lua::PushBool(L, spellID > 0 && ((searchSlots && AnySlotHolds(spellID)) ||
|
||||
OnShapeshiftBar(spellID) || OnPetBar(spellID)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("spellID", "SpellIdentifier", "A spell ID, spell link, or spell name."),
|
||||
Game::Doc::Opt("setToSearch", "ActionBarSet", "All"),
|
||||
};
|
||||
const Game::Doc::Field kFindRets[] = {
|
||||
Game::Doc::Req("slots", "table", "The 1-based action slots; nothing when no slot "
|
||||
"holds the spell."),
|
||||
};
|
||||
const Game::Doc::Function kFindSpellActionButtons{
|
||||
"The action slots that hold the spell as a spell action.",
|
||||
kArgs, kFindRets};
|
||||
|
||||
const Game::Doc::Field kHasRets[] = {
|
||||
Game::Doc::Req("hasSpellActionButtons", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kHasSpellActionButtons{
|
||||
"Whether any action slot holds the spell as a spell action.",
|
||||
kArgs, kHasRets};
|
||||
|
||||
const Game::Doc::Field kOnBarRets[] = {
|
||||
Game::Doc::Req("isOnBarOrSpecialBar", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kIsOnBarOrSpecialBar{
|
||||
"Whether the spell is in an action slot, on the shapeshift bar, or on the pet bar.",
|
||||
kArgs, kOnBarRets};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kActionBarSet[] = {
|
||||
{"None", 0},
|
||||
{"Mkb", ACTION_BAR_SET_MKB},
|
||||
{"Gamepad", ACTION_BAR_SET_GAMEPAD},
|
||||
{"All", ACTION_BAR_SET_ALL},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "FindSpellActionButtons",
|
||||
&Script_FindSpellActionButtons, &kFindSpellActionButtons);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "HasSpellActionButtons",
|
||||
&Script_HasSpellActionButtons, &kHasSpellActionButtons);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "IsOnBarOrSpecialBar",
|
||||
&Script_IsOnBarOrSpecialBar, &kIsOnBarOrSpecialBar);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ActionBarSet", kActionBarSet, "ActionBar");
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Action::Find
|
||||
@@ -38,11 +38,24 @@
|
||||
// through to an empty return. There is no bag-item tag: items are always
|
||||
// by-itemID (0x80), and an earlier reading of the 0x40 "miss" as a bag item
|
||||
// was wrong.
|
||||
//
|
||||
// `C_ActionBar.GetSpell(slot)` — the spell the slot stands for, 0 for none.
|
||||
// It is the engine's own resolver (FUN_ACTION_SLOT_TO_SPELL), which every
|
||||
// action reader (cooldown, usable, range, current) goes through: a spell
|
||||
// action is its spell, a macro is its cached primary spell, and an item is
|
||||
// its first on-use spell — retail's ActionButton.lua calls GetSpell for
|
||||
// exactly that item case. The spellbook flag it also hands back is not part
|
||||
// of the answer.
|
||||
//
|
||||
// `C_ActionBar.IsMacroActionWithShowTooltip(slot)` — the slot holds a macro
|
||||
// whose body carries `#showtooltip`, by the parse `Macro::ShowTooltip` hands
|
||||
// the tooltip over on. `#show` is not it.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "action/Slot.h"
|
||||
#include "equipmentset/Data.h"
|
||||
#include "macro/ShowTooltip.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -126,10 +139,58 @@ int __fastcall Script_GetActionInfo(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetSpell(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_ActionBar.GetSpell(actionID)");
|
||||
return 0;
|
||||
}
|
||||
// The resolver bounds-checks the slot itself and answers 0 outside it.
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
uint32_t petFlag = 0;
|
||||
const uint32_t spellID = reinterpret_cast<SlotToSpell_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_ACTION_SLOT_TO_SPELL))(
|
||||
static_cast<uint32_t>(slot0), &petFlag);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(spellID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsMacroActionWithShowTooltip(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_ActionBar.IsMacroActionWithShowTooltip(actionID)");
|
||||
return 0;
|
||||
}
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
const uint32_t macroID = Action::Slot::MacroIDForSlot(slot0);
|
||||
Game::Lua::PushBool(L, macroID != 0 && Macro::ShowTooltip::HasShowTooltipDirective(macroID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kGetSpellArgs[] = {
|
||||
Game::Doc::Req("actionID", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kGetSpellRets[] = {
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
};
|
||||
const Game::Doc::Function kGetSpellDoc{
|
||||
"The spell an action stands for: a spell's own ID, a macro's spell, or an item's "
|
||||
"on-use spell; 0 when there is none.",
|
||||
kGetSpellArgs, kGetSpellRets};
|
||||
|
||||
const Game::Doc::Field kIsMacroActionWithShowTooltipRets[] = {
|
||||
Game::Doc::Req("isMacroActionWithShowTooltip", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kIsMacroActionWithShowTooltipDoc{
|
||||
"Whether the action is a macro with an explicit #showtooltip line.",
|
||||
kGetSpellArgs, kIsMacroActionWithShowTooltipRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetActionInfo", &Script_GetActionInfo);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "GetSpell", &Script_GetSpell, &kGetSpellDoc);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "IsMacroActionWithShowTooltip",
|
||||
&Script_IsMacroActionWithShowTooltip,
|
||||
&kIsMacroActionWithShowTooltipDoc);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -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,148 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The item behind an action slot, 4.3.4's CGActionBar::GetItem: an item-by-ID
|
||||
// action's item, or the item a macro's `#showtooltip` / `#show` resolved to
|
||||
// (1.12 keeps no item per macro, so `Macro::ShowTooltip` holds it). Both
|
||||
// readers below peek the item cache and never query it, as 4.3.4 does.
|
||||
//
|
||||
// IsStackableAction the item's max stack is more than 1 —
|
||||
// 4.3.4's IsStackableAucItem. The global
|
||||
// answers 1/nil, the C_ActionBar form a bool.
|
||||
// GetItemActionOnEquipSpellID the item's first on-equip spell (trigger 1),
|
||||
// nil when it has none.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Record.h"
|
||||
#include "macro/ShowTooltip.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::SlotItem {
|
||||
|
||||
namespace {
|
||||
|
||||
uint32_t ItemForSlot(int slot0) {
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
|
||||
return 0;
|
||||
const uint32_t entry = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ACTION_TABLE))[slot0];
|
||||
if ((entry & Offsets::ACTION_TYPE_MASK) == Offsets::ACTION_TYPE_ITEM_BY_ID)
|
||||
return entry & Offsets::ACTION_PAYLOAD_MASK_ITEM_BY_ID;
|
||||
Macro::ShowTooltip::Info info;
|
||||
if (Macro::ShowTooltip::ForSlot(slot0, &info) &&
|
||||
info.target == Macro::ShowTooltip::Target::Item)
|
||||
return static_cast<uint32_t>(info.itemID);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Reads the 1-based slot at stack[1]; false (after the usage error) when it
|
||||
// is not a number.
|
||||
bool SlotArg(void *L, const char *usage, int *slot0) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return false;
|
||||
}
|
||||
*slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsStackable(int slot0) {
|
||||
const uint8_t *record = ::Item::PeekRecord(ItemForSlot(slot0));
|
||||
return record != nullptr && Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_STACKABLE) > 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsStackableAction(void *L) {
|
||||
int slot0;
|
||||
if (!SlotArg(L, "Usage: IsStackableAction(slot)", &slot0))
|
||||
return 0;
|
||||
if (IsStackable(slot0))
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_C_IsStackableAction(void *L) {
|
||||
int slot0;
|
||||
if (!SlotArg(L, "Usage: C_ActionBar.IsStackableAction(actionID)", &slot0))
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, IsStackable(slot0));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetItemActionOnEquipSpellID(void *L) {
|
||||
int slot0;
|
||||
if (!SlotArg(L, "Usage: C_ActionBar.GetItemActionOnEquipSpellID(actionID)", &slot0))
|
||||
return 0;
|
||||
const uint8_t *record = ::Item::PeekRecord(ItemForSlot(slot0));
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_SPELL_SLOT_COUNT; ++i) {
|
||||
const uint32_t spellID = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SPELL_ID + i * 4);
|
||||
const uint32_t trigger = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SPELL_TRIGGER + i * 4);
|
||||
if (spellID != 0 && trigger == Offsets::ITEM_SPELLTRIGGER_ON_EQUIP) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(spellID));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kSlotArgs[] = {
|
||||
Game::Doc::Req("slot", "number", "The action slot, 1 to 120."),
|
||||
};
|
||||
const Game::Doc::Field kIsStackableRets[] = {
|
||||
Game::Doc::Opt("isStackable", "number", nullptr,
|
||||
"1 if the slot's item stacks, else nil."),
|
||||
};
|
||||
const Game::Doc::Function kIsStackableAction{
|
||||
"Returns 1 if an action slot holds an item that stacks.",
|
||||
kSlotArgs, kIsStackableRets, "ActionBar"};
|
||||
|
||||
const Game::Doc::Field kActionIDArgs[] = {
|
||||
Game::Doc::Req("actionID", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kCIsStackableRets[] = {
|
||||
Game::Doc::Req("isStackableAction", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kCIsStackableAction{
|
||||
"Whether the action's item stacks.",
|
||||
kActionIDArgs, kCIsStackableRets};
|
||||
|
||||
const Game::Doc::Field kOnEquipRets[] = {
|
||||
Game::Doc::Opt("onEquipSpellID", "number", nullptr,
|
||||
"Nil when the action has no item or its item has no on-equip spell."),
|
||||
};
|
||||
const Game::Doc::Function kGetItemActionOnEquipSpellID{
|
||||
"The spell the action's item grants while equipped.",
|
||||
kActionIDArgs, kOnEquipRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("IsStackableAction", &Script_IsStackableAction,
|
||||
&kIsStackableAction);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "IsStackableAction",
|
||||
&Script_C_IsStackableAction, &kCIsStackableAction);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "GetItemActionOnEquipSpellID",
|
||||
&Script_GetItemActionOnEquipSpellID,
|
||||
&kGetItemActionOnEquipSpellID);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Action::SlotItem
|
||||
@@ -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
|
||||
+11
-30
@@ -65,6 +65,7 @@
|
||||
|
||||
#include "embedded_resources.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/Embedded.h"
|
||||
@@ -76,6 +77,7 @@
|
||||
#include "addons/TocRewrite.h"
|
||||
#include "bindings/Inject.h"
|
||||
#include "embed/Resource.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -103,26 +105,12 @@ constexpr const char *kDevMarkerFile = ".classicapi-dev";
|
||||
// paths are case-insensitive.
|
||||
constexpr const char *kAddOnsPrefix = "Interface\\AddOns\\";
|
||||
|
||||
char NormalizeChar(char c) {
|
||||
if (c == '/') return '\\';
|
||||
if (c >= 'A' && c <= 'Z') return static_cast<char>(c + 32);
|
||||
return c;
|
||||
}
|
||||
|
||||
bool PathEqualsCI(const char *a, const char *b) {
|
||||
while (*a && *b) {
|
||||
if (NormalizeChar(*a) != NormalizeChar(*b)) return false;
|
||||
++a; ++b;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
// `path` with the case-insensitive `prefix` stripped, NULL when it does
|
||||
// not start with it.
|
||||
const char *StripPrefixCI(const char *path, const char *prefix) {
|
||||
while (*prefix) {
|
||||
if (*path == '\0') return nullptr;
|
||||
if (NormalizeChar(*path) != NormalizeChar(*prefix)) return nullptr;
|
||||
if (Common::FoldPathChar(*path) != Common::FoldPathChar(*prefix)) return nullptr;
|
||||
++path; ++prefix;
|
||||
}
|
||||
return path;
|
||||
@@ -130,7 +118,7 @@ const char *StripPrefixCI(const char *path, const char *prefix) {
|
||||
|
||||
int IndexOfAddon(const char *name) {
|
||||
for (size_t i = 0; i < kAddonCount; ++i) {
|
||||
if (PathEqualsCI(name, kAddons[i]))
|
||||
if (Common::SamePath(name, kAddons[i]))
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
return -1;
|
||||
@@ -144,7 +132,7 @@ int MatchAddonPath(const char *path, const char **relPath) {
|
||||
if (rel == nullptr) return -1;
|
||||
for (size_t i = 0; i < kAddonCount; ++i) {
|
||||
const char *rest = StripPrefixCI(rel, kAddons[i]);
|
||||
if (rest != nullptr && NormalizeChar(*rest) == '\\') {
|
||||
if (rest != nullptr && Common::FoldPathChar(*rest) == '\\') {
|
||||
*relPath = rel;
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
@@ -155,12 +143,10 @@ int MatchAddonPath(const char *path, const char **relPath) {
|
||||
// The embedded bytes for manifest path `relPath`; `data` is NULL when it
|
||||
// is not embedded.
|
||||
Embed::Bytes LookupEmbedded(const char *relPath) {
|
||||
for (size_t i = 0; i < Manifest::kFileCount; ++i) {
|
||||
if (PathEqualsCI(relPath, Manifest::kFiles[i].path))
|
||||
return Embed::File(Manifest::kFiles[i].resourceId,
|
||||
Manifest::kFiles[i].index);
|
||||
}
|
||||
return {nullptr, 0};
|
||||
const Manifest::File *file = Embed::FindFile(Manifest::kFiles, relPath);
|
||||
if (file == nullptr)
|
||||
return {nullptr, 0};
|
||||
return Embed::File(file->resourceId, file->index);
|
||||
}
|
||||
|
||||
// Engine file I/O + Storm allocator — see addons/EngineIO.h for the shapes and
|
||||
@@ -168,8 +154,6 @@ Embed::Bytes LookupEmbedded(const char *relPath) {
|
||||
// by the caller's standard SMemFree in turn.
|
||||
using AddOns::EngineIO::FileExistsFn;
|
||||
using AddOns::EngineIO::FileReadFn;
|
||||
using AddOns::EngineIO::SMemAllocFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
|
||||
// The trampolines to the original FUN_FILE_READ / FUN_FILE_EXISTS (installed
|
||||
// by the co-hooks below).
|
||||
@@ -292,8 +276,7 @@ void DecideSource(int addon) {
|
||||
// Free the disk buffer we just borrowed — we only needed it for
|
||||
// the version-line scrape. The actual content for the engine's
|
||||
// TOC read comes through the regular hook path below.
|
||||
auto SMemFree = reinterpret_cast<SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
SMemFree(diskBuf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(diskBuf, __FILE__, __LINE__);
|
||||
|
||||
// Missing or unparseable disk version → assume it's older than
|
||||
// anything we ship, embedded wins.
|
||||
@@ -335,10 +318,8 @@ int ReadAddonFile(int addon, const char *relPath, int unused, const char *path,
|
||||
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);
|
||||
void *buf = Storm::Alloc(totalSize, __FILE__, __LINE__);
|
||||
if (buf == nullptr) return 0;
|
||||
std::memcpy(buf, entry.data, entry.size);
|
||||
if (extraBytes > 0) {
|
||||
|
||||
@@ -32,10 +32,6 @@ using FileReadFn = int(__stdcall *)(int unused, const char *path, void **outBuf,
|
||||
// FUN_FILE_EXISTS (0x00648A30) — __stdcall, RET 8.
|
||||
using FileExistsFn = int(__stdcall *)(const char *path, int mode);
|
||||
|
||||
// FUN_STORM_SMEM_ALLOC / FUN_STORM_SMEM_FREE — __stdcall, RET 0x10. Buffers
|
||||
// FUN_FILE_READ hands out are freed with SMemFreeFn; a buffer we allocate with
|
||||
// SMemAllocFn is freed cleanly by the engine's own SMemFree in turn.
|
||||
using SMemAllocFn = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
|
||||
using SMemFreeFn = void(__stdcall *)(void *buf, const char *file, int line, int flags);
|
||||
// Buffers FUN_FILE_READ hands out are freed with Storm::Free (storm/Memory.h).
|
||||
|
||||
} // namespace AddOns::EngineIO
|
||||
|
||||
+1
-5
@@ -408,11 +408,7 @@ static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnInterfaceVersion",
|
||||
&Script_GetAddOnInterfaceVersion,
|
||||
&kGetAddOnInterfaceVersion);
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "AddOnSecurityStatus",
|
||||
kAddOnSecurityStatusEntries,
|
||||
sizeof(kAddOnSecurityStatusEntries) /
|
||||
sizeof(kAddOnSecurityStatusEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "AddOnSecurityStatus", kAddOnSecurityStatusEntries);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/Toc.h"
|
||||
#include "luasyntax/AddonNamespace.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -51,7 +52,6 @@ using IsLoadedFn_t = uint8_t(__fastcall *)(const char *name);
|
||||
// FUN_FILE_READ address routes through Embedded's file-read hook, which forwards
|
||||
// every non-`!!!ClassicAPI` path to the original untouched — what we want here.
|
||||
using AddOns::EngineIO::FileReadFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
|
||||
// True iff the addon's `.toc` has a line `## AllowAddOnTableAccess: <nonzero>`.
|
||||
// The value is read as a TOC boolean flag: parse the leading integer, true iff
|
||||
@@ -87,8 +87,7 @@ bool TocAllowsTableAccess(const char *name) {
|
||||
allowed = anyDigit && value != 0;
|
||||
}
|
||||
|
||||
auto smemFree = reinterpret_cast<SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
smemFree(buf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(buf, __FILE__, __LINE__);
|
||||
return allowed;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
#include "addons/Embedded.h"
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/Registry.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -235,9 +236,7 @@ bool TryHashToc(const char *name, uint32_t *outHash) {
|
||||
if (!ReadToc(name, &buf, &size))
|
||||
return false;
|
||||
*outHash = HashTocMetadata(static_cast<const char *>(buf), size);
|
||||
reinterpret_cast<AddOns::EngineIO::SMemFreeFn>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_STORM_SMEM_FREE))(
|
||||
buf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(buf, __FILE__, __LINE__);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/Toc.h"
|
||||
#include "script/LoadFile.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -66,7 +67,6 @@ constexpr size_t NPOS = static_cast<size_t>(-1);
|
||||
// FlavorToc applies), the exists-check, and the Storm free. See addons/EngineIO.h.
|
||||
using AddOns::EngineIO::FileExistsFn;
|
||||
using AddOns::EngineIO::FileReadFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
using LoadTocFilesFn = uint32_t(__fastcall *)(char *tocPath, int *a2, int *a3);
|
||||
using NameFn = const char *(*)(); // no-arg readers (return in EAX)
|
||||
|
||||
@@ -160,8 +160,7 @@ SvFlags ReadSvFlags(const char *tocPath) {
|
||||
f.perChar =
|
||||
AddOns::Toc::FindValue(b, size, "SavedVariablesPerCharacter", &v, &n) && n > 0;
|
||||
|
||||
reinterpret_cast<SMemFreeFn>(static_cast<uintptr_t>(Offsets::FUN_STORM_SMEM_FREE))(
|
||||
buf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(buf, __FILE__, __LINE__);
|
||||
return f;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/TocRewrite.h"
|
||||
#include "addons/Toc.h"
|
||||
#include "localization/Locale.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "storm/Memory.h"
|
||||
#include "turtle/Detect.h"
|
||||
|
||||
#include <cstddef>
|
||||
@@ -27,24 +29,12 @@ namespace AddOns::TocRewrite {
|
||||
|
||||
namespace {
|
||||
|
||||
using AddOns::EngineIO::SMemAllocFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
using AddOns::Toc::EqCI;
|
||||
using AddOns::Toc::Lower;
|
||||
|
||||
bool IsSpace(char c) { return c == ' ' || c == '\t'; }
|
||||
|
||||
// The client locale code ("enUS", "frFR", …) — the exact string
|
||||
// GetLocale() returns (Offsets.h VAR_LOCALE_NAME_TABLE derivation).
|
||||
const char *ClientLocale() {
|
||||
uint32_t idx = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
if (idx > 7) idx = 0; // slot 8 overreads the string pool — clamp
|
||||
const char *const *table = reinterpret_cast<const char *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_NAME_TABLE));
|
||||
const char *s = table[idx];
|
||||
return (s != nullptr) ? s : "enUS";
|
||||
}
|
||||
using Localization::Locale::Client;
|
||||
|
||||
// True iff the comma-separated `list` (spaces allowed) contains `token` (CI).
|
||||
bool ListContains(const char *list, size_t listLen, const char *token) {
|
||||
@@ -66,7 +56,7 @@ bool ListContains(const char *list, size_t listLen, const char *token) {
|
||||
const char *VariableValue(const char *head, size_t headLen) {
|
||||
if (EqCI(head, headLen, "Family")) return "Classic";
|
||||
if (EqCI(head, headLen, "Game")) return "Vanilla";
|
||||
if (EqCI(head, headLen, "TextLocale")) return ClientLocale();
|
||||
if (EqCI(head, headLen, "TextLocale")) return Client();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -158,7 +148,7 @@ bool EvalCondition(const char *head, size_t headLen, const char *args,
|
||||
if (EqCI(head, headLen, "ExcludeLoadGameType"))
|
||||
return !ListContainsGameType(args, argsLen);
|
||||
if (EqCI(head, headLen, "AllowLoadTextLocale"))
|
||||
return ListContains(args, argsLen, ClientLocale());
|
||||
return ListContains(args, argsLen, Client());
|
||||
if (EqCI(head, headLen, "AllowLoad"))
|
||||
return ListContains(args, argsLen, "game"); // addon files: in-game only
|
||||
return false;
|
||||
@@ -297,15 +287,13 @@ void Transform(const char *path, void **outBuf, size_t *outSize) {
|
||||
std::memcmp(rebuilt.data(), content, size) == 0)
|
||||
return; // brackets/commas were only in unaffected lines — nothing changed
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAllocFn>(Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
auto SMemFree = reinterpret_cast<SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
const size_t newLen = rebuilt.size();
|
||||
void *buf = SMemAlloc(newLen + 1, __FILE__, __LINE__, 0); // +1 NUL (extraBytes=1)
|
||||
void *buf = Storm::Alloc(newLen + 1, __FILE__, __LINE__); // +1 NUL (extraBytes=1)
|
||||
if (buf == nullptr) return; // OOM — keep the original
|
||||
std::memcpy(buf, rebuilt.data(), newLen);
|
||||
static_cast<char *>(buf)[newLen] = '\0';
|
||||
|
||||
SMemFree(*outBuf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(*outBuf, __FILE__, __LINE__);
|
||||
*outBuf = buf;
|
||||
if (outSize != nullptr) *outSize = newLen;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Layout.h"
|
||||
#include "frame/Alpha.h"
|
||||
#include "frame/Destruction.h"
|
||||
#include "texture/Transform.h"
|
||||
@@ -115,7 +116,6 @@ using SetColor_t = void(__fastcall *)(void *region, void *edx, const uint32_t *b
|
||||
using StoreCorners_t = void(__fastcall *)(void *region, void *edx, float *rect);
|
||||
using Crop_t = void(__fastcall *)(void *region, void *edx, float *rect);
|
||||
using SetStringPos_t = void(__fastcall *)(void *block, const float *position);
|
||||
using NdcToDdc_t = void(__fastcall *)(float *outX, float *outY, float x, float y);
|
||||
using SizeChanged_t = void(__fastcall *)(void *layout, void *edx, const float *oldRect);
|
||||
using InvalidateNode_t = void(__fastcall *)(void *node);
|
||||
|
||||
@@ -216,7 +216,7 @@ bool GetStringPos(void *block, float out[3]) {
|
||||
if (node == nullptr)
|
||||
return false;
|
||||
const float *position = Game::Ptr<const float>(node, Offsets::OFF_TEXT_NODE_POSITION);
|
||||
reinterpret_cast<NdcToDdc_t>(Offsets::FUN_COORD_NDC_TO_DDC)(&out[0], &out[1], position[0], position[1]);
|
||||
Frame::Layout::NdcToDdc(&out[0], &out[1], position[0], position[1]);
|
||||
out[2] = position[2];
|
||||
return true;
|
||||
}
|
||||
@@ -560,9 +560,8 @@ void RegisterLuaFunctions() {
|
||||
RegisterClass(Anim::s_rotationClass);
|
||||
RegisterClass(Anim::s_pathClass);
|
||||
RegisterClass(ControlPoint::s_class);
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), kRegionMethods,
|
||||
static_cast<int>(sizeof(kRegionMethods) / sizeof(kRegionMethods[0])),
|
||||
kRegionDocs, static_cast<int>(sizeof(kRegionDocs) / sizeof(kRegionDocs[0])));
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), kRegionMethods, kRegionDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -59,6 +59,8 @@ bool FloatAttr(const void *node, const char *name, float &out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// object[parentKey] = child, as 4.3.4's LoadXML does for both.
|
||||
void BindParentKey(void *object, Object *child, const char *key) {
|
||||
if (key == nullptr || *key == '\0')
|
||||
@@ -145,6 +147,8 @@ void LoadKeyValues(Object *object, const void *node, void *status) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// LoadXML_AnimOrigin: <Origin point="..."><Offset x= y=/></Origin>.
|
||||
void LoadOrigin(Anim *anim, const void *node) {
|
||||
const void *origin = FindChild(node, "Origin");
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
// (RegisterXMLTemplate), whatever its tag, so `inherits` resolves through
|
||||
// Xml::Template as 4.3.4's XMLHashNodeRef does.
|
||||
namespace Animation {
|
||||
class Object;
|
||||
class Group;
|
||||
class Anim;
|
||||
class ControlPoint;
|
||||
@@ -39,4 +40,13 @@ void LoadControlPoint(ControlPoint *point, const void *node, void *status);
|
||||
// The template node `name` resolves to, or nullptr.
|
||||
const void *Template(const char *name);
|
||||
|
||||
// What every script object's element shares (Model::Scene's actors too):
|
||||
// object[key] = child for parentKey, an append to object[key] for
|
||||
// parentArray, the <Scripts> (a body, a `function` global or a `method`
|
||||
// field) and the <KeyValues>.
|
||||
void BindParentKey(void *object, Object *child, const char *key);
|
||||
void BindParentArray(void *object, Object *child, const char *key);
|
||||
void LoadScripts(Object *object, const void *node);
|
||||
void LoadKeyValues(Object *object, const void *node, void *status);
|
||||
|
||||
} // namespace Animation::Xml
|
||||
|
||||
@@ -665,10 +665,15 @@ void RecordTable(const char *table, const char *name, const Function *doc) {
|
||||
Record(KIND_TABLE, table, name, nullptr, doc);
|
||||
}
|
||||
|
||||
void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry *table,
|
||||
int count, const Method *docs, int docCount) {
|
||||
void RecordFrameMethods(const void *registry,
|
||||
Game::ArrayRef<Game::Lua::FrameMethodEntry> methods,
|
||||
Game::Doc::MethodList docList) {
|
||||
if (g_pass == ENV_NONE)
|
||||
return;
|
||||
const Game::Lua::FrameMethodEntry *table = methods.data;
|
||||
const int count = methods.count;
|
||||
const Method *docs = docList.data;
|
||||
const int docCount = docList.count;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const Function *doc = nullptr;
|
||||
for (int d = 0; d < docCount; ++d) {
|
||||
@@ -720,14 +725,14 @@ void RecordValueTable(Kind kind, const char *parent, const char *sub,
|
||||
|
||||
} // namespace
|
||||
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system) {
|
||||
RecordValueTable(KIND_ENUM, parent, sub, entries, count, system);
|
||||
void RecordEnum(const char *parent, const char *sub,
|
||||
Game::ArrayRef<Game::Lua::EnumIntegerEntry> entries, const char *system) {
|
||||
RecordValueTable(KIND_ENUM, parent, sub, entries.data, entries.count, system);
|
||||
}
|
||||
|
||||
void RecordConstants(const char *sub, const Game::Lua::EnumIntegerEntry *entries, int count,
|
||||
void RecordConstants(const char *sub, Game::ArrayRef<Game::Lua::EnumIntegerEntry> entries,
|
||||
const char *system) {
|
||||
RecordValueTable(KIND_CONSTANTS, "Constants", sub, entries, count, system);
|
||||
RecordValueTable(KIND_CONSTANTS, "Constants", sub, entries.data, entries.count, system);
|
||||
}
|
||||
|
||||
} // namespace Api::Documentation
|
||||
|
||||
@@ -32,14 +32,15 @@ void EndPass();
|
||||
|
||||
void RecordGlobal(const char *name, const Game::Doc::Function *doc);
|
||||
void RecordTable(const char *table, const char *name, const Game::Doc::Function *doc);
|
||||
void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry *table,
|
||||
int count, const Game::Doc::Method *docs, int docCount);
|
||||
void RecordFrameMethods(const void *registry,
|
||||
Game::ArrayRef<Game::Lua::FrameMethodEntry> methods,
|
||||
Game::Doc::MethodList docs);
|
||||
// Names the widget system of a method hash this DLL owns (a frame class it
|
||||
// adds), alongside the engine registries the exporter already knows.
|
||||
void NameWidgetSystem(const void *registry, const char *system);
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system);
|
||||
void RecordConstants(const char *sub, const Game::Lua::EnumIntegerEntry *entries, int count,
|
||||
void RecordEnum(const char *parent, const char *sub,
|
||||
Game::ArrayRef<Game::Lua::EnumIntegerEntry> entries, const char *system);
|
||||
void RecordConstants(const char *sub, Game::ArrayRef<Game::Lua::EnumIntegerEntry> entries,
|
||||
const char *system);
|
||||
|
||||
} // namespace Api::Documentation
|
||||
|
||||
+2
-9
@@ -1023,16 +1023,9 @@ CompressState *FindOrClaimCompressState(uint64_t targetGuid,
|
||||
// would compress it, this gate excludes it exactly as the server's does.
|
||||
// (Drain Soul passes via its effect-2 periodic damage: aura=[86,3,0].)
|
||||
bool HasPeriodicDamageOrLeech(const uint8_t *rec) {
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
constexpr int32_t kPeriodicDamage = 3, kPeriodicLeech = 53;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int32_t aura = *reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME + i * 4);
|
||||
if (aura == kPeriodicDamage || aura == kPeriodicLeech)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return Spell::Lookup::HasAuraEffect(rec, kPeriodicDamage) ||
|
||||
Spell::Lookup::HasAuraEffect(rec, kPeriodicLeech);
|
||||
}
|
||||
|
||||
// Compression pairs by CASTER, mirroring the server (each warlock's Dark
|
||||
|
||||
@@ -457,12 +457,10 @@ static void RegisterLuaFunctions() {
|
||||
&Script_GetAutoCompleteResults, &kGetAutoCompleteResults);
|
||||
Game::Lua::RegisterTableFunction("C_AutoComplete", "IsRecognizedName", &Script_IsRecognizedName,
|
||||
&kIsRecognizedName);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "AutoCompleteEntryFlag", kEntryFlagEntries,
|
||||
sizeof(kEntryFlagEntries) / sizeof(kEntryFlagEntries[0]),
|
||||
"AutoComplete");
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "AutoCompletePriority", kPriorityEntries,
|
||||
sizeof(kPriorityEntries) / sizeof(kPriorityEntries[0]),
|
||||
"AutoComplete");
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "AutoCompleteEntryFlag", kEntryFlagEntries, "AutoComplete");
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "AutoCompletePriority", kPriorityEntries, "AutoComplete");
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
// 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/>.
|
||||
|
||||
// `debuglocals([level [, skipFunctionsAndUserdata]])` — the locals and then
|
||||
// the upvalues of a call level as text, one `name=value` line each, for an
|
||||
// error report. 4.3.4's Script_debuglocals and the FrameScript_PackVariable
|
||||
// it formats with, in retail's spacing (probed: `n=7`, `s="hi"`,
|
||||
// `t=<table>{`, ` k=1`, `}`; 4.3.4 writes `n = 7` and `t = <table> {`):
|
||||
// - Only while an error handler runs; otherwise it returns nothing. 4.3.4
|
||||
// tests a flag its error function sets around the handler call. 1.12's
|
||||
// error function sets none, but while a handler runs that function's
|
||||
// level is on the stack (ErrorCallstack::ErrorFunctionLevel).
|
||||
// - `level` counts as lua_getstack and debugstack do: 1 is the caller.
|
||||
// Less than 1 is 1.
|
||||
// - A table prints as `name=<table>{`, or with the frame's name for a
|
||||
// frame (t[0], as FrameScript_GetObject reads it), then its fields one
|
||||
// space in at the top level only, then `}`. A function prints where it
|
||||
// is defined. The text stops at 4095 bytes, each line at 1023.
|
||||
// Retail's second argument, which its Blizzard_ScriptErrors passes as true,
|
||||
// leaves out the function and userdata values, at the top level and in
|
||||
// tables.
|
||||
//
|
||||
// A number key in int range prints truncated with %d, as 4.3.4's
|
||||
// IsIntegerNumber tests only the range: 2.5 prints as 2. Two departures from
|
||||
// 4.3.4, both where it would fail inside the handler: a number key outside
|
||||
// that range is formatted from a copy, not converted in place (that breaks
|
||||
// lua_next), and a key that is not a number or string prints as its type in
|
||||
// angle brackets (4.3.4 formats a null string).
|
||||
//
|
||||
// Retail also lists `(*temporary)` lines: 5.1's lua_getlocal names each stack
|
||||
// slot past a level's locals so, a C level's arguments included (an
|
||||
// `error("x")` report shows `(*temporary)="x"`). 1.12's lua_getlocal
|
||||
// (0x6FBB20) is 5.0's, which names declared locals only. Those slots are
|
||||
// the compiler's registers, which 5.0 and 5.1 allocate differently, so they
|
||||
// are left out rather than imitated.
|
||||
//
|
||||
// Registered on both Lua states, like the base-library helpers.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/ErrorCallstack.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace BaseLib::DebugLocals {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
using lua_getlocal_t = const char *(__fastcall *)(void *L, const void *ar, int n);
|
||||
using lua_getupvalue_t = const char *(__fastcall *)(void *L, int funcindex, int n);
|
||||
|
||||
const auto GetLocal = reinterpret_cast<lua_getlocal_t>(Offsets::LUA_GET_LOCAL);
|
||||
const auto GetUpvalue = reinterpret_cast<lua_getupvalue_t>(Offsets::LUA_GET_UPVALUE);
|
||||
|
||||
constexpr size_t kTextMax = 4095; // 4.3.4's 4096-byte buffer, less its NUL
|
||||
constexpr size_t kLineSize = 1024;
|
||||
|
||||
struct Packer {
|
||||
void *L;
|
||||
bool skipFunctionsAndUserdata;
|
||||
std::string text;
|
||||
|
||||
void Line(const char *fmt, const char *indent, const char *name, const char *value = "",
|
||||
int number = 0) {
|
||||
char line[kLineSize];
|
||||
std::snprintf(line, sizeof line, fmt, indent, name, value, number);
|
||||
const size_t room = kTextMax - text.size();
|
||||
text.append(line, std::min(room, std::strlen(line)));
|
||||
}
|
||||
|
||||
// The value on the stack top, as 4.3.4's FrameScript_PackVariable.
|
||||
void Pack(const char *name, int depth) {
|
||||
const char *indent = depth > 0 ? " " : "";
|
||||
switch (Lua::Type(L, -1)) {
|
||||
case Lua::TYPE_NIL:
|
||||
Line("%s%s=nil\n", indent, name);
|
||||
break;
|
||||
case Lua::TYPE_BOOLEAN:
|
||||
Line("%s%s=%s\n", indent, name, Lua::ToBoolean(L, -1) ? "true" : "false");
|
||||
break;
|
||||
case Lua::TYPE_LIGHTUSERDATA:
|
||||
case Lua::TYPE_USERDATA:
|
||||
if (!skipFunctionsAndUserdata)
|
||||
Line("%s%s=<userdata>\n", indent, name);
|
||||
break;
|
||||
case Lua::TYPE_NUMBER:
|
||||
Line("%s%s=%s\n", indent, name, Lua::ToString(L, -1)); // a copy: safe to convert
|
||||
break;
|
||||
case Lua::TYPE_STRING:
|
||||
Line("%s%s=\"%s\"\n", indent, name, Lua::ToString(L, -1));
|
||||
break;
|
||||
case Lua::TYPE_TABLE:
|
||||
PackTable(name, depth, indent);
|
||||
break;
|
||||
case Lua::TYPE_FUNCTION:
|
||||
if (!skipFunctionsAndUserdata)
|
||||
PackFunction(name, indent);
|
||||
break;
|
||||
default:
|
||||
Line("%s%s=<%s>\n", indent, name, Lua::TypeName(L, Lua::Type(L, -1)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PackTable(const char *name, int depth, const char *indent) {
|
||||
void *object = Lua::ResolveObject(L, -1);
|
||||
Line("%s%s=%s{\n", indent, name, object ? Lua::ObjectDisplayName(object) : "<table>");
|
||||
if (depth == 0) {
|
||||
Lua::PushNil(L);
|
||||
while (Lua::Next(L, -2) != 0) {
|
||||
char key[32];
|
||||
const char *keyName = key;
|
||||
const int keyType = Lua::Type(L, -2);
|
||||
const double number = keyType == Lua::TYPE_NUMBER ? Lua::ToNumber(L, -2) : 0;
|
||||
if (keyType == Lua::TYPE_NUMBER && std::isfinite(number) && number >= -2147483648.0 &&
|
||||
number <= 2147483647.0) {
|
||||
std::snprintf(key, sizeof key, "%d", static_cast<int>(number));
|
||||
} else if (keyType == Lua::TYPE_STRING) {
|
||||
keyName = Lua::ToString(L, -2);
|
||||
} else if (keyType == Lua::TYPE_NUMBER) {
|
||||
Lua::PushValue(L, -2);
|
||||
std::snprintf(key, sizeof key, "%s", Lua::ToString(L, -1));
|
||||
Lua::SetTop(L, -2);
|
||||
} else {
|
||||
std::snprintf(key, sizeof key, "<%s>", Lua::TypeName(L, keyType));
|
||||
}
|
||||
Pack(keyName, 1);
|
||||
Lua::SetTop(L, -2); // the value; the key stays for lua_next
|
||||
}
|
||||
}
|
||||
Line("%s}\n", indent, "");
|
||||
}
|
||||
|
||||
void PackFunction(const char *name, const char *indent) {
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
Lua::PushValue(L, -1);
|
||||
Lua::GetInfo(L, ">nS", ar); // pops the copy
|
||||
const auto field = [&ar](uintptr_t offset) { return Game::Read<const char *>(ar, offset); };
|
||||
const char *source = field(Offsets::OFF_LUA_DEBUG_SOURCE);
|
||||
const int line = Game::Read<int>(ar, Offsets::OFF_LUA_DEBUG_LINEDEFINED);
|
||||
if (const char *fn = field(Offsets::OFF_LUA_DEBUG_NAME)) {
|
||||
char named[kLineSize];
|
||||
std::snprintf(named, sizeof named, "%s() defined %s", fn, source ? source : "?");
|
||||
Line("%s%s=%s:%d\n", indent, name, named, line);
|
||||
} else {
|
||||
Line("%s%s=<function> defined %s:%d\n", indent, name, source ? source : "?", line);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
int __fastcall Script_debuglocals(void *L) {
|
||||
if (ErrorCallstack::ErrorFunctionLevel(L) == 0)
|
||||
return 0;
|
||||
int level = static_cast<int>(Lua::ToNumber(L, 1));
|
||||
if (level <= 0)
|
||||
level = 1;
|
||||
Packer packer{L, Lua::ToBoolean(L, 2) != 0, {}};
|
||||
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
if (Lua::GetStack(L, level, ar)) {
|
||||
const char *name;
|
||||
for (int i = 1; (name = GetLocal(L, ar, i)) != nullptr; i++) {
|
||||
packer.Pack(name, 0);
|
||||
Lua::SetTop(L, -2);
|
||||
}
|
||||
Lua::GetInfo(L, "f", ar); // the level's function
|
||||
for (int i = 1; (name = GetUpvalue(L, -1, i)) != nullptr; i++) {
|
||||
packer.Pack(name, 0);
|
||||
Lua::SetTop(L, -2);
|
||||
}
|
||||
Lua::SetTop(L, -2);
|
||||
}
|
||||
Lua::PushLString(L, packer.text.data(), static_cast<unsigned int>(packer.text.size()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kDebuglocalsArgs[] = {
|
||||
Game::Doc::Opt("level", "number", "1", "The call level, as debugstack counts it."),
|
||||
Game::Doc::Opt("skipFunctionsAndUserdata", "boolean", "false",
|
||||
"Leaves out function and userdata values."),
|
||||
};
|
||||
const Game::Doc::Field kDebuglocalsRets[] = {
|
||||
Game::Doc::Opt("locals", "string", nullptr,
|
||||
"One `name=value` line for each local and upvalue. Nothing outside an "
|
||||
"error handler."),
|
||||
};
|
||||
const Game::Doc::Function kDebuglocals{
|
||||
"The local variables and upvalues of a call level as text, for an error report.",
|
||||
kDebuglocalsArgs, kDebuglocalsRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("debuglocals", &Script_debuglocals, &kDebuglocals);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("debuglocals", &Script_debuglocals, &kDebuglocals);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::DebugLocals
|
||||
@@ -0,0 +1,102 @@
|
||||
// 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/>.
|
||||
|
||||
// `debugstack([thread,] [start [, count1 [, count2]]])` — the stack of a
|
||||
// coroutine, as 4.3.4's Script_debugstack takes it: with a thread first,
|
||||
// the levels are that thread's and `start` defaults to 0 (the thread's
|
||||
// innermost level) instead of 1.
|
||||
//
|
||||
// 1.12's debugstack (FUN_SCRIPT_DEBUGSTACK) has no thread argument, and a
|
||||
// thread falls through its argument checks so that it returns the thread
|
||||
// itself: `msg .. debugstack(co)` raises "attempt to concatenate a thread
|
||||
// value" in the coroutine error paths of WeakAuras, Questie's ThreadLib,
|
||||
// MRT and Zygor.
|
||||
//
|
||||
// The engine's function still does the work, so its text and its head and
|
||||
// tail windows stay exactly as they are. It reads levels through six calls
|
||||
// (five lua_getstack, one lua_getinfo), and those calls are redirected to
|
||||
// read from the state Script_debugstack names for the call: the thread, or
|
||||
// the calling state when there is none. Script_debugstack replaces the
|
||||
// global and is the engine function's only way in, so the state is set for
|
||||
// every call; an error part way cannot leave a thread behind for the next.
|
||||
//
|
||||
// Registered on both Lua states, like the base-library helpers.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
|
||||
namespace BaseLib::DebugStack {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
// The state whose levels the engine's debugstack reads, or null for its own.
|
||||
void *g_levelsOf = nullptr;
|
||||
|
||||
int __fastcall GetStack(void *L, int level, void *ar) {
|
||||
return Lua::GetStack(g_levelsOf ? g_levelsOf : L, level, ar);
|
||||
}
|
||||
|
||||
int __fastcall GetInfo(void *L, const char *what, void *ar) {
|
||||
return Lua::GetInfo(g_levelsOf ? g_levelsOf : L, what, ar);
|
||||
}
|
||||
|
||||
int __fastcall Script_debugstack(void *L) {
|
||||
const auto engineDebugStack =
|
||||
reinterpret_cast<Lua::CFunction>(Offsets::FUN_SCRIPT_DEBUGSTACK);
|
||||
g_levelsOf = nullptr;
|
||||
if (Lua::Type(L, 1) == Lua::TYPE_THREAD) {
|
||||
g_levelsOf = Lua::ToThread(L, 1);
|
||||
Lua::Remove(L, 1);
|
||||
if (!Lua::IsNumber(L, 1)) { // start defaults to 0 for a thread
|
||||
Lua::PushNumber(L, 0);
|
||||
Lua::Insert(L, 1);
|
||||
}
|
||||
}
|
||||
const int results = engineDebugStack(L);
|
||||
g_levelsOf = nullptr;
|
||||
return results;
|
||||
}
|
||||
|
||||
void Apply() {
|
||||
const auto getStack = reinterpret_cast<const void *>(&GetStack);
|
||||
for (const uintptr_t site :
|
||||
{Offsets::CALL_DEBUGSTACK_GETSTACK_FIRST, Offsets::CALL_DEBUGSTACK_GETSTACK_TAIL_PROBE,
|
||||
Offsets::CALL_DEBUGSTACK_GETSTACK_TAIL_START, Offsets::CALL_DEBUGSTACK_GETSTACK_TAIL_SKIP,
|
||||
Offsets::CALL_DEBUGSTACK_GETSTACK_NEXT})
|
||||
Common::RedirectCall(site, Offsets::LUA_GET_STACK, getStack);
|
||||
Common::RedirectCall(Offsets::CALL_DEBUGSTACK_GETINFO, Offsets::LUA_GET_INFO,
|
||||
reinterpret_cast<const void *>(&GetInfo));
|
||||
}
|
||||
|
||||
void RegisterInGame() {
|
||||
Apply();
|
||||
Lua::RegisterGlobalFunction("debugstack", &Script_debugstack);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Apply();
|
||||
Lua::RegisterGlueFunction("debugstack", &Script_debugstack);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::DebugStack
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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/>.
|
||||
|
||||
// `difftime(t2 [, t1])` — the seconds from `t1` (0 by default) to `t2`, the
|
||||
// companion of the engine's `date` and `time` globals. 4.3.4's
|
||||
// Script_difftime: C `difftime` over the two arguments as time_t.
|
||||
//
|
||||
// Registered on BOTH states, as the engine registers `date` and `time`.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <ctime>
|
||||
|
||||
namespace BaseLib::DiffTime {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_difftime(void *L) {
|
||||
const auto t2 = static_cast<time_t>(Game::Lua::CheckNumber(L, 1));
|
||||
const auto t1 = static_cast<time_t>(Game::Lua::OptNumber(L, 2, 0));
|
||||
Game::Lua::PushNumber(L, std::difftime(t2, t1));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kDifftimeArgs[] = {
|
||||
Game::Doc::Req("time2", "number", "A time in seconds, as `time()` returns."),
|
||||
Game::Doc::Opt("time1", "number", "0"),
|
||||
};
|
||||
const Game::Doc::Field kDifftimeRets[] = {
|
||||
Game::Doc::Req("seconds", "number", "time2 minus time1."),
|
||||
};
|
||||
const Game::Doc::Function kDifftime{"The number of seconds from time1 to time2.",
|
||||
kDifftimeArgs, kDifftimeRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("difftime", &Script_difftime, &kDifftime);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("difftime", &Script_difftime, &kDifftime);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::DiffTime
|
||||
@@ -40,12 +40,26 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/ErrorCallstack.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace BaseLib::ErrorCallstack {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::optional<int> g_setHeight;
|
||||
@@ -59,21 +73,6 @@ int Height(void *L) {
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// 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 BaseLib::ErrorCallstack {
|
||||
|
||||
// The innermost call level whose function is the engine's error function
|
||||
// (FUN_FRAMESCRIPT_CALL_ERROR_HANDLER), or 0 when no error is being
|
||||
// reported. Nonzero exactly while an error handler runs.
|
||||
int ErrorFunctionLevel(void *L);
|
||||
|
||||
} // namespace BaseLib::ErrorCallstack
|
||||
+22
-1
@@ -16,13 +16,16 @@
|
||||
// - `math.fmod(x, y)` — floating-point remainder of `x / y`.
|
||||
// - `math.modf(x)` — split into integral + fractional parts.
|
||||
// - `math.huge` — positive infinity constant.
|
||||
// - `math.cosh(x)`, `math.sinh(x)`, `math.tanh(x)` — hyperbolic functions.
|
||||
//
|
||||
// `math.fmod` is a pure rename: Lua 5.0 ships the exact same C function under
|
||||
// the name `math.mod`; 5.1 renamed the Lua-facing entry to `math.fmod`. We
|
||||
// register it as a direct alias of the engine's `math.mod` C function
|
||||
// (`FUN_LUA_MATH_FMOD`) — no wrapper. `math.mod` stays available too, so code
|
||||
// written for either name works. `modf` and `huge` are genuinely new in 5.1
|
||||
// (not in 1.12's mathlib table), so we implement/define them here.
|
||||
// (not in 1.12's mathlib table), so we implement/define them here, as are the
|
||||
// hyperbolic functions (5.1's math_cosh/sinh/tanh: luaL_checknumber, the C
|
||||
// function, one number).
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -53,9 +56,27 @@ int __fastcall Script_math_modf(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
int __fastcall Script_math_cosh(void *L) {
|
||||
Game::Lua::PushNumber(L, std::cosh(Game::Lua::CheckNumber(L, 1)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_math_sinh(void *L) {
|
||||
Game::Lua::PushNumber(L, std::sinh(Game::Lua::CheckNumber(L, 1)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_math_tanh(void *L) {
|
||||
Game::Lua::PushNumber(L, std::tanh(Game::Lua::CheckNumber(L, 1)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("math", "fmod", Script_math_fmod);
|
||||
Game::Lua::RegisterTableFunction("math", "modf", &Script_math_modf);
|
||||
Game::Lua::RegisterTableFunction("math", "cosh", &Script_math_cosh);
|
||||
Game::Lua::RegisterTableFunction("math", "sinh", &Script_math_sinh);
|
||||
Game::Lua::RegisterTableFunction("math", "tanh", &Script_math_tanh);
|
||||
|
||||
// `math.huge` — a numeric constant, not a function. Fetch the (now
|
||||
// guaranteed-present) `math` table and set the field directly.
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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/>.
|
||||
|
||||
// `newproxy([false | true | proxy])` — Lua 5.1's empty userdata (luaB_newproxy,
|
||||
// lbaselib.c). `false` or nothing: no metatable. `true`: a new metatable of
|
||||
// its own. Another proxy: share that proxy's metatable. Only metatables made
|
||||
// by `newproxy(true)` count; any other argument raises "boolean or proxy
|
||||
// expected". 5.0 has userdata and their `__gc`, only not this function.
|
||||
//
|
||||
// As in 5.1, the closure's upvalue is a weak table that marks the valid
|
||||
// metatables (weak[m] = true), and is its own metatable with __mode "kv".
|
||||
//
|
||||
// Registered on BOTH states (language-level helper, both are 5.0).
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace BaseLib::NewProxy {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
int __fastcall Script_newproxy(void *L) {
|
||||
Lua::SetTop(L, 1);
|
||||
Lua::NewUserdata(L, 0); // the proxy, at 2
|
||||
if (Lua::ToBoolean(L, 1) == 0)
|
||||
return 1; // no metatable
|
||||
if (Lua::Type(L, 1) == Lua::TYPE_BOOLEAN) {
|
||||
Lua::NewTable(L); // a new metatable m ...
|
||||
Lua::PushValue(L, -1);
|
||||
Lua::PushBoolean(L, 1);
|
||||
Lua::RawSet(L, Lua::UpvalueIndex(1)); // ... marked valid: weak[m] = true
|
||||
} else {
|
||||
bool valid = false;
|
||||
if (Lua::GetMetatable(L, 1)) {
|
||||
Lua::RawGet(L, Lua::UpvalueIndex(1));
|
||||
valid = Lua::ToBoolean(L, -1) != 0;
|
||||
Lua::SetTop(L, -2);
|
||||
}
|
||||
if (!valid)
|
||||
Lua::ArgError(L, 1, "boolean or proxy expected");
|
||||
Lua::GetMetatable(L, 1);
|
||||
}
|
||||
Lua::SetMetatable(L, 2);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Register() {
|
||||
void *L = Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
Lua::PushString(L, "newproxy");
|
||||
Lua::NewTable(L); // the weak table w
|
||||
Lua::PushString(L, "__mode");
|
||||
Lua::PushString(L, "kv");
|
||||
Lua::RawSet(L, -3);
|
||||
Lua::PushValue(L, -1);
|
||||
Lua::SetMetatable(L, -2); // w is its own metatable
|
||||
Lua::PushCClosure(L, &Script_newproxy, 1);
|
||||
Lua::SetTable(L, Lua::GLOBALS_INDEX);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::NewProxy
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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/>.
|
||||
|
||||
// `scrub(...)` — returns its arguments, each one that is not a boolean,
|
||||
// number or string replaced by nil. 4.3.4's Script_scrub.
|
||||
//
|
||||
// Registered on both Lua states, like the base-library helpers.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace BaseLib::Scrub {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
int __fastcall Script_scrub(void *L) {
|
||||
const int top = Lua::GetTop(L);
|
||||
Lua::CheckStack(L, 1);
|
||||
for (int i = 1; i <= top; i++) {
|
||||
const int type = Lua::Type(L, i);
|
||||
if (type != Lua::TYPE_BOOLEAN && type != Lua::TYPE_NUMBER && type != Lua::TYPE_STRING) {
|
||||
Lua::PushNil(L);
|
||||
Lua::Replace(L, i);
|
||||
}
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kScrubArgs[] = {
|
||||
Game::Doc::Vararg("any"),
|
||||
};
|
||||
const Game::Doc::Field kScrubRets[] = {
|
||||
Game::Doc::Vararg("any", "Each argument, or nil when it is not a boolean, number or string."),
|
||||
};
|
||||
const Game::Doc::Function kScrub{
|
||||
"Returns its arguments, with each one that is not a boolean, number or string replaced by nil.",
|
||||
kScrubArgs, kScrubRets, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("scrub", &Script_scrub, &kScrub);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("scrub", &Script_scrub, &kScrub);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::Scrub
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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/>.
|
||||
|
||||
// `string.format` takes two-digit argument numbers (`%10$s`).
|
||||
//
|
||||
// 1.12's str_format (0x7FD3D0) is Lua 4.0's: a spec may start with `n$`, but
|
||||
// only one digit, `isdigit(p[0]) && p[1] == '$'`. In "%10$s" the "10" is then
|
||||
// read as a width and the '$' as the conversion, which raises "invalid
|
||||
// option in `format'". 4.3.4's str_format adds a second branch for two
|
||||
// digits, `isdigit(p[1]) && p[2] == '$'`, and its combat log strings need it
|
||||
// (ACTION_SPELL_PERIODIC_LEECH_RESULT is "(%10$s %8$s Gained)").
|
||||
//
|
||||
// The thunk below stands in for the engine's one-digit branch and adds the
|
||||
// two-digit one. Everything else stays the engine's: its locale isdigit on
|
||||
// the first digit (already tested before the site), `arg++` after the site,
|
||||
// the flag/width/precision scan from EBX, and every conversion. Every caller
|
||||
// goes through it: `string.format`, `format`, and the C callers of
|
||||
// FUN_LUA_STR_FORMAT (Frame::FormattedText, Text::NumericFormatter).
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace BaseLib::StringFormat {
|
||||
|
||||
namespace {
|
||||
|
||||
static_assert(Offsets::LUA_STR_FORMAT_FRAME_ARG == 8, "ArgNumberThunk writes [ebp - 8]");
|
||||
|
||||
// ESI = the spec after '%', whose first byte is a digit. Sets the argument
|
||||
// number in [ebp - 8] and moves EBX past the `n$`; leaves both alone when
|
||||
// the spec has no `n$`.
|
||||
__declspec(naked) void ArgNumberThunk() {
|
||||
__asm {
|
||||
cmp byte ptr [esi + 1], '$'
|
||||
jne twoDigits
|
||||
movsx eax, byte ptr [esi]
|
||||
sub eax, '0'
|
||||
mov dword ptr [ebp - 8], eax
|
||||
lea ebx, [esi + 2]
|
||||
ret
|
||||
twoDigits:
|
||||
movzx eax, byte ptr [esi + 1]
|
||||
sub eax, '0'
|
||||
cmp eax, 9
|
||||
ja done
|
||||
cmp byte ptr [esi + 2], '$'
|
||||
jne done
|
||||
movsx ecx, byte ptr [esi]
|
||||
sub ecx, '0'
|
||||
lea ecx, [ecx + ecx * 4]
|
||||
lea eax, [eax + ecx * 2]
|
||||
mov dword ptr [ebp - 8], eax
|
||||
lea ebx, [esi + 3]
|
||||
done:
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// Runs on every registration pass; a site already patched no longer matches
|
||||
// and is skipped.
|
||||
void Apply() {
|
||||
static const uint8_t kOneDigit[18] = {0x80, 0x7E, 0x01, 0x24, 0x75, 0x0C, 0x0F, 0xBE, 0x06,
|
||||
0x83, 0xE8, 0x30, 0x89, 0x45, 0xF8, 0x8D, 0x5E, 0x02};
|
||||
Common::ReplaceWithCall(Offsets::SITE_LUA_STR_FORMAT_ARG_NUMBER, kOneDigit, sizeof kOneDigit,
|
||||
reinterpret_cast<const void *>(&ArgNumberThunk));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Apply};
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&Apply};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::StringFormat
|
||||
@@ -0,0 +1,163 @@
|
||||
// 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/>.
|
||||
|
||||
// `string.gsub` takes 5.1's string and table replacements.
|
||||
//
|
||||
// 1.12's str_gsub (0x7FD0E0) is Lua 5.0's, and differs from 5.1 in its
|
||||
// replacement argument:
|
||||
// - it takes a string or a function, not a table;
|
||||
// - `%0` in a replacement string, and `%1` when the pattern has no
|
||||
// captures, raise "invalid capture index" (5.1: the whole match);
|
||||
// - a function that returns nil or false deletes the match (5.1 keeps it),
|
||||
// and one that returns a table deletes it too (5.1 raises an error).
|
||||
//
|
||||
// The first two only ever raised errors, so 5.1's behavior cannot break a
|
||||
// 1.12 addon. The third does: SuperAPI's CombatText_AddMessage returns nil
|
||||
// to delete the player's own " [0x...]" GUID from heal text. A function
|
||||
// replacement therefore stays the engine's own add_s.
|
||||
//
|
||||
// str_gsub itself stays: its match loop, anchor, count and buffer are the
|
||||
// same in both. Two parts are replaced:
|
||||
// - the argument gate (SITE_LUA_STR_GSUB_REPL_CHECK) becomes CheckReplacement;
|
||||
// - the one add_s call (CALL_LUA_STR_GSUB_ADD_S) goes to AddValue: 5.1's
|
||||
// add_s for a string or number, 5.1's table lookup for a table, and the
|
||||
// engine's add_s for a function. They use the engine's capture helpers.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace BaseLib::StringGsub {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
// Lua 5.0's MatchState, as str_gsub builds it on its stack (str_format's
|
||||
// spec scan reads the same layout: level at +0x0C, capture[0].len at +0x14).
|
||||
struct MatchState {
|
||||
const char *srcInit;
|
||||
const char *srcEnd;
|
||||
void *L;
|
||||
int level;
|
||||
struct {
|
||||
const char *init;
|
||||
int len;
|
||||
} capture[32];
|
||||
};
|
||||
|
||||
using add_s_t = void(__fastcall *)(MatchState *ms, void *b, const char *s, const char *e);
|
||||
using push_onecapture_t = void(__fastcall *)(MatchState *ms, int i);
|
||||
using luaL_addlstring_t = void(__fastcall *)(void *b, const char *s, unsigned int len);
|
||||
using luaL_addvalue_t = void(__fastcall *)(void *b);
|
||||
|
||||
const auto EngineAddS = reinterpret_cast<add_s_t>(Offsets::FUN_LUA_STR_ADD_S);
|
||||
const auto EnginePushOneCapture =
|
||||
reinterpret_cast<push_onecapture_t>(Offsets::FUN_LUA_STR_PUSH_ONECAPTURE);
|
||||
const auto AddLString = reinterpret_cast<luaL_addlstring_t>(Offsets::LUAL_ADD_LSTRING);
|
||||
const auto AddBufferValue = reinterpret_cast<luaL_addvalue_t>(Offsets::LUAL_ADD_VALUE);
|
||||
|
||||
// 5.1's push_onecapture: capture `i`, or the whole match for `i == 0` when
|
||||
// the pattern has no captures.
|
||||
void PushOneCapture(MatchState *ms, int i, const char *s, const char *e) {
|
||||
if (i < ms->level)
|
||||
EnginePushOneCapture(ms, i);
|
||||
else if (i == 0)
|
||||
Lua::PushLString(ms->L, s, static_cast<unsigned int>(e - s));
|
||||
else
|
||||
Lua::Error(ms->L, "invalid capture index");
|
||||
}
|
||||
|
||||
// 5.1's add_s: a replacement string, where `%0` is the whole match and `%1`
|
||||
// to `%9` are captures.
|
||||
void AddString(MatchState *ms, void *b, const char *s, const char *e) {
|
||||
void *L = ms->L;
|
||||
const char *news = Lua::ToString(L, 3);
|
||||
const unsigned int l = Lua::StrLen(L, 3);
|
||||
for (unsigned int i = 0; i < l; i++) {
|
||||
if (news[i] != '%') {
|
||||
AddLString(b, news + i, 1);
|
||||
continue;
|
||||
}
|
||||
i++; // skip the escape
|
||||
const char c = news[i];
|
||||
if (c < '0' || c > '9') {
|
||||
AddLString(b, news + i, 1);
|
||||
} else if (c == '0') {
|
||||
AddLString(b, s, static_cast<unsigned int>(e - s));
|
||||
} else {
|
||||
PushOneCapture(ms, c - '1', s, e);
|
||||
AddBufferValue(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5.1's add_value for a string or table, the engine's add_s for a function.
|
||||
// Called as add_s: __fastcall, ret 8.
|
||||
void __fastcall AddValue(MatchState *ms, void *b, const char *s, const char *e) {
|
||||
void *L = ms->L;
|
||||
switch (Lua::Type(L, 3)) {
|
||||
case Lua::TYPE_FUNCTION:
|
||||
// A result that is not a string or number deletes the match.
|
||||
EngineAddS(ms, b, s, e);
|
||||
return;
|
||||
case Lua::TYPE_TABLE:
|
||||
break;
|
||||
default: // number or string
|
||||
AddString(ms, b, s, e);
|
||||
return;
|
||||
}
|
||||
PushOneCapture(ms, 0, s, e);
|
||||
Lua::GetTable(L, 3);
|
||||
if (!Lua::ToBoolean(L, -1)) {
|
||||
Lua::SetTop(L, -2);
|
||||
Lua::PushLString(L, s, static_cast<unsigned int>(e - s)); // keep the match
|
||||
} else if (!Lua::IsString(L, -1)) {
|
||||
Lua::Error(L, "invalid replacement value (a %s)", Lua::TypeName(L, Lua::Type(L, -1)));
|
||||
}
|
||||
AddBufferValue(b);
|
||||
}
|
||||
|
||||
// 5.1's gate on the replacement argument. Called in place of the engine's,
|
||||
// with ECX = L.
|
||||
void __fastcall CheckReplacement(void *L) {
|
||||
const int t = Lua::Type(L, 3);
|
||||
if (t != Lua::TYPE_NUMBER && t != Lua::TYPE_STRING && t != Lua::TYPE_FUNCTION &&
|
||||
t != Lua::TYPE_TABLE)
|
||||
Lua::ArgError(L, 3, "string/function/table expected");
|
||||
}
|
||||
|
||||
// Runs on every registration pass; a site already patched no longer matches
|
||||
// and is skipped.
|
||||
void Apply() {
|
||||
static const uint8_t kGate[60] = {
|
||||
0xE8, 0x0D, 0x5F, 0xEF, 0xFF, 0x83, 0xF8, 0x03, 0x7C, 0x21, 0xBA, 0x03,
|
||||
0x00, 0x00, 0x00, 0x8B, 0xCF, 0xE8, 0x9C, 0x63, 0xEF, 0xFF, 0x85, 0xC0,
|
||||
0x75, 0x22, 0xBA, 0x03, 0x00, 0x00, 0x00, 0x8B, 0xCF, 0xE8, 0x7C, 0x62,
|
||||
0xEF, 0xFF, 0x83, 0xF8, 0x06, 0x74, 0x11, 0x68, 0x84, 0x23, 0x88, 0x00,
|
||||
0xBA, 0x03, 0x00, 0x00, 0x00, 0x8B, 0xCF, 0xE8, 0x76, 0x76, 0xEF, 0xFF,
|
||||
};
|
||||
Common::ReplaceWithCall(Offsets::SITE_LUA_STR_GSUB_REPL_CHECK, kGate, sizeof kGate,
|
||||
reinterpret_cast<const void *>(&CheckReplacement));
|
||||
Common::RedirectCall(Offsets::CALL_LUA_STR_GSUB_ADD_S, Offsets::FUN_LUA_STR_ADD_S,
|
||||
reinterpret_cast<const void *>(&AddValue));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Apply};
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&Apply};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::StringGsub
|
||||
+38
-124
@@ -15,13 +15,13 @@
|
||||
//
|
||||
// - `string.match(s, pattern [, init])` — first-match extraction (5.1),
|
||||
// also the global `strmatch`, as `string.find` is `strfind`.
|
||||
// - `string.gmatch(s, pattern)` — match iterator (5.1).
|
||||
// - `string.gsub` TABLE replacement — 5.1 form (see the shim below).
|
||||
// - `string.gmatch(s, pattern)` — match iterator (5.1), also `gmatch`.
|
||||
// - `strsplit(sep, str [, pieces])` — WoW global; split on any char.
|
||||
// - `strjoin(delimiter, ...)` — WoW global; join varargs.
|
||||
// - `strtrim(str [, chars])` — WoW global; trim a char set.
|
||||
// - `strreplace(str, find, replace)` — plain-text replace (see below).
|
||||
// - `string.reverse(s)` / `strrev(s)` — 5.1 string function, missing in 5.0.
|
||||
// - `string.byte(s, i, j)` / `strbyte` — 5.1's range form (5.0 reads only i).
|
||||
// - `strlenutf8(s)` — WoW global; characters, not bytes.
|
||||
// - `strcmputf8i(a, b)` — WoW global; case-insensitive UTF-8 order.
|
||||
// - `strsplittable(sep, str [, pieces])`— WoW global; `strsplit` into a table.
|
||||
@@ -341,126 +341,39 @@ int __fastcall Script_string_reverse(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- string.gsub table-replacement upgrade (Lua 5.1) ------------------------
|
||||
//
|
||||
// 5.1 allows a TABLE as gsub's replacement: each match looks up the first
|
||||
// capture (the whole match when the pattern declares no captures) in the
|
||||
// table; a nil/false value keeps the original match; string/number values
|
||||
// substitute. This engine's gsub (FUN_LUA_STR_GSUB) is stock 5.0 —
|
||||
// string/function only ("string or function expected" otherwise), and its
|
||||
// add_value appends NOTHING when a replacement function returns a
|
||||
// non-string (match → empty; verified by decode, see Offsets.h). So a
|
||||
// plain table→closure adapter would EAT unmatched tokens where 5.1 keeps
|
||||
// them, and the closure can't reconstruct the whole match from capture
|
||||
// args alone.
|
||||
//
|
||||
// The shim therefore rewrites the pattern with one OUTER capture —
|
||||
// `pat` → `(pat)`, keeping a leading `^` and an unescaped trailing `$`
|
||||
// outside (both are anchors only at their positional ends; inside the
|
||||
// parens they'd turn literal) — so the adapter closure always receives
|
||||
// the whole match as arg 1, the pattern's own first capture as arg 2, and
|
||||
// can hand arg 1 back to reproduce 5.1's keep-match for missing keys.
|
||||
// Patterns containing a `%1`..`%9` back-reference can't be wrapped (the
|
||||
// outer capture would renumber what `%N` refers to); those fall back to
|
||||
// an unwrapped adapter whose missing-key behavior is the engine's own
|
||||
// (match → empty) — the one documented divergence, and the combination
|
||||
// (table replacement + pattern back-references) is essentially absent
|
||||
// from real code. String/function replacements tail-call the engine gsub
|
||||
// untouched.
|
||||
// `string.byte(s [, i [, j]])` / `strbyte` — the codes of bytes i..j, as
|
||||
// 4.3.4's str_byte (j defaults to i). 1.12's (0x7FC210) is 5.0's: it reads
|
||||
// only `i` and pushes one code. With `j` absent the two give the same
|
||||
// result for every `i`. Positions truncate toward zero as the engine's
|
||||
// str_byte and str_sub do. The engine's lua_checkstack (0x6F2F30) refuses
|
||||
// more than 2048 stack slots, so the longest range is about 2000 bytes.
|
||||
|
||||
// True when the pattern contains a `%1`..`%9` back-reference. Escape-aware:
|
||||
// the char after any `%` is consumed, so `%%1` (literal percent, then '1')
|
||||
// doesn't count.
|
||||
bool PatternHasBackref(const char *p, unsigned len) {
|
||||
for (unsigned i = 0; i + 1 < len; ++i) {
|
||||
if (p[i] != '%')
|
||||
continue;
|
||||
const char c = p[i + 1];
|
||||
if (c >= '1' && c <= '9')
|
||||
return true;
|
||||
++i; // skip the escaped char
|
||||
}
|
||||
return false;
|
||||
// 5.1's posrelat: a negative position counts from the end; before the start is 0.
|
||||
long PosRelat(long pos, long len) {
|
||||
if (pos < 0)
|
||||
pos += len + 1;
|
||||
return pos >= 0 ? pos : 0;
|
||||
}
|
||||
|
||||
// Adapter for the wrapped-pattern path: arg 1 = whole match, arg 2 = the
|
||||
// pattern's own first capture (when it declared any). Upvalue 1 = the
|
||||
// replacement table. Missing/false value → return the whole match (5.1's
|
||||
// keep-match).
|
||||
int __fastcall GsubTableLookupWrapped(void *L) {
|
||||
const int nargs = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushValue(L, nargs >= 2 ? 2 : 1); // the 5.1 lookup key
|
||||
Game::Lua::GetTable(L, Game::Lua::UpvalueIndex(1)); // tbl[key]
|
||||
const int t = Game::Lua::Type(L, -1);
|
||||
if (t == Game::Lua::TYPE_NIL ||
|
||||
(t == Game::Lua::TYPE_BOOLEAN && Game::Lua::ToBoolean(L, -1) == 0))
|
||||
Game::Lua::PushValue(L, 1); // keep the original match
|
||||
return 1; // top value is the result
|
||||
}
|
||||
|
||||
// Adapter for back-reference patterns (unwrapped): arg 1 is already the
|
||||
// 5.1 key. A missing key returns nil → the engine appends nothing.
|
||||
int __fastcall GsubTableLookupPlain(void *L) {
|
||||
Game::Lua::PushValue(L, 1);
|
||||
Game::Lua::GetTable(L, Game::Lua::UpvalueIndex(1));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_string_gsub(void *L) {
|
||||
const auto engineGsub =
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_STR_GSUB);
|
||||
if (Game::Lua::Type(L, 3) != Game::Lua::TYPE_TABLE)
|
||||
return engineGsub(L); // string/function/error paths untouched
|
||||
|
||||
const char *pat = Game::Lua::ToString(L, 2);
|
||||
if (pat == nullptr)
|
||||
return engineGsub(L); // let the engine raise its own arg error
|
||||
const unsigned patLen = Game::Lua::StrLen(L, 2);
|
||||
|
||||
Game::Lua::SetTop(L, 4); // normalize to (s, pat, tbl, n) — gsub reads ≤4
|
||||
|
||||
if (!PatternHasBackref(pat, patLen)) {
|
||||
// Build `(body)` with the anchors outside the capture.
|
||||
unsigned b = 0, e = patLen;
|
||||
std::string wrapped;
|
||||
wrapped.reserve(patLen + 3);
|
||||
if (e > b && pat[0] == '^') {
|
||||
wrapped += '^';
|
||||
b = 1;
|
||||
}
|
||||
bool anchoredBack = false;
|
||||
if (e > b && pat[e - 1] == '$') {
|
||||
// `$` is an anchor only when not %-escaped: an even number of
|
||||
// `%` immediately before it means the `$` itself is unescaped.
|
||||
unsigned pc = 0;
|
||||
for (int idx = static_cast<int>(e) - 2;
|
||||
idx >= static_cast<int>(b) && pat[idx] == '%'; --idx)
|
||||
++pc;
|
||||
anchoredBack = (pc % 2) == 0;
|
||||
if (anchoredBack)
|
||||
--e;
|
||||
}
|
||||
wrapped += '(';
|
||||
wrapped.append(pat + b, e - b);
|
||||
wrapped += ')';
|
||||
if (anchoredBack)
|
||||
wrapped += '$';
|
||||
|
||||
Game::Lua::PushLString(L, wrapped.data(),
|
||||
static_cast<unsigned int>(wrapped.size())); // 5
|
||||
Game::Lua::PushValue(L, 3); // 6: tbl
|
||||
Game::Lua::PushCClosure(L, &GsubTableLookupWrapped, 1); // 6: closure
|
||||
Game::Lua::Insert(L, 2); // closure→2: s, closure, pat, tbl, n, wrapped
|
||||
Game::Lua::Insert(L, 2); // wrapped→2: s, wrapped, closure, pat, tbl, n
|
||||
Game::Lua::Remove(L, 4); // drop the original pattern
|
||||
Game::Lua::Remove(L, 4); // drop the table → (s, wrapped, closure, n)
|
||||
} else {
|
||||
Game::Lua::PushValue(L, 3); // 5: tbl
|
||||
Game::Lua::PushCClosure(L, &GsubTableLookupPlain, 1); // 5: closure
|
||||
Game::Lua::Insert(L, 3); // → s, pat, closure, tbl, n
|
||||
Game::Lua::Remove(L, 4); // → s, pat, closure, n
|
||||
}
|
||||
return engineGsub(L);
|
||||
int __fastcall Script_string_byte(void *L) {
|
||||
unsigned int l = 0;
|
||||
const char *s = Game::Lua::CheckLString(L, 1, &l);
|
||||
const long len = static_cast<long>(l);
|
||||
long posi = PosRelat(static_cast<long>(Game::Lua::OptNumber(L, 2, 1)), len);
|
||||
long pose =
|
||||
PosRelat(static_cast<long>(Game::Lua::OptNumber(L, 3, static_cast<double>(posi))), len);
|
||||
if (posi <= 0)
|
||||
posi = 1;
|
||||
if (pose > len)
|
||||
pose = len;
|
||||
if (posi > pose)
|
||||
return 0;
|
||||
const int n = static_cast<int>(pose - posi + 1);
|
||||
if (!Game::Lua::CheckStack(L, n))
|
||||
Game::Lua::Error(L, "stack overflow (%s)", "string slice too long");
|
||||
for (int i = 0; i < n; i++)
|
||||
Game::Lua::PushNumber(L, static_cast<unsigned char>(s[posi + i - 1]));
|
||||
return n;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
@@ -515,27 +428,28 @@ const Game::Doc::Function kStrconcat{
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("string", "match", &Script_string_match);
|
||||
Game::Lua::RegisterTableFunction("string", "gmatch", Script_string_gmatch);
|
||||
Game::Lua::RegisterTableFunction("string", "gsub", &Script_string_gsub);
|
||||
Game::Lua::RegisterTableFunction("string", "reverse", &Script_string_reverse);
|
||||
Game::Lua::RegisterTableFunction("string", "byte", &Script_string_byte);
|
||||
Game::Lua::RegisterTableFunction("string", "split", &Script_strsplit);
|
||||
Game::Lua::RegisterTableFunction("string", "trim", &Script_strtrim);
|
||||
Game::Lua::RegisterTableFunction("string", "join", &Script_strjoin);
|
||||
Game::Lua::RegisterTableFunction("string", "concat", &Script_strconcat, &kStrconcat);
|
||||
// 4.3.4's compat.lua binds each short global and its string-table twin
|
||||
// to ONE function by assignment (`gsub = str.gsub`, `strmatch =
|
||||
// to ONE function by assignment (`gmatch = str.gmatch`, `strmatch =
|
||||
// str.match`, `strrev = str.reverse`, `str.trim = strtrim`, `str.split =
|
||||
// strsplit`, `str.join = strjoin`), so `strtrim == string.trim`. A second
|
||||
// Register* call would mint a second closure over the same C function, so
|
||||
// the globals alias the table entries by value instead. The string-table
|
||||
// half is what lets `(" x "):trim()` work and libraries capture it at
|
||||
// file scope (AceConfigCmd-3.0: `strtrim = string.trim`); the bare `gsub`
|
||||
// gets the table-replacement upgrade by being the same function.
|
||||
Game::Lua::RegisterGlobalAlias("gsub", "string", "gsub");
|
||||
// file scope (AceConfigCmd-3.0: `strtrim = string.trim`). 1.12's own
|
||||
// compat.lua set `strbyte` from the old string.byte, so it is re-aliased.
|
||||
Game::Lua::RegisterGlobalAlias("gmatch", "string", "gmatch");
|
||||
Game::Lua::RegisterGlobalAlias("strmatch", "string", "match");
|
||||
Game::Lua::RegisterGlobalAlias("strsplit", "string", "split");
|
||||
Game::Lua::RegisterGlobalAlias("strjoin", "string", "join");
|
||||
Game::Lua::RegisterGlobalAlias("strtrim", "string", "trim");
|
||||
Game::Lua::RegisterGlobalAlias("strrev", "string", "reverse");
|
||||
Game::Lua::RegisterGlobalAlias("strbyte", "string", "byte");
|
||||
Game::Lua::RegisterGlobalAlias("strconcat", "string", "concat", &kStrconcat);
|
||||
Game::Lua::RegisterGlobalFunction("strreplace", &Script_strreplace);
|
||||
Game::Lua::RegisterGlobalFunction("strlenutf8", &Script_strlenutf8, &kStrlenutf8);
|
||||
|
||||
+5
-21
@@ -47,7 +47,9 @@
|
||||
// section's header follows.
|
||||
|
||||
#include "Inject.h"
|
||||
#include "Common.h"
|
||||
#include "Offsets.h"
|
||||
#include "storm/Memory.h"
|
||||
|
||||
#include "embed/Resource.h"
|
||||
#include "embedded_resources.h"
|
||||
@@ -212,28 +214,12 @@ const char *FindSplicePosition(const char *content, size_t size,
|
||||
return FindOutsideComment(content, end, "</Bindings>");
|
||||
}
|
||||
|
||||
char NormalizeChar(char c) {
|
||||
if (c == '/') return '\\';
|
||||
if (c >= 'A' && c <= 'Z') return static_cast<char>(c + 32);
|
||||
return c;
|
||||
}
|
||||
|
||||
bool PathEqualsCI(const char *a, const char *b) {
|
||||
while (*a != '\0' && *b != '\0') {
|
||||
if (NormalizeChar(*a) != NormalizeChar(*b)) return false;
|
||||
++a; ++b;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
using SMemAlloc_t = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
|
||||
using SMemFree_t = void(__stdcall *)(void *buf, const char *file, int line, int flags);
|
||||
|
||||
} // namespace
|
||||
|
||||
bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
size_t extraBytes, int flag1, int flag2, FileReadFn original) {
|
||||
if (path == nullptr || !PathEqualsCI(path, kFrameXMLBindingsPath))
|
||||
if (path == nullptr || !Common::SamePath(path, kFrameXMLBindingsPath))
|
||||
return false;
|
||||
|
||||
// Pull the real file through the original reader with the caller's
|
||||
@@ -270,10 +256,8 @@ bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
const size_t splicePos = static_cast<size_t>(splice - content);
|
||||
const size_t newSize = origSize + config.innerSize;
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAlloc_t>(Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
auto SMemFree = reinterpret_cast<SMemFree_t>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
|
||||
void *newBuf = SMemAlloc(newSize + extraBytes, __FILE__, __LINE__, 0);
|
||||
void *newBuf = Storm::Alloc(newSize + extraBytes, __FILE__, __LINE__);
|
||||
if (newBuf == nullptr) {
|
||||
// Allocation failed — fall back to the original buffer so the
|
||||
// engine still loads its own bindings.
|
||||
@@ -290,7 +274,7 @@ bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
if (extraBytes > 0)
|
||||
std::memset(dest + newSize, 0, extraBytes);
|
||||
|
||||
SMemFree(origBuf, __FILE__, __LINE__, 0);
|
||||
Storm::Free(origBuf, __FILE__, __LINE__);
|
||||
|
||||
if (outBuf != nullptr) *outBuf = newBuf;
|
||||
if (outSize != nullptr) *outSize = newSize;
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
#include "baselib/Ascii.h"
|
||||
#include "frame/ClickEvents.h"
|
||||
#include "input/Modifier.h"
|
||||
#include "storm/Memory.h"
|
||||
#include "xml/FrameLoad.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -219,8 +220,7 @@ void LoadModifiedClicks(const char *path, void *status) {
|
||||
return;
|
||||
void *tree = reinterpret_cast<TreeLoad_t>(Offsets::FUN_XML_TREE_LOAD)(
|
||||
static_cast<const char *>(data), static_cast<uint32_t>(size));
|
||||
reinterpret_cast<AddOns::EngineIO::SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE)(data, __FILE__,
|
||||
__LINE__, 0);
|
||||
Storm::Free(data, __FILE__, __LINE__);
|
||||
if (tree == nullptr)
|
||||
return; // the engine loader reports the parse error
|
||||
const void *root = Game::Read<const void *>(tree, 0);
|
||||
|
||||
+21
-9
@@ -72,18 +72,25 @@ struct State {
|
||||
uint32_t endMs = 0;
|
||||
bool active = false; // at least one reset has happened this session
|
||||
bool dirty = false; // needs a PLAYER_SWING fire on the next WorldTick
|
||||
bool held = false; // the latest reset was recorded but not reported yet
|
||||
};
|
||||
State g_swing[3];
|
||||
|
||||
// `notify` false records the server's new timer without firing PLAYER_SWING:
|
||||
// the reset is real, but no swing is coming yet (see OnSpellGo).
|
||||
// the reset is real, but no swing is coming yet (see OnSpellGo). It stays
|
||||
// `held` until the attack start reports it or a later reported reset replaces it.
|
||||
void MarkReset(SwingType type, uint32_t durationMs, bool notify = true) {
|
||||
if (durationMs == 0)
|
||||
return; // no weapon for this hand right now -- nothing to report
|
||||
State &s = g_swing[type];
|
||||
s.endMs = NowMs() + durationMs;
|
||||
s.active = true;
|
||||
s.dirty = s.dirty || notify;
|
||||
if (notify) {
|
||||
s.dirty = true;
|
||||
s.held = false;
|
||||
} else {
|
||||
s.held = true;
|
||||
}
|
||||
}
|
||||
|
||||
using HasAttackTarget_t = int(__fastcall *)(const void *unit);
|
||||
@@ -410,11 +417,17 @@ void OnPacket(uint32_t opcode, CDataStore *packet) {
|
||||
auto *playerObj = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (InMeleeRange(playerObj, victimObj)) {
|
||||
MarkReset(OFF_HAND, AttackTimeMs(OFF_HAND));
|
||||
// A cast reset made before the attack still holds the
|
||||
// main-hand swing back; report it now that one is coming.
|
||||
// A cast reset made before the attack is still held back;
|
||||
// report it now that a swing is coming. Only a held reset:
|
||||
// a target switch mid-attack also lands here, and there the
|
||||
// main-hand swing was never reset -- re-reporting its
|
||||
// remaining time would read as a fresh, short swing.
|
||||
State &main = g_swing[MAIN_HAND];
|
||||
if (main.active && Time::Clock::Remaining(NowMs(), main.endMs) > 0)
|
||||
main.dirty = true;
|
||||
if (main.held) {
|
||||
main.held = false;
|
||||
if (Time::Clock::Remaining(NowMs(), main.endMs) > 0)
|
||||
main.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -477,6 +490,7 @@ void OnWorldTick() {
|
||||
if (!s.dirty)
|
||||
continue;
|
||||
s.dirty = false;
|
||||
s.held = false;
|
||||
if (slot < 0)
|
||||
continue;
|
||||
const uint32_t remaining = Time::Clock::Remaining(now, s.endMs);
|
||||
@@ -497,9 +511,7 @@ const Game::Lua::EnumIntegerEntry kSwingTypeEntries[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "PlayerSwingType", kSwingTypeEntries,
|
||||
sizeof(kSwingTypeEntries) / sizeof(kSwingTypeEntries[0]), "SwingTimer");
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "PlayerSwingType", kSwingTypeEntries, "SwingTimer");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+20
-22
@@ -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"
|
||||
@@ -44,6 +45,7 @@
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Focus.h"
|
||||
#include "unit/Identity.h"
|
||||
#include "unit/Power.h"
|
||||
|
||||
#include <ctime>
|
||||
|
||||
@@ -405,9 +407,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) {
|
||||
@@ -419,14 +419,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) {
|
||||
@@ -488,9 +486,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) {
|
||||
@@ -581,6 +577,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); }
|
||||
@@ -608,15 +612,10 @@ bool Ranged(const uint8_t *spell) {
|
||||
|
||||
uint32_t SchoolMask(uint32_t school) { return school < kSchoolCount ? 1u << school : 0; }
|
||||
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName) {
|
||||
if (spell == nullptr)
|
||||
return false;
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
if (Game::Read<int>(spell, Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME + i * 4) ==
|
||||
auraName)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
uint32_t SpellSchoolMask(const uint8_t *spell) {
|
||||
return spell != nullptr
|
||||
? SchoolMask(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_SCHOOL))
|
||||
: 0;
|
||||
}
|
||||
|
||||
Entry Begin(uint64_t source, uint64_t dest, int event, const uint8_t *spell) {
|
||||
@@ -675,9 +674,7 @@ void SetItem(Entry &e, int itemId) {
|
||||
}
|
||||
|
||||
int DisplayAmount(int amount, int powerType) {
|
||||
using Divisor_t = int(__fastcall *)(int powerType);
|
||||
const int divisor = reinterpret_cast<Divisor_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_POWER_DISPLAY_DIVISOR))(powerType);
|
||||
const int divisor = static_cast<int>(Unit::Power::DisplayDivisor(powerType));
|
||||
return divisor != 0 ? amount / divisor : amount;
|
||||
}
|
||||
|
||||
@@ -700,6 +697,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,9 +164,8 @@ bool Ranged(const uint8_t *spell);
|
||||
// A school index (Spell.dbc School, the packets' school) as a school mask.
|
||||
uint32_t SchoolMask(uint32_t school);
|
||||
|
||||
// Whether one of the spell's three effects applies the aura `auraName`
|
||||
// (EffectApplyAuraName).
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName);
|
||||
// The spell's school as a mask (4.3.4's SpellRec m_schoolMask), 0 for nullptr.
|
||||
uint32_t SpellSchoolMask(const uint8_t *spell);
|
||||
|
||||
// GetCombatLogEntry: an entry for `event` from `source` to `dest` (either may
|
||||
// be 0), with the spell prefix when `spell` is a record.
|
||||
|
||||
@@ -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
|
||||
@@ -55,6 +55,7 @@
|
||||
#include "aura/Data.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "time/Clock.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -501,7 +502,7 @@ const uint8_t *ShieldSpell(uint64_t owner, uint32_t school) {
|
||||
const uint8_t *spell = SpellRecord(static_cast<int>(spellId));
|
||||
if (spell == nullptr ||
|
||||
Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_SCHOOL) != school ||
|
||||
!HasAuraEffect(spell, AURA_DAMAGE_SHIELD))
|
||||
!Spell::Lookup::HasAuraEffect(spell, AURA_DAMAGE_SHIELD))
|
||||
continue;
|
||||
if (found != nullptr && found != spell)
|
||||
return nullptr;
|
||||
|
||||
@@ -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
|
||||
@@ -145,12 +145,8 @@ void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleGetAllCommands", &Script_GetAllCommands);
|
||||
Game::Lua::RegisterGlobalFunction("ConsolePrintAllMatchingCommands",
|
||||
&Script_ConsolePrintAllMatchingCommands);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCommandType", kCommandTypeEntries,
|
||||
sizeof(kCommandTypeEntries) /
|
||||
sizeof(kCommandTypeEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCategory", kCategoryEntries,
|
||||
sizeof(kCategoryEntries) /
|
||||
sizeof(kCategoryEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCommandType", kCommandTypeEntries);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCategory", kCategoryEntries);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "console/Registry.h"
|
||||
#include "input/Modifier.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -63,7 +64,6 @@ namespace {
|
||||
using Handler_t = int(__fastcall *)(const void *data, void *param);
|
||||
using RegisterEx_t = void(__fastcall *)(int eventId, Handler_t handler, void *param,
|
||||
float priority);
|
||||
using IsKeyDown_t = int(__fastcall *)(int key);
|
||||
using GetInputLine_t = uintptr_t(__fastcall *)();
|
||||
using MakeCommandCurrent_t = void(__fastcall *)(uintptr_t line, const char *text);
|
||||
using SetInputString_t = void(__fastcall *)(const char *text);
|
||||
@@ -205,7 +205,7 @@ int __fastcall OnKeyDown(const void *data, void * /*param*/) {
|
||||
return 1;
|
||||
if (Game::Read<uint32_t>(Offsets::VAR_CONSOLE_VISIBLE) == 0)
|
||||
return 1;
|
||||
if (reinterpret_cast<IsKeyDown_t>(Offsets::FUN_GAMEUI_IS_KEY_DOWN)(toggleKey) != 0)
|
||||
if (Input::Modifier::IsKeyDown(toggleKey))
|
||||
return 1;
|
||||
|
||||
const uintptr_t line =
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// 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 "container/Bag.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "equipmentset/Locations.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
namespace Container::Bag {
|
||||
|
||||
namespace {
|
||||
|
||||
bool PlayerRange(int first, int count, View *out) {
|
||||
auto *inv = Unit::Identity::PlayerInventoryManager();
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
*out = {inv, first, count, 0};
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The main bank resolves only while a banker is open — 3.3.5 gates it on
|
||||
// `CGBankInfo::m_banker` the same way. Its GUIDs sit in the player's list
|
||||
// from login, but the server refuses every bank move without a banker and
|
||||
// the engine's own slot reader (`0x6228a0`) hides slots 39..68 on the same
|
||||
// banker-GUID test. Bank bags get that gate from `BagContainer`, whose
|
||||
// engine GUID getter answers 0 for them while the bank is closed.
|
||||
bool ViewFor(int bagID, View *out) {
|
||||
if (bagID == Offsets::BAG_INDEX_BACKPACK)
|
||||
return PlayerRange(Offsets::INVMGR_BACKPACK_FIRST_SLOT,
|
||||
Offsets::BACKPACK_NUM_SLOTS, out);
|
||||
if (bagID == Offsets::BAG_INDEX_BANK) {
|
||||
if (!EquipmentSet::Locations::BankerOpen())
|
||||
return false;
|
||||
return PlayerRange(Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
|
||||
Offsets::INVMGR_BANK_MAIN_LAST_SLOT -
|
||||
Offsets::INVMGR_BANK_MAIN_FIRST_SLOT + 1,
|
||||
out);
|
||||
}
|
||||
|
||||
const uint8_t *bag = Item::Location::BagContainer(bagID);
|
||||
auto *inv = static_cast<const uint8_t *>(Item::Location::ContainerInventory(bag));
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
const int slots = static_cast<int>(
|
||||
Game::Read<uint32_t>(inv, Offsets::OFF_INVMGR_SLOT_COUNT));
|
||||
if (slots <= 0)
|
||||
return false;
|
||||
|
||||
// The bag's own family, as 3.3.5's `CGItem_C::GetBagFamily` reads it (0
|
||||
// while its record isn't cached). `BitmaskForRecord` supplies the family
|
||||
// a vanilla bag's empty field leaves out, so a quiver reports one rather
|
||||
// than reading as general-purpose, and agrees with `C_Item.GetItemFamily`.
|
||||
uint32_t family = 0;
|
||||
const int bagItemID = Item::ID::FromCGItem(bag);
|
||||
if (bagItemID > 0) {
|
||||
if (auto *rec = Item::PeekRecord(static_cast<uint32_t>(bagItemID)))
|
||||
family = Item::BagFamily::BitmaskForRecord(rec);
|
||||
}
|
||||
*out = {inv, 0, slots, family};
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t SlotGuid(const View &view, int slotIndex0) {
|
||||
const uint32_t linear = static_cast<uint32_t>(view.firstLinear + slotIndex0);
|
||||
if (linear >= Game::Read<uint32_t>(view.invMgr, Offsets::OFF_INVMGR_SLOT_COUNT))
|
||||
return 0;
|
||||
auto *guids = Game::Read<const uint64_t *>(view.invMgr, Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
return guids != nullptr ? guids[linear] : 0;
|
||||
}
|
||||
|
||||
int CountFree(const View &view) {
|
||||
if (view.invMgr != Unit::Identity::PlayerInventoryManager()) {
|
||||
using CountFreeSlots_t = uint32_t(__thiscall *)(const uint8_t *invMgr);
|
||||
auto countFreeSlots =
|
||||
reinterpret_cast<CountFreeSlots_t>(Offsets::FUN_INVMGR_COUNT_FREE_SLOTS);
|
||||
return static_cast<int>(countFreeSlots(view.invMgr));
|
||||
}
|
||||
int free = 0;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
if (SlotGuid(view, i) == 0)
|
||||
free++;
|
||||
}
|
||||
return free;
|
||||
}
|
||||
|
||||
} // namespace Container::Bag
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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 Container::Bag {
|
||||
|
||||
// A container's storage, addressed without Lua: the inventory list holding
|
||||
// its slot GUIDs, the linear index its first slot sits at, its slot count,
|
||||
// and the BagFamily bitmask it accepts (0 = anything). The backpack and the
|
||||
// main bank live inside the player's own inventory list at different linear
|
||||
// bases; equipped bags and bank bags each have a list of their own.
|
||||
struct View {
|
||||
const uint8_t *invMgr;
|
||||
int firstLinear;
|
||||
int slotCount;
|
||||
uint32_t family;
|
||||
};
|
||||
|
||||
// Resolves Lua container `bagID` — backpack 0, bags 1..4, main bank -1, bank
|
||||
// bags 5..10 — to its `View`. False for the keyring, an out-of-range ID, an
|
||||
// empty bag slot, and any bank container while no banker is open. Pure C — no
|
||||
// Lua stack.
|
||||
bool ViewFor(int bagID, View *out);
|
||||
|
||||
// GUID of the item in 0-based slot `slotIndex0` of `view`, 0 when empty.
|
||||
uint64_t SlotGuid(const View &view, int slotIndex0);
|
||||
|
||||
// Empty slots in `view`. A bag's own list is counted by the engine
|
||||
// (`FUN_INVMGR_COUNT_FREE_SLOTS`, 3.3.5's `CGContainer_C::GetFreeSlots`); a
|
||||
// range of the player's list (backpack, main bank) is walked slot by slot,
|
||||
// the way 3.3.5's `Script_GetContainerNumFreeSlots` walks it.
|
||||
int CountFree(const View &view);
|
||||
|
||||
} // namespace Container::Bag
|
||||
@@ -42,8 +42,7 @@ const Game::Lua::EnumIntegerEntry kEntries[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "BagIndex", kEntries,
|
||||
sizeof(kEntries) / sizeof(kEntries[0]), "BagIndexConstants");
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "BagIndex", kEntries, "BagIndexConstants");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+29
-115
@@ -12,94 +12,21 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "container/Bag.h"
|
||||
|
||||
namespace Container::FreeSlots {
|
||||
|
||||
namespace {
|
||||
|
||||
// Resolves the bag's metadata: total slot count + BagFamily bitfield.
|
||||
// `C_Container.GetContainerNumFreeSlots(bagID)` — `(numFreeSlots,
|
||||
// bagFamily)`. 3.3.5's `Script_GetContainerNumFreeSlots` (`0x005D7590`):
|
||||
// the backpack and, at a banker, the main bank report family 0; bags 1..4
|
||||
// and bank bags 5..10 report the bag's own family. A container that isn't
|
||||
// there — keyring, an empty bag slot, the bank while it's closed — returns
|
||||
// only `0`, as 3.3.5 does and as retail's nilable `bagFamily` documents.
|
||||
//
|
||||
// - bagID 0 (backpack) → (16, 0). Vanilla 1.12 backpack is fixed at 16
|
||||
// slots and unfamilied (0). 3.3.5's `Script_GetContainerNumFreeSlots`
|
||||
// hardcodes both values for the backpack path too.
|
||||
// - bagID 1..4 → looks up the equipped bag at INVSLOT_BAGn, reads
|
||||
// `m_containerSlots` and `m_bagFamily` from the cached `ItemStats_C`
|
||||
// record. If no bag equipped or item not cached: (0, 0).
|
||||
// - other bagIDs (bank, keyring) → (0, 0). Not supported in this
|
||||
// implementation.
|
||||
struct BagInfo {
|
||||
int slotCount;
|
||||
int bagType;
|
||||
};
|
||||
|
||||
BagInfo ResolveBagInfo(int bagID) {
|
||||
if (bagID == 0)
|
||||
return {Offsets::BACKPACK_NUM_SLOTS, 0};
|
||||
if (bagID < 1 || bagID > 4)
|
||||
return {0, 0};
|
||||
|
||||
const int equipSlot = Offsets::INVSLOT_BAG1 + bagID - 1;
|
||||
auto *bagItem = Item::Location::ResolveEquipmentSlot(equipSlot);
|
||||
if (bagItem == nullptr)
|
||||
return {0, 0};
|
||||
|
||||
const int itemID = Item::ID::FromCGItem(bagItem);
|
||||
if (itemID == 0)
|
||||
return {0, 0};
|
||||
|
||||
auto *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
return {0, 0};
|
||||
|
||||
const int slots = static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_CONTAINER_SLOTS));
|
||||
// BagFamily bitmask: the item's field if populated, else derived from
|
||||
// the container subclass — Turtle leaves bags' m_bagFamily at 0, so a
|
||||
// Soul Bag would otherwise report family 0 and get miscounted as a
|
||||
// general-purpose bag by CalculateTotalNumberOfFreeBagSlots. Shared
|
||||
// with C_Item.GetItemFamily so a bag and its contents agree. See
|
||||
// `Item::BagFamily`.
|
||||
const int family =
|
||||
static_cast<int>(Item::BagFamily::BitmaskForRecord(record));
|
||||
return {slots, family};
|
||||
}
|
||||
|
||||
// Walks the bag and counts empty slots. Each `Item::Location::ResolveBag`
|
||||
// call stomps Lua stack[1]/[2] (a requirement of the engine's
|
||||
// `PackBagSlot` helper), but we own the stack here — Lua just called us
|
||||
// — and the caller's bagID arg has already been read into a local. So
|
||||
// it's safe to repeatedly clobber the stack across iterations.
|
||||
int CountFreeSlotsInBag(void *L, int bagID, int slotCount) {
|
||||
int free = 0;
|
||||
for (int slot = 1; slot <= slotCount; slot++) {
|
||||
if (Item::Location::ResolveBag(L, bagID, slot) == nullptr)
|
||||
free++;
|
||||
}
|
||||
return free;
|
||||
}
|
||||
|
||||
// `C_Container.GetContainerNumFreeSlots(bagID)` — returns
|
||||
// `(numberOfFreeSlots, bagType)`. `bagID` is 0..4 (0 = backpack,
|
||||
// 1..4 = equipped bag slots). For invalid or unsupported bag IDs
|
||||
// (negative bank/keyring indices, out-of-range values), returns
|
||||
// `(0, 0)` — matching 3.3.5's behavior of always pushing two values
|
||||
// rather than nil/nothing.
|
||||
//
|
||||
// `bagType` is the `BagFamily` bitmask in modern encoding. Bags leave the
|
||||
// raw family field empty in Turtle's data (Soul/Herb/quiver/ammo pouch/…
|
||||
// all read 0), so it's derived from the bag's class + subclass via
|
||||
// `Item::BagFamily`: a Soul Bag reports `0x4`, a quiver `0x1`, an ammo
|
||||
// pouch `0x2` — matching what `C_Item.GetItemFamily` reports for both the
|
||||
// bag and the items it holds. See that helper for the Turtle custom-family
|
||||
// caveat (Meat/Fish/Leather/Mining, IDs 10–13).
|
||||
// `bagFamily` is the modern bitmask; see `Container::Bag::ViewFor` for how
|
||||
// a vanilla bag with an empty family field gets one.
|
||||
int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L,
|
||||
@@ -108,11 +35,13 @@ int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
const BagInfo info = ResolveBagInfo(bagID);
|
||||
const int free = (info.slotCount > 0) ? CountFreeSlotsInBag(L, bagID, info.slotCount) : 0;
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(free));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(info.bagType));
|
||||
Container::Bag::View view;
|
||||
if (!Container::Bag::ViewFor(bagID, &view)) {
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(Container::Bag::CountFree(view)));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(view.family));
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -120,11 +49,8 @@ int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
|
||||
// as an array of slot numbers in ascending order, rather than
|
||||
// `GetContainerNumFreeSlots`'s count of them. A full bag returns an
|
||||
// empty table; a bag that isn't there returns nothing, which is how
|
||||
// callers tell "no free slots" from "no such bag".
|
||||
//
|
||||
// `bagID` is 0..4 like the rest of this file (0 = backpack, 1..4 =
|
||||
// equipped bag slots); bank and keyring indices are the "no such bag"
|
||||
// case.
|
||||
// callers tell "no free slots" from "no such bag". Takes the same
|
||||
// containers as `GetContainerNumFreeSlots`.
|
||||
int __fastcall Script_C_Container_GetContainerFreeSlots(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L,
|
||||
@@ -133,24 +59,17 @@ int __fastcall Script_C_Container_GetContainerFreeSlots(void *L) {
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
const BagInfo info = ResolveBagInfo(bagID);
|
||||
if (info.slotCount <= 0)
|
||||
Container::Bag::View view;
|
||||
if (!Container::Bag::ViewFor(bagID, &view))
|
||||
return 0; // nothing — no such bag
|
||||
|
||||
// Collect before building the table. Each `ResolveBag` stomps stack
|
||||
// slots 1 and 2, so the table cannot be on the stack during the walk.
|
||||
std::vector<int> freeSlots;
|
||||
freeSlots.reserve(static_cast<size_t>(info.slotCount));
|
||||
for (int slot = 1; slot <= info.slotCount; ++slot) {
|
||||
if (Item::Location::ResolveBag(L, bagID, slot) == nullptr)
|
||||
freeSlots.push_back(slot);
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
for (size_t i = 0; i < freeSlots.size(); ++i) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
if (Container::Bag::SlotGuid(view, i) != 0)
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(++n));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(freeSlots[i]));
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
return 1;
|
||||
@@ -165,18 +84,13 @@ int __fastcall Script_C_Container_GetContainerFreeSlots(void *L) {
|
||||
// free, family = C_Container.GetContainerNumFreeSlots(bag)
|
||||
// if family == 0 then total = total + free end
|
||||
//
|
||||
// Takes no arguments. Specialty bags are correctly excluded: their family
|
||||
// comes from `Item::BagFamily` (container-subclass fallback), so a Soul
|
||||
// Bag / Herb Bag / Meat Bag reports a non-zero family and its slots are
|
||||
// left out of the general-purpose total — matching retail's FrameXML
|
||||
// behavior. (An earlier version miscounted them because Turtle leaves the
|
||||
// raw family field at 0 for bags.)
|
||||
// Takes no arguments.
|
||||
int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
|
||||
int total = 0;
|
||||
for (int bagID = 0; bagID <= 4; bagID++) {
|
||||
const BagInfo info = ResolveBagInfo(bagID);
|
||||
if (info.slotCount > 0 && info.bagType == 0)
|
||||
total += CountFreeSlotsInBag(L, bagID, info.slotCount);
|
||||
Container::Bag::View view;
|
||||
if (Container::Bag::ViewFor(bagID, &view) && view.family == 0)
|
||||
total += Container::Bag::CountFree(view);
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(total));
|
||||
return 1;
|
||||
|
||||
+31
-13
@@ -55,6 +55,7 @@
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/TypeInfo.h"
|
||||
#include "localization/Locale.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -86,12 +87,29 @@ char16_t ToLower(char16_t c) {
|
||||
return c;
|
||||
}
|
||||
|
||||
// Appends the lowercased `c` with 4.3.4's accent folding for `locale`
|
||||
// (frFR 2, deDE 3, esES/esMX 6/7, ruRU 8); æ, œ and ß become two letters.
|
||||
// Returns the new length.
|
||||
size_t AppendFolded(int locale, char16_t c, char16_t *out, size_t n) {
|
||||
switch (locale) {
|
||||
case 2: // frFR
|
||||
// 4.3.4's accent-folding sets. 4.3.4 picks one by its locale index (frFR 2,
|
||||
// deDE 3, esES/esMX 6/7, ruRU 8); 1.12's index has other slots, so the set
|
||||
// is picked by locale name.
|
||||
enum class Folding { None, French, German, Spanish, Russian };
|
||||
|
||||
Folding ClientFolding() {
|
||||
using Localization::Locale::Is;
|
||||
if (Is("frFR"))
|
||||
return Folding::French;
|
||||
if (Is("deDE"))
|
||||
return Folding::German;
|
||||
if (Is("esES") || Is("esMX"))
|
||||
return Folding::Spanish;
|
||||
if (Is("ruRU"))
|
||||
return Folding::Russian;
|
||||
return Folding::None;
|
||||
}
|
||||
|
||||
// Appends the lowercased `c` with `folding`'s accent folding; æ, œ and ß
|
||||
// become two letters. Returns the new length.
|
||||
size_t AppendFolded(Folding folding, char16_t c, char16_t *out, size_t n) {
|
||||
switch (folding) {
|
||||
case Folding::French:
|
||||
switch (c) {
|
||||
case 0xE0: case 0xE2: case 0xE4: c = u'a'; break;
|
||||
case 0xE6: out[n++] = u'a'; c = u'e'; break;
|
||||
@@ -103,14 +121,13 @@ size_t AppendFolded(int locale, char16_t c, char16_t *out, size_t n) {
|
||||
case 0xF9: case 0xFA: case 0xFB: case 0xFC: c = u'u'; break;
|
||||
}
|
||||
break;
|
||||
case 3: // deDE
|
||||
case Folding::German:
|
||||
if (c == 0xDF) {
|
||||
out[n++] = u's';
|
||||
c = u's';
|
||||
}
|
||||
break;
|
||||
case 6: // esES
|
||||
case 7: // esMX
|
||||
case Folding::Spanish:
|
||||
switch (c) {
|
||||
case 0xE1: c = u'a'; break;
|
||||
case 0xE9: c = u'e'; break;
|
||||
@@ -120,10 +137,12 @@ size_t AppendFolded(int locale, char16_t c, char16_t *out, size_t n) {
|
||||
case 0xF1: c = u'n'; break;
|
||||
}
|
||||
break;
|
||||
case 8: // ruRU
|
||||
case Folding::Russian:
|
||||
if (c == 0x451)
|
||||
c = 0x435;
|
||||
break;
|
||||
case Folding::None:
|
||||
break;
|
||||
}
|
||||
out[n++] = c;
|
||||
return n;
|
||||
@@ -141,11 +160,10 @@ size_t Fold(const char *text, char16_t (&out)[kFoldCap]) {
|
||||
// One unit short of the buffer: a full conversion stores no terminator.
|
||||
reinterpret_cast<Convert_t>(Offsets::FUN_SUNI_CONVERT_UTF8_TO_16)(
|
||||
wide, kFoldCap - 1, text, 0x7FFFFFFF, nullptr, nullptr);
|
||||
const int locale =
|
||||
*reinterpret_cast<const int *>(static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
const Folding folding = ClientFolding();
|
||||
size_t n = 0;
|
||||
for (size_t i = 0; wide[i] != 0 && n + 2 < kFoldCap; ++i)
|
||||
n = AppendFolded(locale, ToLower(wide[i]), out, n);
|
||||
n = AppendFolded(folding, ToLower(wide[i]), out, n);
|
||||
out[n] = 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -60,18 +60,16 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "container/Bag.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "equipmentset/Locations.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/Swap.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -82,6 +80,8 @@ namespace Container::SortBags {
|
||||
|
||||
namespace {
|
||||
|
||||
using BagView = Container::Bag::View;
|
||||
|
||||
// The two sets this can arrange. Both go through the same merge,
|
||||
// snapshot, comparator and placement.
|
||||
constexpr int kInventoryBags[] = {0, 1, 2, 3, 4};
|
||||
@@ -258,96 +258,13 @@ bool Precedes(const Entry &a, const Entry &b) {
|
||||
return a.itemID < b.itemID; // total order, so the result is stable
|
||||
}
|
||||
|
||||
// A bag's storage, addressed without Lua: the invMgr holding its GUID
|
||||
// array, the linear index its first slot sits at, and the family it
|
||||
// accepts. The backpack lives inside the player's own invMgr; bags 1..4
|
||||
// each have one of their own.
|
||||
struct BagView {
|
||||
const uint8_t *invMgr;
|
||||
int firstLinear;
|
||||
int slotCount;
|
||||
uint32_t family;
|
||||
};
|
||||
|
||||
// The backpack and the main bank both live inside the player's own
|
||||
// invMgr, at different linear bases. Equipped bags and bank bags each
|
||||
// have an invMgr of their own, reached through the bag item.
|
||||
//
|
||||
// The bank resolves only while a banker is open: the server refuses every
|
||||
// bank move without one, and the engine's own slot reader (`0x6228a0`)
|
||||
// hides slots 39..68 on the same banker-GUID test. The main bank's GUIDs
|
||||
// sit in the player's invMgr from login, so without this gate a closed-
|
||||
// bank sort would plan and send moves the server rejects one by one.
|
||||
bool ViewForBag(int bagID, BagView *out) {
|
||||
if ((bagID == -1 || bagID >= 5) && !EquipmentSet::Locations::BankerOpen())
|
||||
return false;
|
||||
if (bagID == 0) {
|
||||
auto *inv = Unit::Identity::PlayerInventoryManager();
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
*out = {inv, Offsets::INVMGR_BACKPACK_FIRST_SLOT,
|
||||
Offsets::BACKPACK_NUM_SLOTS, 0};
|
||||
return true;
|
||||
}
|
||||
if (bagID == -1) {
|
||||
auto *inv = Unit::Identity::PlayerInventoryManager();
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
*out = {inv, Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
|
||||
Offsets::INVMGR_BANK_MAIN_LAST_SLOT -
|
||||
Offsets::INVMGR_BANK_MAIN_FIRST_SLOT + 1,
|
||||
0};
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resolve the bag item that owns the container.
|
||||
int equipSlot = 0;
|
||||
if (bagID >= 1 && bagID <= 4)
|
||||
equipSlot = Offsets::INVSLOT_BAG1 + bagID - 1;
|
||||
else if (bagID >= 5 && bagID <= 10)
|
||||
equipSlot = Offsets::INVMGR_BANK_BAG_FIRST_SLOT + 1 + (bagID - 5);
|
||||
else
|
||||
return false;
|
||||
|
||||
auto *bagItem = Item::Location::ResolveEquipmentSlot(equipSlot);
|
||||
if (bagItem == nullptr)
|
||||
return false;
|
||||
auto *bagInv = static_cast<const uint8_t *>(
|
||||
Item::Location::ContainerInventory(bagItem));
|
||||
if (bagInv == nullptr)
|
||||
return false;
|
||||
const int slots = static_cast<int>(
|
||||
Game::Read<uint32_t>(bagInv, Offsets::OFF_INVMGR_SLOT_COUNT));
|
||||
if (slots <= 0)
|
||||
return false;
|
||||
|
||||
// A specialty bag only accepts its own family, and Turtle leaves the
|
||||
// raw field empty on bags — `BitmaskForRecord` derives it from the
|
||||
// container subclass, so a quiver reports a family here rather than
|
||||
// reading as general-purpose. Shared with `C_Item.GetItemFamily`, so
|
||||
// a bag and the items it holds agree.
|
||||
uint32_t family = 0;
|
||||
const int bagItemID = Item::ID::FromCGItem(bagItem);
|
||||
if (bagItemID > 0) {
|
||||
auto *rec = Item::PeekRecord(static_cast<uint32_t>(bagItemID));
|
||||
if (rec != nullptr)
|
||||
family = Item::BagFamily::BitmaskForRecord(rec);
|
||||
}
|
||||
*out = {bagInv, 0, slots, family};
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint8_t *ItemByGuid(uint64_t guid) {
|
||||
return static_cast<const uint8_t *>(
|
||||
Object::ByGuid(Offsets::TYPEMASK_ITEM, guid, "SortBags", 0));
|
||||
}
|
||||
|
||||
const uint8_t *ItemAt(const BagView &v, int slotIndex0, uint64_t *guidOut) {
|
||||
auto *guids = *reinterpret_cast<const uint64_t *const *>(
|
||||
v.invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (guids == nullptr)
|
||||
return nullptr;
|
||||
const uint64_t guid = guids[v.firstLinear + slotIndex0];
|
||||
const uint64_t guid = Container::Bag::SlotGuid(v, slotIndex0);
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
*guidOut = guid;
|
||||
@@ -417,7 +334,7 @@ std::vector<Group> GatherGroups(const int *bags, int bagCount) {
|
||||
for (int b = 0; b < bagCount; ++b) {
|
||||
const int bag = bags[b];
|
||||
BagView view;
|
||||
if (!ViewForBag(bag, &view))
|
||||
if (!Container::Bag::ViewFor(bag, &view))
|
||||
continue;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
uint64_t guid = 0;
|
||||
@@ -590,7 +507,7 @@ bool Place(const int *bags, int bagCount, int mergesPending) {
|
||||
for (int b = 0; b < bagCount; ++b) {
|
||||
const int bag = bags[b];
|
||||
BagView view;
|
||||
if (!ViewForBag(bag, &view))
|
||||
if (!Container::Bag::ViewFor(bag, &view))
|
||||
continue;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
const int slot = i + 1;
|
||||
@@ -958,7 +875,7 @@ int __fastcall Script_SortBagsBags(void *L) {
|
||||
int pushed = 0;
|
||||
for (int b = 0; b < count; ++b) {
|
||||
BagView view;
|
||||
if (!ViewForBag(bags[b], &view))
|
||||
if (!Container::Bag::ViewFor(bags[b], &view))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, bags[b]);
|
||||
Game::Lua::PushNumber(L, view.slotCount);
|
||||
|
||||
@@ -131,10 +131,7 @@ int __fastcall Script_RequestLoadCreatureByID(void *L) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE);
|
||||
const bool accepted =
|
||||
Cache::QueryLoad::RequestLoad(cache, static_cast<uint32_t>(creatureID));
|
||||
Game::Lua::PushBool(L, accepted);
|
||||
Game::Lua::PushBool(L, RequestLoad(static_cast<uint32_t>(creatureID)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -381,4 +378,9 @@ const uint8_t *CacheRecord(uint32_t creatureID) {
|
||||
return creatureID != 0 ? PeekCreature(creatureID) : nullptr;
|
||||
}
|
||||
|
||||
bool RequestLoad(uint32_t creatureID) {
|
||||
return Cache::QueryLoad::RequestLoad(reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE),
|
||||
creatureID);
|
||||
}
|
||||
|
||||
} // namespace Creature::Info
|
||||
|
||||
@@ -24,6 +24,12 @@ namespace Creature::Info {
|
||||
// engine's SetDisplay takes, and `PlayerModel:SetCreature` passes it there.
|
||||
const uint8_t *CacheRecord(uint32_t creatureID);
|
||||
|
||||
// Starts loading `creatureID` into the creature cache; fires
|
||||
// `CREATURE_DATA_LOAD_RESULT(creatureID, success)` when it lands (at once
|
||||
// when already cached). False only when the load could not be tracked.
|
||||
// Backs `C_CreatureInfo.RequestLoadCreatureByID`.
|
||||
bool RequestLoad(uint32_t creatureID);
|
||||
|
||||
// The faction group a player race belongs to, resolved the way
|
||||
// `Script_UnitFactionGroup` does it for a player whose object isn't in the
|
||||
// world (its name-cache branch, which reads the race): ChrRaces -> FactionTemplate
|
||||
|
||||
@@ -27,10 +27,8 @@ const Game::Lua::EnumIntegerEntry kMoneyFormattingConstants[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterConstants("MoneyFormattingConstants", kMoneyFormattingConstants,
|
||||
sizeof(kMoneyFormattingConstants) /
|
||||
sizeof(kMoneyFormattingConstants[0]),
|
||||
"MoneyConstants");
|
||||
Game::Lua::RegisterConstants(
|
||||
"MoneyFormattingConstants", kMoneyFormattingConstants, "MoneyConstants");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -119,9 +119,7 @@ void OnTick() {
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "UICursorType", kCursorTypeEntries,
|
||||
sizeof(kCursorTypeEntries) / sizeof(kCursorTypeEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "UICursorType", kCursorTypeEntries);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// 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 cursor modes later clients added, shown on 1.12's cursor, and the
|
||||
// `expandedCursors` cvar that turns them on (0 by default: they change what
|
||||
// 1.12 shows).
|
||||
//
|
||||
// The engine's cursor is a table of 42 images of 32x32, one per mode;
|
||||
// SendCursorImage copies slot VAR_CURSOR_MODE to the device. SetCursorMode
|
||||
// stores the mode it is given and sends, so a mode past 41 must never reach
|
||||
// it. Slot 41 is how the engine shows an image outside the table:
|
||||
// SetCustomCursor loads a named BLP into it, stores mode 41, sends it and
|
||||
// keeps the name, and returns at once while that name is already showing. The
|
||||
// game object classifier uses it for a record-named cursor.
|
||||
//
|
||||
// Show() calls SetCustomCursor with the mode's path, so a file of that name (a
|
||||
// patch MPQ, a loose file) wins, and a repeat each frame costs one name
|
||||
// compare. 1.12 has none of these files, so on a miss Show() does what
|
||||
// SetCustomCursor does after its load, with the image embedded in the DLL
|
||||
// (Texture::Builtin), which a texture load of the same path also gets.
|
||||
//
|
||||
// The files are 4.3.4's Interface\Cursor images, under 4.3.4's names. 4.3.4
|
||||
// already ships StableMaster.blp and Fishing.blp, but only as art a record
|
||||
// could name. StableMaster is a mode in retail's table (Enum.Cursormode
|
||||
// StablemasterCursor, listed after Ping and Enchant), not in 4.3.4's; no
|
||||
// client's table has Fishing.
|
||||
|
||||
#include "cursor/Extended.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "texture/Builtin.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace Cursor::Extended {
|
||||
|
||||
namespace {
|
||||
|
||||
using SetCustomCursor_t = uint32_t(__fastcall *)(const char *path);
|
||||
using SendCursorImage_t = void(__cdecl *)();
|
||||
|
||||
// In mode order, the Unable forms after the others.
|
||||
const char *const kPaths[] = {
|
||||
"Interface\\Cursor\\RepairNPC.blp",
|
||||
"Interface\\Cursor\\Innkeeper.blp",
|
||||
"Interface\\Cursor\\Quest.blp",
|
||||
"Interface\\Cursor\\QuestRepeatable.blp",
|
||||
"Interface\\Cursor\\QuestTurnIn.blp",
|
||||
"Interface\\Cursor\\StableMaster.blp",
|
||||
"Interface\\Cursor\\Fishing.blp",
|
||||
"Interface\\Cursor\\UnableRepairNPC.blp",
|
||||
"Interface\\Cursor\\UnableInnkeeper.blp",
|
||||
"Interface\\Cursor\\UnableQuest.blp",
|
||||
"Interface\\Cursor\\UnableQuestRepeatable.blp",
|
||||
"Interface\\Cursor\\UnableQuestTurnIn.blp",
|
||||
"Interface\\Cursor\\UnableStableMaster.blp",
|
||||
"Interface\\Cursor\\UnableFishing.blp",
|
||||
};
|
||||
static_assert(sizeof kPaths / sizeof kPaths[0] == 2 * kUnableOffset, "one path per mode");
|
||||
|
||||
CVar::Factory::Handle g_enabled = nullptr;
|
||||
|
||||
void Register() {
|
||||
g_enabled = CVar::Factory::Register(
|
||||
"expandedCursors",
|
||||
"Show what an NPC or object offers on the cursor: anvil for repairs, ! and ? for quests, "
|
||||
"hook for your bobber",
|
||||
Offsets::CVAR_CATEGORY_GAME, "0");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Enabled() { return CVar::Factory::GetInt(g_enabled) != 0; }
|
||||
|
||||
bool Is(uint32_t mode) { return mode >= kRepairNpc && mode < kRepairNpc + 2 * kUnableOffset; }
|
||||
|
||||
bool IsUnable(uint32_t mode) { return Is(mode) && mode >= kRepairNpc + kUnableOffset; }
|
||||
|
||||
bool IsQuest(uint32_t mode) {
|
||||
if (!Is(mode))
|
||||
return false;
|
||||
const uint32_t kind = IsUnable(mode) ? mode - kUnableOffset : mode;
|
||||
return kind >= kQuest && kind <= kQuestTurnIn;
|
||||
}
|
||||
|
||||
const char *Path(uint32_t mode) { return kPaths[mode - kRepairNpc]; }
|
||||
|
||||
void Show(uint32_t mode) {
|
||||
const char *path = Path(mode);
|
||||
if (reinterpret_cast<SetCustomCursor_t>(Offsets::FUN_SET_CUSTOM_CURSOR)(path) != 0)
|
||||
return;
|
||||
const Texture::Builtin::Image *image = Texture::Builtin::Find(path);
|
||||
if (image == nullptr || image->width != Offsets::CURSOR_IMAGE_SIZE ||
|
||||
image->height != Offsets::CURSOR_IMAGE_SIZE)
|
||||
return;
|
||||
Game::Ref<uint32_t>(Offsets::VAR_CURSOR_MODE) = Offsets::CURSOR_MODE_CUSTOM;
|
||||
std::memcpy(reinterpret_cast<void *>(Offsets::VAR_CURSOR_IMAGE_CUSTOM), image->texels,
|
||||
Offsets::CURSOR_IMAGE_SIZE * Offsets::CURSOR_IMAGE_SIZE * sizeof(uint32_t));
|
||||
reinterpret_cast<SendCursorImage_t>(Offsets::FUN_SEND_CURSOR_IMAGE)();
|
||||
Common::BoundedCopy(reinterpret_cast<char *>(Offsets::VAR_CUSTOM_CURSOR_NAME), path,
|
||||
Offsets::CURSOR_FILE_SIZE);
|
||||
}
|
||||
|
||||
} // namespace Cursor::Extended
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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 "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Cursor::Extended {
|
||||
|
||||
// The cursor modes later clients added that 1.12 has no image for, numbered
|
||||
// on from the engine's last mode (CURSOR_MODE_CUSTOM): 4.3.4's RepairNPC,
|
||||
// Innkeeper and quest cursors, retail's StableMaster, and the fish hook for
|
||||
// the fishing bobber. As with the engine's modes, each has an Unable form,
|
||||
// kUnableOffset further on.
|
||||
constexpr uint32_t kRepairNpc = Offsets::CURSOR_MODE_CUSTOM + 1;
|
||||
constexpr uint32_t kInnkeeper = kRepairNpc + 1;
|
||||
constexpr uint32_t kQuest = kRepairNpc + 2;
|
||||
constexpr uint32_t kQuestRepeatable = kRepairNpc + 3;
|
||||
constexpr uint32_t kQuestTurnIn = kRepairNpc + 4;
|
||||
constexpr uint32_t kStableMaster = kRepairNpc + 5;
|
||||
constexpr uint32_t kFishing = kRepairNpc + 6;
|
||||
constexpr uint32_t kUnableOffset = 7;
|
||||
|
||||
// Whether the `expandedCursors` cvar asks for these modes. The pickers that
|
||||
// give them (cursor/Npc, cursor/Fishing) keep 1.12's cursor while it is 0.
|
||||
bool Enabled();
|
||||
|
||||
// Whether `mode` is one of these, in either form.
|
||||
bool Is(uint32_t mode);
|
||||
|
||||
// Whether `mode` is one of these in its Unable form.
|
||||
bool IsUnable(uint32_t mode);
|
||||
|
||||
// Whether `mode` is a quest giver's cursor (Quest, QuestRepeatable,
|
||||
// QuestTurnIn), in either form.
|
||||
bool IsQuest(uint32_t mode);
|
||||
|
||||
// The image file of one of these modes, `Interface\Cursor\<its name>.blp`.
|
||||
const char *Path(uint32_t mode);
|
||||
|
||||
// Shows one of these modes as the cursor. The engine's SetCursorMode cannot,
|
||||
// so its hook (cursor/Hover) sends them here.
|
||||
void Show(uint32_t mode);
|
||||
|
||||
} // namespace Cursor::Extended
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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/>.
|
||||
|
||||
// With `expandedCursors` at 1 (Cursor::Extended::Enabled), the cursor over
|
||||
// the player's fishing bobber is a fish hook in place of the Interact gear.
|
||||
//
|
||||
// A game object's cursor is data. GetCursor (FUN_GAMEOBJECT_GET_CURSOR) gives
|
||||
// the cursor its record names, else its type handler's: BaseHandlerGetCursor
|
||||
// for most types, which gives a lock's LockType cursor, else Interact. The
|
||||
// record's name comes from the server, and tortoise-wow sends it empty; even
|
||||
// 3.3.5's data names none for the bobber (AzerothCore's template 35591), and
|
||||
// a bobber has no lock, so every client shows the gear. Later clients ship
|
||||
// the hook (Interface\Cursor\Fishing.blp) only for data to name.
|
||||
//
|
||||
// The fishing node handler (type 17) gets its own cursor slot, the way
|
||||
// 4.3.4's guild bank type overrides its cursor. The slot still runs
|
||||
// BaseHandlerGetCursor, which decides everything (0 for a bobber the player
|
||||
// is not channelling, Unable when not usable), and swaps only the image. The
|
||||
// classifier passes the mode to SetCursorMode, whose hook (cursor/Hover)
|
||||
// shows it.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cursor/Extended.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Cursor::Fishing {
|
||||
|
||||
namespace {
|
||||
|
||||
using HandlerGetCursor_t = uint32_t(__thiscall *)(void *handler);
|
||||
|
||||
uint32_t __fastcall FishingNodeCursor(void *handler, void * /*edx*/) {
|
||||
const uint32_t mode =
|
||||
reinterpret_cast<HandlerGetCursor_t>(Offsets::FUN_GO_HANDLER_BASE_GET_CURSOR)(handler);
|
||||
if (!Cursor::Extended::Enabled())
|
||||
return mode;
|
||||
if (mode == Offsets::CURSOR_MODE_INTERACT)
|
||||
return Cursor::Extended::kFishing;
|
||||
if (mode == Offsets::CURSOR_MODE_INTERACT + Offsets::CURSOR_MODE_UNABLE_OFFSET)
|
||||
return Cursor::Extended::kFishing + Cursor::Extended::kUnableOffset;
|
||||
return mode;
|
||||
}
|
||||
|
||||
void Patch() {
|
||||
Common::RedirectSlot(Offsets::SLOT_FISHING_NODE_HANDLER_GET_CURSOR,
|
||||
Offsets::FUN_GO_HANDLER_BASE_GET_CURSOR,
|
||||
reinterpret_cast<const void *>(&FishingNodeCursor));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Patch};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Cursor::Fishing
|
||||
+23
-1
@@ -27,14 +27,21 @@
|
||||
// The spell-targeting and mouselook legs of 0x4828D0 are left out: they
|
||||
// answer for the mouse, not for the object.
|
||||
//
|
||||
// The unit classifier and the fishing node's handler can also pick a mode of
|
||||
// Cursor::Extended (cursor/Npc, cursor/Fishing). The engine's SetCursorMode
|
||||
// cannot show one, so the same hook hands those to Cursor::Extended::Show,
|
||||
// and while capturing records them like any other.
|
||||
//
|
||||
// The images are the engine's own: `Interface\Cursor\<name>.blp` from its
|
||||
// cursor name table, and a game object's named cursor from its record
|
||||
// (FormatGameObjectCursorFile, `Unable` when not usable).
|
||||
// (FormatGameObjectCursorFile, `Unable` when not usable). An extended mode's
|
||||
// image is its embedded file (Cursor::Extended::Path).
|
||||
|
||||
#include "cursor/Hover.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cursor/Extended.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
@@ -58,6 +65,10 @@ void __fastcall SetCursorMode_h(uint32_t mode) {
|
||||
g_captured = mode;
|
||||
return;
|
||||
}
|
||||
if (Cursor::Extended::Is(mode)) {
|
||||
Cursor::Extended::Show(mode);
|
||||
return;
|
||||
}
|
||||
SetCursorMode_o(mode);
|
||||
}
|
||||
|
||||
@@ -122,6 +133,17 @@ bool ImagePath(void *object, uint32_t mode, bool lowPriority, char *buf, std::si
|
||||
std::snprintf(buf, size, "%s", file);
|
||||
return true;
|
||||
}
|
||||
if (Cursor::Extended::Is(mode)) {
|
||||
// Retail shows no icon for a quest giver's cursor, not even the
|
||||
// low-priority pointer (checked in game, 2026-11-03, with the soft
|
||||
// interact icon and SoftTargetLowPriorityIcons 1). Blizzard_NamePlates
|
||||
// shows that icon exactly when SetUnitCursorTexture returns true, so
|
||||
// the refusal is the function's own.
|
||||
if (Cursor::Extended::IsQuest(mode))
|
||||
return false;
|
||||
std::snprintf(buf, size, "%s", Cursor::Extended::Path(mode));
|
||||
return true;
|
||||
}
|
||||
if (mode >= Offsets::CURSOR_MODE_CUSTOM)
|
||||
return false;
|
||||
const char *name = Game::Read<const char *>(Offsets::VAR_CURSOR_NAMES + 4 * mode);
|
||||
|
||||
+7
-6
@@ -18,9 +18,9 @@
|
||||
|
||||
namespace Cursor::Hover {
|
||||
|
||||
// The cursor mode (Offsets::CURSOR_MODE_*) the world cursor would show with
|
||||
// the mouse over `object` — a unit, player, game object or corpse — or 0
|
||||
// when it shows none. Computes it without showing it.
|
||||
// The cursor mode (Offsets::CURSOR_MODE_* or Cursor::Extended) the world
|
||||
// cursor would show with the mouse over `object` — a unit, player, game
|
||||
// object or corpse — or 0 when it shows none. Computes it without showing it.
|
||||
uint32_t Mode(void *object);
|
||||
|
||||
// For a game object's named cursor (CURSOR_MODE_CUSTOM): whether the player
|
||||
@@ -28,9 +28,10 @@ uint32_t Mode(void *object);
|
||||
bool IsUsable(void *object);
|
||||
|
||||
// Writes the image file of `mode` over `object` into `buf`. False, with
|
||||
// `buf` untouched, for no mode, and for the base (resting) mode unless
|
||||
// `lowPriority` — retail's low-priority image, the plain pointer over a unit
|
||||
// with nothing to do (SetUnitCursorTexture's includeLowPriority).
|
||||
// `buf` untouched, for no mode, for a quest giver's cursor (retail shows no
|
||||
// icon for one), and for the base (resting) mode unless `lowPriority` —
|
||||
// retail's low-priority image, the plain pointer over a unit with nothing to
|
||||
// do (SetUnitCursorTexture's includeLowPriority).
|
||||
bool ImagePath(void *object, uint32_t mode, bool lowPriority, char *buf, std::size_t size);
|
||||
|
||||
} // namespace Cursor::Hover
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
// 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/>.
|
||||
|
||||
// With `expandedCursors` at 1 (Cursor::Extended::Enabled), the cursor over an
|
||||
// NPC shows what it offers, as in later clients: an anvil for a repairer, a
|
||||
// hearthstone for an innkeeper, a horseshoe for a stable master, and a quest
|
||||
// giver's ! or ? (Cursor::Extended). At 0, the default, it stays 1.12's.
|
||||
//
|
||||
// 1.12 picks the cursor for a hovered unit in ClassifyHoveredUnit
|
||||
// (FUN_WORLD_FRAME_CLASSIFY_UNIT). For a unit the player can interact with,
|
||||
// its NPC flag chain takes the lowest flag bit first: gossip, quest giver,
|
||||
// vendor, flight master and so on. It never tests the repair flag, and shows
|
||||
// Interact for an innkeeper. 4.3.4's CursorTrackUnit first asks the quest
|
||||
// giver's status (GetQuestGiverCursor), then tests the services from repair
|
||||
// down, with vendor and gossip near the end.
|
||||
//
|
||||
// The thunk stands in for the head of 1.12's chain
|
||||
// (SITE_CLASSIFY_UNIT_NPC_FLAGS). Everything the engine decides before it
|
||||
// stays the engine's: mouselook, spell targeting, charm, whether the unit can
|
||||
// be interacted with now, and whether it is beyond the interact reach (EBX,
|
||||
// the Unable form). With the cvar at 0 the thunk does the instruction it
|
||||
// replaced and the stock chain runs. The pick goes to the engine's
|
||||
// SetCursorMode, whose hook (cursor/Hover) shows an extended mode and records
|
||||
// every mode while the soft-interact code asks for the hover verdict.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cursor/Extended.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Cursor::Npc {
|
||||
|
||||
namespace {
|
||||
|
||||
// UNIT_NPC_FLAGS bits, as 1.12's chain tests them (tortoise-wow
|
||||
// UnitDefines.h names them).
|
||||
constexpr uint32_t kGossip = 0x1;
|
||||
constexpr uint32_t kVendor = 0x4;
|
||||
constexpr uint32_t kFlightMaster = 0x8;
|
||||
constexpr uint32_t kTrainer = 0x10;
|
||||
constexpr uint32_t kSpiritHealer = 0x20;
|
||||
constexpr uint32_t kSpiritGuide = 0x40;
|
||||
constexpr uint32_t kInnkeeper = 0x80;
|
||||
constexpr uint32_t kBanker = 0x100;
|
||||
constexpr uint32_t kPetitioner = 0x200;
|
||||
constexpr uint32_t kTabardDesigner = 0x400;
|
||||
constexpr uint32_t kBattleMaster = 0x800;
|
||||
constexpr uint32_t kAuctioneer = 0x1000;
|
||||
constexpr uint32_t kStableMaster = 0x2000;
|
||||
constexpr uint32_t kRepair = 0x4000;
|
||||
|
||||
// Quest giver statuses (OFF_CGUNIT_QUEST_STATUS). 1.12 numbers them its own
|
||||
// way (tortoise-wow QuestDef.h); its status-to-marker table (0x80C454) shows
|
||||
// each one's meaning: 3 a grey ?, 4 a blue ?, 5 a yellow !, 6 and 7 a yellow
|
||||
// ?. 0 none, 1 unavailable (grey !) and 2 chat (no marker) have no cursor,
|
||||
// as 4.3.4's none and unavailable have none.
|
||||
constexpr uint32_t kStatusIncomplete = 3;
|
||||
constexpr uint32_t kStatusRewardRepeatable = 4;
|
||||
constexpr uint32_t kStatusAvailable = 5;
|
||||
constexpr uint32_t kStatusRewardOld = 6;
|
||||
constexpr uint32_t kStatusReward = 7;
|
||||
|
||||
using IsQuestGiverToTalk_t = uint32_t(__stdcall *)(void *unit);
|
||||
using SetCursorMode_t = void(__fastcall *)(uint32_t mode);
|
||||
|
||||
uintptr_t g_classifyReturn = Offsets::CLASSIFY_UNIT_RETURN;
|
||||
|
||||
// 4.3.4's quest cursor for a status (GetQuestGiverCursor), matched by the
|
||||
// status's meaning: 4.3.4 gives Quest to an available and to an incomplete
|
||||
// quest, QuestRepeatable to a repeatable reward (its blue ?), and
|
||||
// QuestTurnIn to a reward (its yellow ?).
|
||||
uint32_t QuestMode(uint32_t status) {
|
||||
switch (status) {
|
||||
case kStatusIncomplete:
|
||||
case kStatusAvailable:
|
||||
return Cursor::Extended::kQuest;
|
||||
case kStatusRewardRepeatable:
|
||||
return Cursor::Extended::kQuestRepeatable;
|
||||
case kStatusRewardOld:
|
||||
case kStatusReward:
|
||||
return Cursor::Extended::kQuestTurnIn;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 4.3.4's order (CursorTrackUnit) in 1.12's flag bits, or 0 for none.
|
||||
// 4.3.4's guild master, guild banker and spell click tests have no 1.12
|
||||
// flag, and its flight master test also reads an icon type; for that flag
|
||||
// 1.12 shows Taxi, as here. 4.3.4 also takes a cursor that the creature's
|
||||
// cache record names (IconName), which 1.12's creature query does not carry.
|
||||
// A stable master gets retail's StableMaster in 4.3.4's place for it, where
|
||||
// 4.3.4 shows Speak. Where 4.3.4 tests gossip, this also takes the quest
|
||||
// giver 1.12 would speak to (IsQuestGiverToTalk), so a status without a
|
||||
// quest cursor keeps 1.12's Speak.
|
||||
uint32_t ServiceMode(void *unit) {
|
||||
if (const uint32_t quest = QuestMode(Game::Read<uint32_t>(unit, Offsets::OFF_CGUNIT_QUEST_STATUS)))
|
||||
return quest;
|
||||
const uint32_t flags =
|
||||
Game::Read<uint32_t>(Game::Read<const void *>(unit, Offsets::OFF_UNIT_DESCRIPTOR),
|
||||
Offsets::OFF_UNIT_DESCRIPTOR_NPC_FLAGS);
|
||||
if (flags & kRepair)
|
||||
return Cursor::Extended::kRepairNpc;
|
||||
if (flags & kInnkeeper)
|
||||
return Cursor::Extended::kInnkeeper;
|
||||
if (flags & kFlightMaster)
|
||||
return Offsets::CURSOR_MODE_TAXI;
|
||||
if (flags & kTrainer)
|
||||
return Offsets::CURSOR_MODE_TRAINER;
|
||||
if (flags & (kSpiritHealer | kSpiritGuide))
|
||||
return Offsets::CURSOR_MODE_SPEAK;
|
||||
if (flags & kBanker)
|
||||
return Offsets::CURSOR_MODE_BUY;
|
||||
if (flags & (kPetitioner | kTabardDesigner | kBattleMaster))
|
||||
return Offsets::CURSOR_MODE_SPEAK;
|
||||
if (flags & kAuctioneer)
|
||||
return Offsets::CURSOR_MODE_BUY;
|
||||
if (flags & kStableMaster)
|
||||
return Cursor::Extended::kStableMaster;
|
||||
if (flags & kVendor)
|
||||
return Offsets::CURSOR_MODE_PICKUP;
|
||||
if ((flags & kGossip) ||
|
||||
reinterpret_cast<IsQuestGiverToTalk_t>(Offsets::FUN_IS_QUEST_GIVER_TO_TALK)(unit) != 0)
|
||||
return Offsets::CURSOR_MODE_SPEAK;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// True when it set the mode. `unable` is the engine's EBX: 1 beyond the
|
||||
// interact reach, else 0. A unit with no service gets the base mode, where
|
||||
// the stock chain ends too (ShowBaseCursor).
|
||||
bool __stdcall Pick(void *unit, uint32_t unable) {
|
||||
if (!Cursor::Extended::Enabled())
|
||||
return false;
|
||||
uint32_t mode = ServiceMode(unit);
|
||||
if (mode == 0)
|
||||
mode = Game::Read<uint32_t>(Offsets::VAR_CURSOR_BASE_MODE);
|
||||
else if (unable != 0)
|
||||
mode += Cursor::Extended::Is(mode) ? Cursor::Extended::kUnableOffset
|
||||
: Offsets::CURSOR_MODE_UNABLE_OFFSET;
|
||||
reinterpret_cast<SetCursorMode_t>(Offsets::FUN_SET_CURSOR_MODE)(mode);
|
||||
return true;
|
||||
}
|
||||
|
||||
static_assert(Offsets::OFF_UNIT_DESCRIPTOR == 0x110, "NpcFlagsThunk replays [esi + 0x110]");
|
||||
|
||||
// Stands in for `mov ecx, [esi + 0x110]`, the head of the chain: the unit in
|
||||
// ESI, the Unable flag in EBX. When Pick set the mode, it leaves the
|
||||
// classifier through the chain's epilogue; otherwise it does the instruction
|
||||
// it replaced and the chain runs.
|
||||
__declspec(naked) void NpcFlagsThunk() {
|
||||
__asm {
|
||||
push ebx
|
||||
push esi
|
||||
call Pick
|
||||
test al, al
|
||||
jnz picked
|
||||
mov ecx, dword ptr [esi + 110h]
|
||||
ret
|
||||
picked:
|
||||
add esp, 4
|
||||
jmp dword ptr [g_classifyReturn]
|
||||
}
|
||||
}
|
||||
|
||||
void Patch() {
|
||||
static const uint8_t kFlagsRead[6] = {0x8B, 0x8E, 0x10, 0x01, 0x00, 0x00};
|
||||
Common::ReplaceWithCall(Offsets::SITE_CLASSIFY_UNIT_NPC_FLAGS, kFlagsRead, sizeof kFlagsRead,
|
||||
reinterpret_cast<const void *>(&NpcFlagsThunk));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Patch};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Cursor::Npc
|
||||
+5
-3
@@ -25,11 +25,13 @@ const char *NormalizeEmpty(const char *s) {
|
||||
|
||||
} // namespace
|
||||
|
||||
// The id is compared unsigned: a DBC column's "none" is -1, which a signed
|
||||
// compare passes as less than the count and then indexes records[-1]. The
|
||||
// engine rejects it with a signed `jl` before its `jg` against the count
|
||||
// (0x00472797). An unloaded store's count is -1, so it takes no id.
|
||||
const uint8_t *Record(uintptr_t recordsVar, uintptr_t countVar, uint32_t id) {
|
||||
if (id == 0)
|
||||
return nullptr;
|
||||
const int count = *reinterpret_cast<const int *>(countVar);
|
||||
if (static_cast<int>(id) > count)
|
||||
if (id == 0 || count <= 0 || id > static_cast<uint32_t>(count))
|
||||
return nullptr;
|
||||
const uint8_t *const *records =
|
||||
*reinterpret_cast<const uint8_t *const *const *>(recordsVar);
|
||||
|
||||
+2
-1
@@ -39,7 +39,8 @@ namespace DBC {
|
||||
|
||||
// Resolves `id` to its record pointer in the table whose records-base
|
||||
// global is at `recordsVar` and count global at `countVar`. Returns
|
||||
// `nullptr` for `id == 0`, OOR ids (> count), and unpopulated slots.
|
||||
// `nullptr` for `id == 0`, OOR ids (> count, which includes -1 and every
|
||||
// other negative id), and unpopulated slots.
|
||||
// Caller reads whatever fields it needs off the returned record.
|
||||
//
|
||||
// Use this when `LocalizedField` / `StringField` don't fit — e.g. for
|
||||
|
||||
@@ -206,9 +206,8 @@ void RegisterLuaFunctions() {
|
||||
&Script_GetSelfResurrectOptions, &kGetSelfResurrectOptions);
|
||||
Game::Lua::RegisterTableFunction("C_DeathInfo", "UseSelfResurrectOption",
|
||||
&Script_UseSelfResurrectOption, &kUseSelfResurrectOption);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "SelfResurrectOptionType", kOptionTypeEntries,
|
||||
sizeof(kOptionTypeEntries) / sizeof(kOptionTypeEntries[0]),
|
||||
"DeathInfo");
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "SelfResurrectOptionType", kOptionTypeEntries, "DeathInfo");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <iterator>
|
||||
|
||||
namespace Duration::Object {
|
||||
|
||||
@@ -451,8 +450,7 @@ const Game::Doc::Function kCreateDuration{"Creates a zero duration object that c
|
||||
void RegisterLuaFunctions() {
|
||||
CreateMetatable(Lua::State());
|
||||
Lua::RegisterTableFunction("C_DurationUtil", "CreateDuration", &Script_CreateDuration, &kCreateDuration);
|
||||
Lua::RegisterIntegerEnum("Enum", "DurationTimeModifier", kModifiers, static_cast<int>(std::size(kModifiers)),
|
||||
"LuaDurationObjectShared");
|
||||
Lua::RegisterIntegerEnum("Enum", "DurationTimeModifier", kModifiers, "LuaDurationObjectShared");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -780,8 +779,8 @@ void RegisterLuaFunctions() {
|
||||
CreateMetatable(Lua::State());
|
||||
Lua::RegisterTableFunction("C_DurationUtil", "CreateDurationTextBinding", &Script_CreateDurationTextBinding,
|
||||
&kCreateDurationTextBinding);
|
||||
Lua::RegisterIntegerEnum("Enum", "DurationTextBindingProperty", kProperties,
|
||||
static_cast<int>(std::size(kProperties)), "DurationTextBindingShared");
|
||||
Lua::RegisterIntegerEnum(
|
||||
"Enum", "DurationTextBindingProperty", kProperties, "DurationTextBindingShared");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -125,8 +125,8 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -83,9 +83,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
|
||||
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Layout.h"
|
||||
#include "text/InlineTexture.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -59,21 +60,17 @@ using IndexAtPoint_t = void(__fastcall *)(void *editBox, void *edx, float x, flo
|
||||
using FontHeight_t = float(__fastcall *)(void *fs, void *edx, int mode);
|
||||
using MeasureRun_t = float(__fastcall *)(void *fs, void *edx, const uint8_t *text, int len);
|
||||
using PixelAlignY_t = float(__stdcall *)(float length);
|
||||
using GetRect_t = int(__fastcall *)(void *anchor, void *edx, float *out);
|
||||
using ClearAllPoints_t = void(__fastcall *)(void *anchor);
|
||||
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint, float x, float y,
|
||||
int flag);
|
||||
using Resize_t = void(__fastcall *)(void *anchor, void *edx, int force);
|
||||
using Extent_t = float(__fastcall *)(void *anchor, void *edx);
|
||||
using ScreenAspect_t = float(__cdecl *)();
|
||||
using DivideByScreenScaleX_t = float(__stdcall *)(float value);
|
||||
using FireVariadic_t = void(__cdecl *)(void *frame, uint32_t *slotPtr, const char *fmt, ...);
|
||||
|
||||
PlaceCaret_t g_origPlaceCaret = nullptr;
|
||||
PlaceSelectionPiece_t g_origPlaceSelectionPiece = nullptr;
|
||||
IndexAtPoint_t g_origIndexAtPoint = nullptr;
|
||||
|
||||
void *AnchorOf(void *region) { return static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR; }
|
||||
using Frame::Layout::Of;
|
||||
|
||||
// A multi-line box's rows: its label, the buffer the line table indexes (the
|
||||
// '*' copy of a password box) and the table — each line's first byte, the
|
||||
@@ -179,9 +176,9 @@ bool AnchorX(const Rows &rows, int line, uint32_t pos, float &x, int &relPoint)
|
||||
|
||||
// Anchors `region` as the placer did, at `y`.
|
||||
void Anchor(const Rows &rows, void *region, int relPoint, float x, float y, int flag) {
|
||||
void *anchor = AnchorOf(region);
|
||||
void *anchor = Of(region);
|
||||
reinterpret_cast<ClearAllPoints_t>(Offsets::FUN_LAYOUT_CLEAR_ALL_POINTS)(anchor);
|
||||
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, Offsets::FRAMEPOINT_TOPLEFT, AnchorOf(rows.fs),
|
||||
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, Offsets::FRAMEPOINT_TOPLEFT, Of(rows.fs),
|
||||
relPoint, x, y, flag);
|
||||
}
|
||||
|
||||
@@ -192,7 +189,7 @@ void __fastcall IndexAtPoint_h(void *editBox, void *edx, float x, float y, int *
|
||||
if (!MultiLineRows(editBox, rows))
|
||||
return g_origIndexAtPoint(editBox, edx, x, y, index);
|
||||
float rect[4] = {0.0f, 0.0f, 0.0f, 0.0f}; // bottom, left, top, right
|
||||
if (reinterpret_cast<GetRect_t>(Offsets::FUN_REGION_GET_RECT)(AnchorOf(rows.fs), nullptr, rect) == 0)
|
||||
if (!Frame::Layout::GetRect(rows.fs, rect))
|
||||
return g_origIndexAtPoint(editBox, edx, x, y, index);
|
||||
// The engine walks rows down from the label's top while the click is
|
||||
// below the row (strictly), stopping at the last line. Walk the rows as
|
||||
@@ -240,7 +237,7 @@ void __fastcall PlaceSelectionPiece_h(void *editBox, void *edx, void *piece, int
|
||||
return;
|
||||
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
|
||||
Anchor(rows, piece, relPoint, x, y, 0);
|
||||
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(AnchorOf(piece), nullptr, 1);
|
||||
Frame::Layout::Resize(piece, true);
|
||||
}
|
||||
|
||||
// --- the caret ----------------------------------------------------------------
|
||||
@@ -276,15 +273,12 @@ void __fastcall PlaceCaret_h(void *editBox) {
|
||||
return;
|
||||
// OnCursorChanged(x, y, width, height) as the engine fires it: each
|
||||
// value by the anchor→script factor, pushed as a double.
|
||||
void *anchor = AnchorOf(caret);
|
||||
void *anchor = Of(caret);
|
||||
void **vtable = *static_cast<void ***>(anchor);
|
||||
const float width = reinterpret_cast<Extent_t>(vtable[7])(anchor, nullptr); // +0x1C GetWidth
|
||||
const float height = reinterpret_cast<Extent_t>(vtable[8])(anchor, nullptr); // +0x20 GetHeight
|
||||
const auto aspect = reinterpret_cast<ScreenAspect_t>(Offsets::FUN_COORD_GET_SCALED_SCREEN_ASPECT);
|
||||
const auto divide = reinterpret_cast<DivideByScreenScaleX_t>(Offsets::FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X);
|
||||
const float factor = Game::Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE);
|
||||
const auto push = [&](float value) { return static_cast<double>(divide(aspect() * factor * value)); };
|
||||
const double args[4] = {push(x), push(y), push(width), push(height)};
|
||||
const double args[4] = {Game::InternalToPixel(x), Game::InternalToPixel(y), Game::InternalToPixel(width),
|
||||
Game::InternalToPixel(height)};
|
||||
void *L = Game::Lua::State();
|
||||
const int savedTop = (L != nullptr) ? Game::Lua::GetTop(L) : 0;
|
||||
reinterpret_cast<FireVariadic_t>(Offsets::FUN_FRAME_RUN_SCRIPT_VARIADIC)(
|
||||
|
||||
+1
-3
@@ -86,9 +86,7 @@ void RegisterLuaFunctions() {
|
||||
Common::RedirectCall(Offsets::CALL_EDITBOX_IME_COMPOSITION_INSERT, Offsets::FUN_EDITBOX_INSERT,
|
||||
reinterpret_cast<const void *>(&CompositionInsert));
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
|
||||
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -30,8 +30,7 @@ const Game::Lua::EnumIntegerEntry kEntries[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "InputContext", kEntries,
|
||||
sizeof(kEntries) / sizeof(kEntries[0]), "InputConstants");
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "InputContext", kEntries, "InputConstants");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -234,9 +234,7 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY),
|
||||
g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY), g_methods);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
## Interface: 11200
|
||||
## Title: !!!ClassicAPI
|
||||
## Notes: Library to emulate aspects of the modern WoW API on 1.12.1 / Lua 5.0.
|
||||
## Notes-ruRU: Библиотека, эмулирующая аспекты современного API WoW на 1.12.1 / Lua 5.0.
|
||||
## Version: DEV
|
||||
## Author: Brues
|
||||
## X-Website: https://github.com/brues-code/ClassicAPI
|
||||
@@ -143,6 +144,18 @@ FrameXML\ColorSwatch.lua
|
||||
FrameXML\ColorSwatch.xml
|
||||
FrameXML\ScriptErrorsFrame.lua
|
||||
FrameXML\ScriptErrorsFrame.xml
|
||||
FrameXML\ModelScene\CameraRegistry.lua
|
||||
FrameXML\ModelScene\CameraBaseMixin.lua
|
||||
FrameXML\ModelScene\OrbitCameraMixin.lua
|
||||
FrameXML\ModelScene\FollowCameraMixin.lua
|
||||
FrameXML\ModelScene\ModelSceneMixin.lua
|
||||
FrameXML\ModelScene\ModelSceneMixin.xml
|
||||
FrameXML\ModelScene\ModelSceneActorMixin.lua
|
||||
FrameXML\ModelScene\ModelSceneActorMixin.xml
|
||||
FrameXML\ModelScene\ModelSceneControlFrame.lua
|
||||
FrameXML\ModelScene\ModelSceneControlFrame.xml
|
||||
FrameXML\DressUpFrame.xml
|
||||
FrameXML\DressUpFrame.lua
|
||||
|
||||
Menu\MenuConstants.lua
|
||||
Menu\Classic\MenuConstants.lua
|
||||
|
||||
@@ -24,3 +24,22 @@ end
|
||||
function C_AutoComplete.GetAutoCompleteRealms()
|
||||
return {}
|
||||
end
|
||||
|
||||
-- C_ModelInfo's getters come from the DLL. Retail's active-scene bookkeeping
|
||||
-- "does nothing in public clients" (its own documentation).
|
||||
function C_ModelInfo.AddActiveModelScene(modelSceneFrame, modelSceneID)
|
||||
end
|
||||
|
||||
function C_ModelInfo.AddActiveModelSceneActor(modelSceneFrameActor, modelSceneActorID)
|
||||
end
|
||||
|
||||
function C_ModelInfo.ClearActiveModelScene(modelSceneFrame)
|
||||
end
|
||||
|
||||
function C_ModelInfo.ClearActiveModelSceneActor(modelSceneFrameActor)
|
||||
end
|
||||
|
||||
-- This client has no alternate player forms.
|
||||
function C_PlayerInfo.GetAlternateFormInfo()
|
||||
return false, false
|
||||
end
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
-- 4.3.4's DressUpItemLink (FrameXML/ModelFrames.lua): the whole link goes to
|
||||
-- the model, so DressUpModel:TryOn shows the link's enchant glow, and an item
|
||||
-- the model cannot wear does not open the window. 1.12.1's version passes
|
||||
-- only the item ID.
|
||||
--
|
||||
-- Differences from the 4.3.4 source:
|
||||
-- - The link goes through DressUpItem, which 4.3.4 folded in. Here
|
||||
-- Blizzard_AuctionUI replaces DressUpItem to show its own model, and
|
||||
-- some clients add steps to it (Turtle dresses the tabard).
|
||||
-- - An uncached item skips the IsDressableItem test, which reads only the
|
||||
-- cache. 1.12.1 opens no tooltip before a chat link's Control-click, so
|
||||
-- the test would drop most chat links; TryOn loads the item instead.
|
||||
-- - An item the model cannot wear goes on to DressUpMountLink, retail's
|
||||
-- next step in DressUpLink: 1.12.1's FrameXML (ItemRef, bags, loot, the
|
||||
-- auction house) calls DressUpItemLink alone.
|
||||
-- - It returns true when it showed the item or its mount, and false for a
|
||||
-- link of another type, as retail's does.
|
||||
|
||||
-- A link DressUpItemLink takes: an item link, or anything that is not a
|
||||
-- link (an item ID or name).
|
||||
local function IsItemLink(link)
|
||||
local linkType = type(link) == "string" and LinkUtil.ExtractLink(link);
|
||||
return not linkType or linkType == "item";
|
||||
end
|
||||
|
||||
-- 4.3.4's DressUpItemLink body: the item, when the model can wear it.
|
||||
local function DressUpWearable(link)
|
||||
if ( C_Item.IsItemDataCachedByID(link) and not IsDressableItem(link) ) then
|
||||
return false;
|
||||
end
|
||||
DressUpItem(link);
|
||||
return true;
|
||||
end
|
||||
|
||||
function DressUpItemLink(link)
|
||||
if ( not link or not IsItemLink(link) ) then
|
||||
return false;
|
||||
end
|
||||
return DressUpWearable(link) or DressUpMountLink(link);
|
||||
end
|
||||
|
||||
-- Retail's DressUpLink (Mainline/DressUpFrames.lua), without battle pets.
|
||||
function DressUpLink(link, forcedFrame)
|
||||
return link and ((IsItemLink(link) and DressUpWearable(link)) or DressUpMountLink(link, forcedFrame));
|
||||
end
|
||||
|
||||
-- Retail's mount preview (Mainline/DressUpFrames.lua DressUpMountLink and
|
||||
-- DressUpMount, Blizzard_SharedXMLGame/DressUpModelFrameMixin.lua's mode and
|
||||
-- last link) on 1.12.1's dressing room. Mount mode shows DressUpFrameModelScene
|
||||
-- in place of DressUpModel and hides the Reset button, as retail's SetMode
|
||||
-- hides it outside player mode; DressUpItem goes back to player mode.
|
||||
--
|
||||
-- Differences from the retail source:
|
||||
-- - DressUpFrame keeps its own background (retail switches to the warrior
|
||||
-- one) and has no view insets to set (retail sets them to 0, the default).
|
||||
-- - There are no mount links, so a link never sets the model itself, and
|
||||
-- shouldSetModelFromHyperlink changes nothing.
|
||||
-- - A mount whose creature is not cached yet returns false, as retail does
|
||||
-- with no display, and opens when CREATURE_DATA_LOAD_RESULT reports the
|
||||
-- creature: retail has every display on disk, this client asks the server.
|
||||
|
||||
DressUpFrame.ModelScene = DressUpFrameModelScene;
|
||||
DressUpFrame.ModelScene.ControlFrame:SetModelScene(DressUpFrame.ModelScene);
|
||||
|
||||
function DressUpFrame:GetLastLink()
|
||||
return self.lastLink;
|
||||
end
|
||||
|
||||
function DressUpFrame:SetLastLink(link)
|
||||
self.lastLink = link;
|
||||
end
|
||||
|
||||
function DressUpFrame:GetMode()
|
||||
return self.mode;
|
||||
end
|
||||
|
||||
function DressUpFrame:SetMode(mode)
|
||||
self.mode = mode;
|
||||
local inPlayerMode = mode ~= "mount";
|
||||
DressUpModel:SetShown(inPlayerMode);
|
||||
DressUpFrameResetButton:SetShown(inPlayerMode);
|
||||
self.ModelScene:SetShown(not inPlayerMode);
|
||||
end
|
||||
|
||||
DressUpFrame.ModelScene:SetResetCallback(function()
|
||||
local link = DressUpFrame:GetLastLink();
|
||||
if link then
|
||||
DressUpLink(link, DressUpFrame);
|
||||
end
|
||||
end);
|
||||
|
||||
-- The stock DressUpItem sets the unit only when it opens the window, and the
|
||||
-- model was hidden while a mount showed.
|
||||
local originalDressUpItem = DressUpItem;
|
||||
function DressUpItem(item)
|
||||
if ( DressUpFrame:GetMode() == "mount" ) then
|
||||
DressUpFrame:SetMode("player");
|
||||
DressUpFrame:SetLastLink(nil);
|
||||
if ( DressUpFrame:IsVisible() ) then
|
||||
DressUpModel:SetUnit("player");
|
||||
end
|
||||
end
|
||||
return originalDressUpItem(item);
|
||||
end
|
||||
|
||||
function DressUpMountLink(link, forcedFrame)
|
||||
if( link ) then
|
||||
local mountID = 0;
|
||||
|
||||
local linkType, linkOptions = LinkUtil.ExtractLink(link);
|
||||
if linkType == "item" then
|
||||
local linkID = LinkUtil.SplitLinkOptions(linkOptions);
|
||||
mountID = C_MountJournal.GetMountFromItem(tonumber(linkID));
|
||||
elseif linkType == "spell" then
|
||||
local linkID = LinkUtil.SplitLinkOptions(linkOptions);
|
||||
mountID = C_MountJournal.GetMountFromSpell(tonumber(linkID));
|
||||
end
|
||||
|
||||
if ( mountID ) then
|
||||
return DressUpMount(mountID, forcedFrame, false, link);
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local pendingMount;
|
||||
|
||||
-- The creature GetMountInfoExtraByID loads for a mount: a mount ID is its
|
||||
-- spell ID, and the mount aura's misc value is the creature.
|
||||
local SPELL_AURA_MOUNTED = 78;
|
||||
local function MountCreatureID(mountID)
|
||||
local effects = C_Spell.GetSpellEffectInfo(mountID);
|
||||
if ( effects ) then
|
||||
for _, effect in ipairs(effects) do
|
||||
if ( effect.auraName == SPELL_AURA_MOUNTED ) then
|
||||
return effect.miscValue;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function DressUpMount(mountID, forcedFrame, shouldSetModelFromHyperlink, link)
|
||||
if ( not mountID or mountID == 0 ) then
|
||||
return false;
|
||||
end
|
||||
|
||||
local creatureDisplayID, _, _, isSelfMount, _, modelSceneID, animID, spellVisualKitID, disablePlayerMountPreview = C_MountJournal.GetMountInfoExtraByID(mountID);
|
||||
if ( not creatureDisplayID ) then
|
||||
pendingMount = { mountID = mountID, creatureID = MountCreatureID(mountID), forcedFrame = forcedFrame, link = link };
|
||||
return false;
|
||||
end
|
||||
pendingMount = nil;
|
||||
|
||||
local frame = forcedFrame or DressUpFrame;
|
||||
|
||||
--Show the frame
|
||||
if ( not frame:IsShown() or frame:GetMode() ~= "mount" ) then
|
||||
frame:SetMode("mount");
|
||||
ShowUIPanel(frame);
|
||||
end
|
||||
frame:SetLastLink(link);
|
||||
frame.ModelScene:ClearScene();
|
||||
local forceEvenIfSame = true;
|
||||
frame.ModelScene:TransitionToModelSceneID(modelSceneID, CAMERA_TRANSITION_TYPE_IMMEDIATE, CAMERA_MODIFICATION_TYPE_DISCARD, forceEvenIfSame);
|
||||
|
||||
local mountActor = frame.ModelScene:GetActorByTag("unwrapped");
|
||||
if mountActor then
|
||||
mountActor:SetModelByCreatureDisplayID(creatureDisplayID);
|
||||
|
||||
-- mount self idle animation
|
||||
if (isSelfMount) then
|
||||
mountActor:SetAnimationBlendOperation(Enum.ModelBlendOperation.None);
|
||||
mountActor:SetAnimation(618); -- MountSelfIdle
|
||||
else
|
||||
mountActor:SetAnimationBlendOperation(Enum.ModelBlendOperation.Anim);
|
||||
mountActor:SetAnimation(0);
|
||||
end
|
||||
frame.ModelScene:AttachPlayerToMount(mountActor, animID, isSelfMount, disablePlayerMountPreview);
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
local creatureLoadFrame = CreateFrame("Frame");
|
||||
creatureLoadFrame:RegisterEvent("CREATURE_DATA_LOAD_RESULT");
|
||||
creatureLoadFrame:SetScript("OnEvent", function(self, event, creatureID, success)
|
||||
local pending = pendingMount;
|
||||
if ( not pending or creatureID ~= pending.creatureID ) then
|
||||
return;
|
||||
end
|
||||
pendingMount = nil;
|
||||
if ( success ) then
|
||||
DressUpMount(pending.mountID, pending.forcedFrame, false, pending.link);
|
||||
end
|
||||
end);
|
||||
|
||||
-- Control-click on a mount in the spellbook previews it. Retail has no such
|
||||
-- click (its mounts are in the mount journal); this client keeps mounts in
|
||||
-- the spellbook, where stock 1.12.1 casts on a Control-click. A mount spell
|
||||
-- never casts here, even while its creature loads; any other spell does.
|
||||
local originalSpellButtonOnClick = SpellButton_OnClick;
|
||||
function SpellButton_OnClick(drag)
|
||||
if ( not drag and IsControlKeyDown() and SpellBookFrame.bookType ~= BOOKTYPE_PET ) then
|
||||
local id = SpellBook_GetSpellID(this:GetID());
|
||||
local link = id <= MAX_SPELLS and GetSpellLink(id, SpellBookFrame.bookType);
|
||||
if ( link and C_MountJournal.GetMountFromSpell(link) ) then
|
||||
this:SetChecked("false");
|
||||
DressUpMountLink(link);
|
||||
return;
|
||||
end
|
||||
end
|
||||
return originalSpellButtonOnClick(drag);
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
<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">
|
||||
<!-- The dressing room's mount mode (DressUpFrame.lua): a scene shown in
|
||||
place of DressUpModel, with retail's control frame. Like retail's, it
|
||||
fills the backdrop above the buttons: the four DressUpBackground
|
||||
textures, cut off at the top of the Reset / Close row (centred 422
|
||||
below the frame top, 22 tall, so 48 above the backdrop's bottom).
|
||||
DressUpModel's own rect is no guide: it is a scale 2 frame that
|
||||
reaches past the backdrop and over the buttons. -->
|
||||
<ModelScene name="DressUpFrameModelScene" parent="DressUpFrame" inherits="ModelSceneMixinTemplate" hidden="true">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="DressUpBackgroundTopLeft"/>
|
||||
<Anchor point="BOTTOMRIGHT" relativeTo="DressUpBackgroundBotRight">
|
||||
<Offset>
|
||||
<AbsDimension x="0" y="48"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<Frame parentKey="ControlFrame" inherits="ModelSceneControlFrameTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOP" x="0" y="-10"/>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
</Frames>
|
||||
</ModelScene>
|
||||
</Ui>
|
||||
@@ -0,0 +1,103 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/ModelSceneCameras/CameraBaseMixin.lua.
|
||||
|
||||
|
||||
CameraBaseMixin = {};
|
||||
|
||||
function CameraBaseMixin:SetOwningScene(owningScene)
|
||||
if self.owningScene ~= owningScene then
|
||||
if self.owningScene then
|
||||
self:OnRemoved();
|
||||
end
|
||||
|
||||
self.owningScene = owningScene;
|
||||
|
||||
if self.owningScene then
|
||||
self:OnAdded();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CameraBaseMixin:GetOwningScene()
|
||||
return self.owningScene;
|
||||
end
|
||||
|
||||
CAMERA_TRANSITION_TYPE_IMMEDIATE = 1; -- Switch to the camera view instantly
|
||||
CAMERA_TRANSITION_TYPE_INTERPOLATION = 2; -- Switch to the camera view by interpolating from the current view
|
||||
|
||||
CAMERA_MODIFICATION_TYPE_DISCARD = 1; -- Discard any modifications performed to the view by script
|
||||
CAMERA_MODIFICATION_TYPE_MAINTAIN = 2; -- Retain any modifications performed to the view by script
|
||||
|
||||
-- Override this to handle transitions to a modelSceneCamera data set
|
||||
function CameraBaseMixin:ApplyFromModelSceneCameraInfo(modelSceneCameraInfo, transitionType, modificationType)
|
||||
|
||||
end
|
||||
|
||||
-- Common accessors from the owning scene
|
||||
function CameraBaseMixin:IsLeftMouseButtonDown()
|
||||
return self:GetOwningScene():IsLeftMouseButtonDown();
|
||||
end
|
||||
|
||||
function CameraBaseMixin:IsRightMouseButtonDown()
|
||||
return self:GetOwningScene():IsRightMouseButtonDown();
|
||||
end
|
||||
|
||||
function CameraBaseMixin:SetPosition(x, y, z)
|
||||
self:GetOwningScene():SetCameraPosition(x, y, z);
|
||||
end
|
||||
|
||||
function CameraBaseMixin:GetPosition()
|
||||
return self:GetOwningScene():GetCameraPosition();
|
||||
end
|
||||
|
||||
function CameraBaseMixin:GetForwardVector()
|
||||
return self:GetOwningScene():GetCameraForward();
|
||||
end
|
||||
|
||||
function CameraBaseMixin:GetRightVector()
|
||||
return self:GetOwningScene():GetCameraRight();
|
||||
end
|
||||
|
||||
function CameraBaseMixin:GetUpVector()
|
||||
return self:GetOwningScene():GetCameraUp();
|
||||
end
|
||||
|
||||
-- Override this with a uniquely identifying name for the camera type
|
||||
function CameraBaseMixin:GetCameraType()
|
||||
return "UnknownCamera";
|
||||
end
|
||||
|
||||
-- Override this to perform any per-frame updates
|
||||
function CameraBaseMixin:OnUpdate(elapsed)
|
||||
end
|
||||
|
||||
-- Override this with code to synchronize the camera state with the model scene API, may be called by the ModelScene to synchronize deferred camera state in case it needs to be queried
|
||||
function CameraBaseMixin:SynchronizeCamera()
|
||||
end
|
||||
|
||||
-- Override this to be notified when the mouse is down on the model scene
|
||||
function CameraBaseMixin:OnMouseDown(button)
|
||||
end
|
||||
|
||||
-- Override this to be notified when the mouse is up on the model scene
|
||||
function CameraBaseMixin:OnMouseUp(button)
|
||||
end
|
||||
|
||||
-- Override this to be notified when the mouse wheel is changed on the model scene
|
||||
function CameraBaseMixin:OnMouseWheel(delta)
|
||||
end
|
||||
|
||||
-- Override this to be notified when the camera is added to a scene
|
||||
function CameraBaseMixin:OnAdded()
|
||||
end
|
||||
|
||||
-- Override this to be notified when the camera is remove from a scene
|
||||
function CameraBaseMixin:OnRemoved()
|
||||
end
|
||||
|
||||
-- Override this to be notified when the camera becomes activated
|
||||
function CameraBaseMixin:OnActivated()
|
||||
end
|
||||
|
||||
-- Override this to be notified when the camera becomes deactivated
|
||||
function CameraBaseMixin:OnDeactivated()
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/ModelSceneCameras/CameraRegistry.lua.
|
||||
|
||||
|
||||
CameraRegistry = { cameraTypeToFactoryFunction = {}, };
|
||||
|
||||
function CameraRegistry:AddCameraFactory(cameraTypeName, factoryFunction)
|
||||
self.cameraTypeToFactoryFunction[cameraTypeName] = factoryFunction;
|
||||
end
|
||||
|
||||
function CameraRegistry:AddCameraFactoryFromMixin(cameraTypeName, mixin)
|
||||
self:AddCameraFactory(cameraTypeName, function() return CreateFromMixins(mixin); end);
|
||||
end
|
||||
|
||||
function CameraRegistry:CreateCameraByType(cameraTypeName)
|
||||
if self.cameraTypeToFactoryFunction[cameraTypeName] then
|
||||
return self.cameraTypeToFactoryFunction[cameraTypeName]();
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,133 @@
|
||||
-- ClassicAPI extension: an orbit camera that follows an actor and stops in
|
||||
-- front of the WMO between the actor and the eye.
|
||||
|
||||
--[[
|
||||
FollowCameraMixin - An OrbitCamera whose target is an actor's pivot: its position, raised by the pivot height.
|
||||
|
||||
The pivot height is the one the world's follow camera eases to for a unit (actor:GetCameraPivotHeight), and
|
||||
it eases the same way: the camera's first height is taken at once (BeginHeightTransition 0x00512790), and
|
||||
each new one is armed (ArmHeight 0x005126B0) for |change| / cameraHeightSmoothSpeed seconds and eased with
|
||||
the camera's cosine curve (0x0050F160). Until the actor's model has loaded, the camera waits, as the world's
|
||||
does (0x0050E900).
|
||||
|
||||
The eye is placed the way the world's follow camera places it (the boom sweep of its solver 0x0050E570):
|
||||
a trace of the camera's faces from the pivot to the eye (0x0050E816) shortens the distance, which then
|
||||
backs off 1/9 yard (0x0050E871 to 0x0050E8A4). With no actor to follow, it is a plain OrbitCamera.
|
||||
|
||||
Create it beside the scene's own camera and give it that camera's settings:
|
||||
local _, cameraIDs = C_ModelInfo.GetModelSceneInfoByID(modelSceneID);
|
||||
local camera = scene:AddCamera(CameraRegistry:CreateCameraByType("FollowCamera"));
|
||||
camera:ApplyFromModelSceneCameraInfo(C_ModelInfo.GetModelSceneCameraInfoByID(cameraIDs[1]), CAMERA_TRANSITION_TYPE_IMMEDIATE, CAMERA_MODIFICATION_TYPE_DISCARD);
|
||||
camera:SetFollowActor(actor);
|
||||
scene:SetActiveCamera(camera);
|
||||
]]
|
||||
|
||||
FollowCameraMixin = CreateFromMixins(OrbitCameraMixin);
|
||||
|
||||
local CAMERA_NAME = "FollowCamera";
|
||||
CameraRegistry:AddCameraFactoryFromMixin(CAMERA_NAME, FollowCameraMixin);
|
||||
|
||||
-- The solver skips the sweep when the distance is not past the near clip by
|
||||
-- more than this (0x008026BC, 2^-20).
|
||||
local SWEEP_MIN_DISTANCE = 9.5367431640625e-07;
|
||||
-- A distance shortened by more than this (0x008029D4, 2^-22) backs off by
|
||||
-- BOOM_BACKOFF (0x00808A00, 1/9 yard), to no less than 0.
|
||||
local SHORTENED_EPSILON = 2.384185791015625e-07;
|
||||
local BOOM_BACKOFF = 1 / 9;
|
||||
-- A height within this of the armed or the live one arms nothing (0x00801360).
|
||||
local ARM_EPSILON = 0.001;
|
||||
-- The ease runs while the live height is this far or more from its target
|
||||
-- (0x008029D4, 2^-22).
|
||||
local EASE_EPSILON = 2.384185791015625e-07;
|
||||
|
||||
-- "public" functions
|
||||
function FollowCameraMixin:GetCameraType() -- override
|
||||
return CAMERA_NAME;
|
||||
end
|
||||
|
||||
-- The target becomes the actor's pivot. Nil stops following.
|
||||
function FollowCameraMixin:SetFollowActor(actor)
|
||||
self.followActor = actor;
|
||||
end
|
||||
|
||||
function FollowCameraMixin:GetFollowActor()
|
||||
return self.followActor;
|
||||
end
|
||||
|
||||
-- "private" functions
|
||||
|
||||
-- ArmHeight (0x005126B0) with no delay and a factor of 1, and its inner
|
||||
-- BeginHeightTransition (0x00512790).
|
||||
function FollowCameraMixin:ArmPivotHeight(height, now)
|
||||
local live = self.pivotHeight or 0;
|
||||
if self.pivotEasing and math.abs(self.pivotTarget - height) < ARM_EPSILON then
|
||||
return;
|
||||
end
|
||||
if math.abs(live - height) < ARM_EPSILON then
|
||||
return;
|
||||
end
|
||||
self.pivotTarget = height;
|
||||
if not self.pivotSnapped then
|
||||
self.pivotSnapped = true;
|
||||
self.pivotHeight = height;
|
||||
self.pivotEasing = false;
|
||||
return;
|
||||
end
|
||||
self.pivotFrom = live;
|
||||
self.pivotStart = now;
|
||||
self.pivotDuration = math.abs(height - live) / tonumber(GetCVar("cameraHeightSmoothSpeed"));
|
||||
self.pivotEasing = true;
|
||||
end
|
||||
|
||||
-- The height channel of the camera's ease (0x0050F160).
|
||||
function FollowCameraMixin:EasePivotHeight(now)
|
||||
if math.abs(self.pivotTarget - self.pivotHeight) < EASE_EPSILON then
|
||||
self.pivotEasing = false;
|
||||
return;
|
||||
end
|
||||
if not self.pivotEasing or self.pivotStart > now then
|
||||
return;
|
||||
end
|
||||
local t = (now - self.pivotStart) / self.pivotDuration;
|
||||
if t >= 1 then
|
||||
self.pivotHeight = self.pivotTarget;
|
||||
else
|
||||
self.pivotHeight = self.pivotFrom + (self.pivotTarget - self.pivotFrom) * (1 - math.cos(math.pi * t)) * 0.5;
|
||||
end
|
||||
end
|
||||
|
||||
function FollowCameraMixin:OnUpdate(elapsed) -- override
|
||||
local actor = self.followActor;
|
||||
if actor then
|
||||
local height = actor:GetCameraPivotHeight(self:GetZoomDistance());
|
||||
if not height then
|
||||
return;
|
||||
end
|
||||
local now = GetTime();
|
||||
self:ArmPivotHeight(height, now);
|
||||
self:EasePivotHeight(now);
|
||||
local x, y, z = actor:GetPosition();
|
||||
self:SetTarget(x, y, z + self.pivotHeight);
|
||||
end
|
||||
OrbitCameraMixin.OnUpdate(self, elapsed);
|
||||
end
|
||||
|
||||
function FollowCameraMixin:CalculatePositionByDistanceFromTarget(targetX, targetY, targetZ, zoomDistance, axisAngleX, axisAngleY, axisAngleZ) -- override
|
||||
if not self.followActor then
|
||||
return OrbitCameraMixin.CalculatePositionByDistanceFromTarget(self, targetX, targetY, targetZ, zoomDistance, axisAngleX, axisAngleY, axisAngleZ);
|
||||
end
|
||||
|
||||
local scene = self:GetOwningScene();
|
||||
local distance = zoomDistance;
|
||||
if distance - scene:GetCameraNearClip() > SWEEP_MIN_DISTANCE then
|
||||
local eyeX, eyeY, eyeZ = OrbitCameraMixin.CalculatePositionByDistanceFromTarget(self, targetX, targetY, targetZ, distance, axisAngleX, axisAngleY, axisAngleZ);
|
||||
local _, _, _, _, _, _, fraction = scene:TraceSegment(targetX, targetY, targetZ, eyeX, eyeY, eyeZ, true);
|
||||
if fraction then
|
||||
distance = distance * fraction;
|
||||
end
|
||||
if math.abs(zoomDistance - distance) > SHORTENED_EPSILON then
|
||||
distance = math.max(distance - BOOM_BACKOFF, 0);
|
||||
end
|
||||
end
|
||||
return OrbitCameraMixin.CalculatePositionByDistanceFromTarget(self, targetX, targetY, targetZ, distance, axisAngleX, axisAngleY, axisAngleZ);
|
||||
end
|
||||
@@ -0,0 +1,144 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/ModelSceneActorMixin.lua.
|
||||
|
||||
|
||||
ModelSceneActorMixin = {};
|
||||
|
||||
-- "public" methods
|
||||
function ModelSceneActorMixin:ApplyFromModelSceneActorInfo(actorInfo)
|
||||
self:SetUseCenterForOrigin(actorInfo.useCenterForOriginX, actorInfo.useCenterForOriginY, actorInfo.useCenterForOriginZ);
|
||||
|
||||
self:SetPosition(actorInfo.position:GetXYZ());
|
||||
self:SetYaw(actorInfo.yaw);
|
||||
self:SetPitch(actorInfo.pitch);
|
||||
self:SetRoll(actorInfo.roll);
|
||||
|
||||
self.requestedScale = nil;
|
||||
|
||||
local actorDisplayInfo = actorInfo.modelActorDisplayID and C_ModelInfo.GetModelSceneActorDisplayInfoByID(actorInfo.modelActorDisplayID);
|
||||
if actorDisplayInfo then
|
||||
if actorDisplayInfo.animationKitID then
|
||||
self:PlayAnimationKit(actorDisplayInfo.animationKitID);
|
||||
else
|
||||
self:SetAnimation(actorDisplayInfo.animation, actorDisplayInfo.animationVariation, actorDisplayInfo.animSpeed);
|
||||
end
|
||||
|
||||
self:SetSpellVisualKit(actorDisplayInfo.spellVisualKitID);
|
||||
|
||||
self:SetAlpha(actorDisplayInfo.alpha);
|
||||
self:SetRequestedScale(actorDisplayInfo.scale);
|
||||
else
|
||||
self:StopAnimationKit();
|
||||
self:SetSpellVisualKit(nil);
|
||||
self:SetAnimation(0, 0, 1.0);
|
||||
self:SetAlpha(1.0);
|
||||
self:SetRequestedScale(1.0);
|
||||
end
|
||||
|
||||
self:SetNormalizedScaleAggressiveness(actorInfo.normalizeScaleAggressiveness or 0.0);
|
||||
|
||||
self:MarkScaleDirty();
|
||||
self:UpdateScale();
|
||||
|
||||
C_ModelInfo.AddActiveModelSceneActor(self, actorInfo.modelActorID);
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:OnModelCleared()
|
||||
C_ModelInfo.ClearActiveModelSceneActor(self);
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:SetOnSizeChangedCallback(onSizeChangedCallback)
|
||||
self.onSizeChangedCallback = onSizeChangedCallback;
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:GetOnSizeChangedCallback()
|
||||
return self.onSizeChangedCallback;
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:SetOnModelLoadedCallback(onModelLoadedCallback)
|
||||
self.onModelLoadedCallback = onModelLoadedCallback;
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:GetOnModelLoadedCallback()
|
||||
return self.onModelLoadedCallback;
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:SetNormalizedScaleAggressiveness(normalizedScaleAggressiveness)
|
||||
if self.normalizedScaleAggressiveness ~= normalizedScaleAggressiveness then
|
||||
self.normalizedScaleAggressiveness = normalizedScaleAggressiveness;
|
||||
self:MarkScaleDirty();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:GetNormalizedScaleAggressiveness()
|
||||
return self.normalizedScaleAggressiveness or 0;
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:SetRequestedScale(requestedScale)
|
||||
if self.requestedScale ~= requestedScale then
|
||||
self.requestedScale = requestedScale;
|
||||
self:MarkScaleDirty();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:GetRequestedScale()
|
||||
return self.requestedScale or self:GetScale();
|
||||
end
|
||||
|
||||
MODEL_SCENE_ACTOR_DIMENSIONS_FOR_NORMALIZATION = { width = .9984, depth = 1.121, height = 2.118 }; -- Roughly the dimensions of a human male model (but why male models?)
|
||||
function ModelSceneActorMixin:CalculateNormalizedScale(normalizedScaleAggressiveness)
|
||||
if self:IsLoaded() and normalizedScaleAggressiveness > 0.0 then
|
||||
local bottomX, bottomY, bottomZ, topX, topY, topZ = self:GetActiveBoundingBox(); -- Could be nil for invisible models
|
||||
if bottomX and bottomY and bottomZ and topX and topY and topZ then
|
||||
local width = topX - bottomX;
|
||||
local depth = topY - bottomY;
|
||||
local height = topZ - bottomZ;
|
||||
|
||||
local widthScale = width / MODEL_SCENE_ACTOR_DIMENSIONS_FOR_NORMALIZATION.width;
|
||||
local depthScale = depth / MODEL_SCENE_ACTOR_DIMENSIONS_FOR_NORMALIZATION.depth;
|
||||
local heightScale = height / MODEL_SCENE_ACTOR_DIMENSIONS_FOR_NORMALIZATION.height;
|
||||
|
||||
local maxScale = math.max(widthScale, math.max(depthScale, heightScale));
|
||||
if maxScale == 0 then
|
||||
return 1.0;
|
||||
end
|
||||
|
||||
return 1 / Lerp(1.0, maxScale, normalizedScaleAggressiveness);
|
||||
end
|
||||
end
|
||||
|
||||
return 1.0;
|
||||
end
|
||||
|
||||
-- "private" methods
|
||||
function ModelSceneActorMixin:OnReleased()
|
||||
self:SetOnSizeChangedCallback(nil);
|
||||
self:SetOnModelLoadedCallback(nil);
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:OnUpdate()
|
||||
self:UpdateScale();
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:OnModelLoaded()
|
||||
self:MarkScaleDirty();
|
||||
|
||||
if self.onModelLoadedCallback then
|
||||
self.onModelLoadedCallback(self);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:UpdateScale()
|
||||
if self.scaleDirty and self:IsLoaded() then
|
||||
self.scaleDirty = nil;
|
||||
local effectiveScale = self:GetRequestedScale() * self:CalculateNormalizedScale(self:GetNormalizedScaleAggressiveness());
|
||||
self:SetScale(effectiveScale);
|
||||
|
||||
if self.onSizeChangedCallback then
|
||||
self.onSizeChangedCallback(self);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneActorMixin:MarkScaleDirty()
|
||||
self.scaleDirty = true;
|
||||
end
|
||||
@@ -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/
|
||||
UI.xsd">
|
||||
<Actor name="ModelSceneActorTemplate" mixin="ModelSceneActorMixin" virtual="true">
|
||||
<Scripts>
|
||||
<OnModelCleared method="OnModelCleared" />
|
||||
<OnModelLoaded method="OnModelLoaded" />
|
||||
<OnUpdate method="OnUpdate" />
|
||||
</Scripts>
|
||||
</Actor>
|
||||
</Ui>
|
||||
@@ -0,0 +1,282 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/ModelSceneControlFrame.lua.
|
||||
|
||||
|
||||
local DEFAULT_ZOOM_INCREMENT = 1;
|
||||
local DEFAULT_ROTATE_INCREMENT = 0.05;
|
||||
|
||||
--------------------------------------------------
|
||||
-- MODEL SCENE CONTROL FRAME MIXIN
|
||||
ModelSceneControlFrameMixin = {};
|
||||
function ModelSceneControlFrameMixin:OnLoad()
|
||||
if self.enableZoom then
|
||||
local increment = self:GetZoomIncrement();
|
||||
self.zoomInButton:SetZoomAmount(increment);
|
||||
self.zoomInButton:Init();
|
||||
|
||||
self.zoomOutButton:SetZoomAmount(-increment);
|
||||
self.zoomOutButton:Init();
|
||||
end
|
||||
self.zoomInButton:SetShown(self.enableZoom);
|
||||
self.zoomOutButton:SetShown(self.enableZoom);
|
||||
|
||||
if self.enableRotate then
|
||||
local increment = self:GetRotateIncrement();
|
||||
self.rotateLeftButton:SetRotation("left");
|
||||
self.rotateLeftButton:SetRotationIncrement(increment);
|
||||
self.rotateLeftButton:Init();
|
||||
|
||||
self.rotateRightButton:SetRotation("right");
|
||||
self.rotateRightButton:SetRotationIncrement(increment);
|
||||
self.rotateRightButton:Init();
|
||||
end
|
||||
self.rotateLeftButton:SetShown(self.enableRotate);
|
||||
self.rotateRightButton:SetShown(self.enableRotate);
|
||||
|
||||
if self.enableReset then
|
||||
self.resetButton:Init();
|
||||
end
|
||||
self.resetButton:SetShown(self.enableReset);
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:OnShow()
|
||||
self:UpdateLayout();
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:OnEnter()
|
||||
self:SetAlpha(1);
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:OnLeave()
|
||||
self:SetAlpha(0.5);
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:SetZoomIncrement(increment)
|
||||
self.zoomIncrement = increment or DEFAULT_ZOOM_INCREMENT;
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:GetZoomIncrement()
|
||||
return self.zoomIncrement or DEFAULT_ZOOM_INCREMENT;
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:SetRotateIncrement(increment)
|
||||
self.rotationIncrement = increment or DEFAULT_ROTATE_INCREMENT;
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:GetRotateIncrement()
|
||||
return self.rotationIncrement or DEFAULT_ROTATE_INCREMENT;
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:SetModelScene(modelScene)
|
||||
self.modelScene = modelScene;
|
||||
|
||||
self.zoomInButton:SetModelScene(modelScene);
|
||||
self.zoomOutButton:SetModelScene(modelScene);
|
||||
self.rotateLeftButton:SetModelScene(modelScene);
|
||||
self.rotateRightButton:SetModelScene(modelScene);
|
||||
self.resetButton:SetModelScene(modelScene);
|
||||
end
|
||||
|
||||
function ModelSceneControlFrameMixin:UpdateLayout()
|
||||
local lastButton;
|
||||
local hPadding = self.buttonHorizontalPadding or 0;
|
||||
local vPadding = 0;
|
||||
local buttonSize = 32;
|
||||
local totalHeight = buttonSize;
|
||||
local totalWidth = 2; -- Initial non-zero total width to prevent flickering from negataive padding
|
||||
|
||||
local function LayoutButton(button)
|
||||
button:ClearAllPoints();
|
||||
if lastButton then
|
||||
button:SetPoint("LEFT", lastButton, "RIGHT", hPadding, vPadding);
|
||||
else
|
||||
button:SetPoint("LEFT", 0, vPadding);
|
||||
end
|
||||
totalWidth = totalWidth + buttonSize + hPadding;
|
||||
lastButton = button;
|
||||
end
|
||||
|
||||
if self.enableZoom then
|
||||
-- Make sure that zoom is supported on the model scene.
|
||||
if self.modelScene then
|
||||
local camera = self.modelScene:GetActiveCamera();
|
||||
self.canZoom = camera ~= nil and camera:GetZoomAvailable();
|
||||
self.zoomInButton:SetShown(self.canZoom);
|
||||
self.zoomOutButton:SetShown(self.canZoom);
|
||||
end
|
||||
|
||||
if self.canZoom then
|
||||
LayoutButton(self.zoomInButton);
|
||||
LayoutButton(self.zoomOutButton);
|
||||
end
|
||||
end
|
||||
|
||||
if self.enableRotate then
|
||||
LayoutButton(self.rotateLeftButton);
|
||||
LayoutButton(self.rotateRightButton);
|
||||
end
|
||||
|
||||
if self.enableReset then
|
||||
LayoutButton(self.resetButton);
|
||||
end
|
||||
self:SetSize(totalWidth, totalHeight);
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- MODEL SCENE CONTROL BUTTON MIXIN
|
||||
ModelSceneControlButtonMixin = {};
|
||||
function ModelSceneControlButtonMixin:Init(clickTypes, atlas, tooltip, tooltipText)
|
||||
self:RegisterForClicks(clickTypes);
|
||||
if atlas then
|
||||
self.Icon:SetAtlas(atlas);
|
||||
end
|
||||
self.tooltip = tooltip;
|
||||
self.tooltipText = tooltipText;
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:SetModelScene(modelScene)
|
||||
self.modelScene = modelScene;
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:OnMouseDown()
|
||||
self.Icon:AdjustPointsOffset(1, -1);
|
||||
self:GetParent().buttonDown = self;
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:OnMouseUp()
|
||||
self.Icon:AdjustPointsOffset(-1, 1);
|
||||
self:GetParent().buttonDown = nil;
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:OnClick()
|
||||
--override for your mouse click
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:OnEnter()
|
||||
self:GetParent():SetAlpha(1);
|
||||
if ( GetCVar("UberTooltips") == "1" ) then
|
||||
local tooltip = GetAppropriateTooltip();
|
||||
local topLevelParent = GetAppropriateTopLevelParent();
|
||||
GameTooltip_SetDefaultAnchor(tooltip, topLevelParent);
|
||||
GameTooltip_SetTitle(tooltip, self.tooltip);
|
||||
if ( self.tooltipText ) then
|
||||
GameTooltip_AddBodyLine(tooltip, self.tooltipText);
|
||||
end
|
||||
tooltip:Show();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneControlButtonMixin:OnLeave()
|
||||
self:GetParent():SetAlpha(0.5);
|
||||
|
||||
local tooltip = GetAppropriateTooltip();
|
||||
tooltip:Hide();
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- MODEL ZOOM BUTTON MIXIN
|
||||
ModelSceneZoomButtonMixin = CreateFromMixins(ModelSceneControlButtonMixin);
|
||||
function ModelSceneZoomButtonMixin:OnLoad()
|
||||
self.zoomAmount = 0;
|
||||
end
|
||||
|
||||
function ModelSceneZoomButtonMixin:Init()
|
||||
if self.zoomAmount < 0 then
|
||||
ModelSceneControlButtonMixin.Init(self, "AnyUp", "common-icon-zoomout", ZOOM_OUT, KEY_MOUSEWHEELDOWN);
|
||||
else
|
||||
ModelSceneControlButtonMixin.Init(self, "AnyUp", "common-icon-zoomin", ZOOM_IN, KEY_MOUSEWHEELUP);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneZoomButtonMixin:SetZoomAmount(amountToZoom)
|
||||
self.zoomAmount = amountToZoom;
|
||||
end
|
||||
|
||||
function ModelSceneZoomButtonMixin:OnClick()
|
||||
if self.modelScene then
|
||||
self.modelScene:OnMouseWheel(self.zoomAmount);
|
||||
else
|
||||
assertsafe(fale, "Model Scene not specified");
|
||||
end
|
||||
PlaySound(SOUNDKIT.IG_INVENTORY_ROTATE_CHARACTER);
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- MODEL SCENE ROTATE BUTTON MIXIN
|
||||
ModelScenelRotateButtonMixin = CreateFromMixins(ModelSceneControlButtonMixin);
|
||||
function ModelScenelRotateButtonMixin:OnLoad()
|
||||
ModelSceneControlButtonMixin.OnLoad(self);
|
||||
self.rotateDirection = "none";
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:Init()
|
||||
if (self.rotateDirection == "left") then
|
||||
ModelSceneControlButtonMixin.Init(self, "AnyUp", "common-icon-rotateleft", ROTATE_LEFT, ROTATE_TOOLTIP);
|
||||
elseif (self.rotateDirection == "right") then
|
||||
ModelSceneControlButtonMixin.Init(self, "AnyUp", "common-icon-rotateright", ROTATE_RIGHT, ROTATE_TOOLTIP);
|
||||
else
|
||||
assertsafe(false, "Invalid rotation specified: "..tostring(self.rotateDirection));
|
||||
end
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:SetRotation(rotation)
|
||||
self.rotateDirection = rotation;
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:SetRotationIncrement(increment)
|
||||
self.rotationIncrement = increment or 0.05;
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:OnMouseDown()
|
||||
ModelSceneControlButtonMixin.OnMouseDown(self);
|
||||
if self.modelScene then
|
||||
self.modelScene:AdjustCameraYaw(self.rotateDirection, self.rotationIncrement);
|
||||
else
|
||||
assertsafe(false, "Model Scene not specified");
|
||||
end
|
||||
PlaySound(SOUNDKIT.IG_INVENTORY_ROTATE_CHARACTER);
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:OnMouseUp()
|
||||
ModelSceneControlButtonMixin.OnMouseUp(self);
|
||||
if self.modelScene then
|
||||
self.modelScene:StopCameraYaw();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:OnSmartNavClick()
|
||||
self:RotateStep();
|
||||
end
|
||||
|
||||
function ModelScenelRotateButtonMixin:RotateStep()
|
||||
local camera = self.modelScene and self.modelScene:GetActiveCamera();
|
||||
if not camera then
|
||||
return;
|
||||
end
|
||||
|
||||
if self.rotateDirection == "left" then
|
||||
camera:AdjustYaw(-1, -1, 0.25);
|
||||
else
|
||||
camera:AdjustYaw(1, 1, 0.25);
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- MODEL SCENE RESET BUTTON MIXIN
|
||||
ModelSceneResetButtonMixin = CreateFromMixins(ModelSceneControlButtonMixin);
|
||||
function ModelSceneResetButtonMixin:OnLoad()
|
||||
ModelSceneControlButtonMixin.OnLoad(self);
|
||||
end
|
||||
|
||||
function ModelSceneResetButtonMixin:Init()
|
||||
local tooltipText = nil;
|
||||
ModelSceneControlButtonMixin.Init(self, "AnyUp", "common-icon-undo", RESET_POSITION, tooltipText);
|
||||
end
|
||||
|
||||
function ModelSceneResetButtonMixin:OnClick()
|
||||
if self.modelScene then
|
||||
self.modelScene:Reset();
|
||||
else
|
||||
assertsafe(false, "Model Scene not specified");
|
||||
end
|
||||
PlaySound(SOUNDKIT.IG_INVENTORY_ROTATE_CHARACTER);
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
<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/
|
||||
UI.xsd">
|
||||
<!-- Retail's Blizzard_SharedXML/ModelSceneControlFrame.xml. The one change:
|
||||
HitRectInsets takes this client's <AbsInset> child. The atlases are
|
||||
built in (src/texture/AtlasData.h, EMBEDDED ART). -->
|
||||
<Button name="ModelSceneControlButtonTemplate" mixin="ModelSceneControlButtonMixin" virtual="true">
|
||||
<Size x="32" y="32"/>
|
||||
<HitRectInsets>
|
||||
<AbsInset left="4" right="4" top="4" bottom="4"/>
|
||||
</HitRectInsets>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture parentKey="Icon" >
|
||||
<Size x="16" y="16"/>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
<!--<Texture parentKey="Icon" setAllPoints="true" />-->
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnMouseDown method="OnMouseDown" />
|
||||
<OnMouseUp method="OnMouseUp" />
|
||||
<OnClick method="OnClick" />
|
||||
<OnEnter method="OnEnter" />
|
||||
<OnLeave method="OnLeave" />
|
||||
</Scripts>
|
||||
<NormalTexture parentKey="NormalTexture" atlas="common-button-square-gray-up"/>
|
||||
<HighlightTexture parentKey="HighlightTexture" alphaMode="ADD" alpha="0.4">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeKey="$parent.Icon" relativePoint="TOPLEFT"/>
|
||||
<Anchor point="BOTTOMRIGHT" relativeKey="$parent.Icon" relativePoint="BOTTOMRIGHT"/>
|
||||
</Anchors>
|
||||
</HighlightTexture>
|
||||
<PushedTexture parentKey="PushedTexture" atlas="common-button-square-gray-down">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="1" y="-1"/>
|
||||
<Anchor point="BOTTOMRIGHT" x="1" y="-1"/>
|
||||
</Anchors>
|
||||
</PushedTexture>
|
||||
</Button>
|
||||
<Frame name="ModelSceneControlFrameTemplate" virtual="true" mixin="ModelSceneControlFrameMixin" alpha="0.5" hidden="true">
|
||||
<Size x="32" y="32"/>
|
||||
<KeyValues>
|
||||
<KeyValue key="enableZoom" value="true" type="boolean" />
|
||||
<KeyValue key="enableRotate" value="true" type="boolean" />
|
||||
<KeyValue key="enableReset" value="true" type="boolean" />
|
||||
<KeyValue key="rotationIncrement" value="0.05" type="number" />
|
||||
<KeyValue key="zoomIncrement" value="1" type="number" />
|
||||
<KeyValue key="buttonHorizontalPadding" value="-6" type="number" />
|
||||
</KeyValues>
|
||||
<Frames>
|
||||
<Button parentKey="zoomInButton" hidden="true" mixin="ModelSceneZoomButtonMixin" inherits="ModelSceneControlButtonTemplate"/>
|
||||
<Button parentKey="zoomOutButton" hidden="true" mixin="ModelSceneZoomButtonMixin" inherits="ModelSceneControlButtonTemplate"/>
|
||||
<Button parentKey="rotateLeftButton" hidden="true" mixin="ModelScenelRotateButtonMixin" inherits="ModelSceneControlButtonTemplate"/>
|
||||
<Button parentKey="rotateRightButton" hidden="true" mixin="ModelScenelRotateButtonMixin" inherits="ModelSceneControlButtonTemplate"/>
|
||||
<Button parentKey="resetButton" hidden="true" mixin="ModelSceneResetButtonMixin" inherits="ModelSceneControlButtonTemplate"/>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad" />
|
||||
<OnShow method="OnShow" />
|
||||
<OnEnter method="OnEnter" />
|
||||
<OnLeave method="OnLeave" />
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,571 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/ModelSceneMixin.lua, verbatim except where
|
||||
-- marked "ClassicAPI:".
|
||||
|
||||
|
||||
ModelSceneMixin = {}
|
||||
|
||||
-- "public" functions
|
||||
function ModelSceneMixin:OnLoad()
|
||||
self.cameras = {};
|
||||
self.actorTemplate = "ModelSceneActorTemplate";
|
||||
self.tagToActor = {};
|
||||
self.tagToCamera = {};
|
||||
|
||||
if self.allowOverlappedModels then
|
||||
self:SetAllowOverlappedModels(true);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:ClearScene()
|
||||
self.modelSceneID = nil;
|
||||
|
||||
self:ReleaseAllActors();
|
||||
self:ReleaseAllCameras();
|
||||
|
||||
C_ModelInfo.ClearActiveModelScene(self);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetModelSceneID()
|
||||
return self.modelSceneID;
|
||||
end
|
||||
|
||||
-- Adjusts this scene to mirror a model scene from static data without transition
|
||||
function ModelSceneMixin:SetFromModelSceneID(modelSceneID, forceEvenIfSame, noAutoCreateActors)
|
||||
local modelSceneType, cameraIDs, actorIDs = C_ModelInfo.GetModelSceneInfoByID(modelSceneID);
|
||||
if not modelSceneType then
|
||||
return;
|
||||
end
|
||||
|
||||
if self.modelSceneID ~= modelSceneID or forceEvenIfSame then
|
||||
self.modelSceneID = modelSceneID;
|
||||
self:ReleaseAllActors();
|
||||
self:ReleaseAllCameras();
|
||||
|
||||
if not noAutoCreateActors then
|
||||
for actorIndex, actorID in ipairs(actorIDs) do
|
||||
self:CreateActorFromScene(actorID);
|
||||
end
|
||||
end
|
||||
|
||||
for cameraIndex, cameraID in ipairs(cameraIDs) do
|
||||
self:CreateCameraFromScene(cameraID);
|
||||
end
|
||||
end
|
||||
|
||||
C_ModelInfo.AddActiveModelScene(self, self.modelSceneID);
|
||||
EventRegistry:TriggerEvent("ModelScene.SetFromModelSceneID", self, self.modelSceneID);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:SetResetCallback(callback)
|
||||
self.resetCallback = callback;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:Reset()
|
||||
if self.modelSceneID then
|
||||
self:TransitionToModelSceneID(self.modelSceneID, self.cameraTransitionType, self.cameraModificationType, self.forceEvenIfSame);
|
||||
if self.resetCallback then
|
||||
self.resetCallback(self);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Adjusts this scene to mirror a model scene from static data but with transition effects
|
||||
-- Only actors/cameras with script tags will be transitioned
|
||||
function ModelSceneMixin:TransitionToModelSceneID(modelSceneID, cameraTransitionType, cameraModificationType, forceEvenIfSame)
|
||||
local modelSceneType, cameraIDs, actorIDs = C_ModelInfo.GetModelSceneInfoByID(modelSceneID);
|
||||
if not modelSceneType or #cameraIDs == 0 or #actorIDs == 0 then
|
||||
return;
|
||||
end
|
||||
|
||||
if self.modelSceneID ~= modelSceneID or forceEvenIfSame then
|
||||
self.modelSceneID = modelSceneID;
|
||||
self.cameraTransitionType = cameraTransitionType;
|
||||
self.cameraModificationType = cameraModificationType;
|
||||
self.forceEvenIfSame = forceEvenIfSame;
|
||||
|
||||
local actorsToRelease = {};
|
||||
for actor in self:EnumerateActiveActors() do
|
||||
actorsToRelease[actor] = true;
|
||||
end
|
||||
|
||||
local oldTagToActor = self.tagToActor;
|
||||
self.tagToActor = {};
|
||||
|
||||
for actorIndex, actorID in ipairs(actorIDs) do
|
||||
local actor = self:CreateOrTransitionActorFromScene(oldTagToActor, actorID);
|
||||
if actor then
|
||||
actorsToRelease[actor] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
for actor in pairs(actorsToRelease) do
|
||||
self.actorPool:Release(actor);
|
||||
end
|
||||
|
||||
local oldTagToCamera = self.tagToCamera;
|
||||
self.tagToCamera = {};
|
||||
|
||||
self.cameras = {};
|
||||
|
||||
local needsNewCamera = true;
|
||||
for cameraIndex, cameraID in ipairs(cameraIDs) do
|
||||
local camera = self:CreateOrTransitionCameraFromScene(oldTagToCamera, cameraTransitionType, cameraModificationType, cameraID);
|
||||
if camera == self.activeCamera then
|
||||
needsNewCamera = false;
|
||||
end
|
||||
end
|
||||
|
||||
if needsNewCamera then
|
||||
self:SetActiveCamera(self.cameras[1]);
|
||||
end
|
||||
end
|
||||
|
||||
C_ModelInfo.AddActiveModelScene(self, self.modelSceneID);
|
||||
EventRegistry:TriggerEvent("ModelScene.TransitionToModelSceneID", self, self.modelSceneID);
|
||||
end
|
||||
|
||||
-- There may be inactive (pooled) actors maintained by this scene, these function only returns the active actors
|
||||
function ModelSceneMixin:GetNumActiveActors()
|
||||
return self.actorPool and self.actorPool:GetNumActive() or 0;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:EnumerateActiveActors()
|
||||
if self.actorPool then
|
||||
return self.actorPool:EnumerateActive();
|
||||
end
|
||||
return nop;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetActorByTag(tag)
|
||||
return self.tagToActor[tag];
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AttachPlayerToMount(mountActor, animID, isSelfMount, disablePlayerMountPreview, spellVisualKitID, usePlayerNativeForm, optionalPlayerRiderTag)
|
||||
local playerRiderTag = optionalPlayerRiderTag or "player-rider";
|
||||
local playerActor = self:GetPlayerActor(playerRiderTag);
|
||||
if (playerActor) then
|
||||
if disablePlayerMountPreview or isSelfMount then
|
||||
playerActor:ClearModel();
|
||||
else
|
||||
local sheatheWeapons = true;
|
||||
local autoDress = true;
|
||||
local hideWeapons = true;
|
||||
|
||||
if C_Glue.IsOnGlueScreen() then
|
||||
local characterIndex = nil; -- defaults to selected character.
|
||||
playerActor:SetPlayerModelFromGlues(characterIndex, sheatheWeapons, autoDress, hideWeapons, usePlayerNativeForm);
|
||||
else
|
||||
playerActor:SetModelByUnit("player", sheatheWeapons, autoDress, hideWeapons, usePlayerNativeForm);
|
||||
end
|
||||
local calcMountScale = mountActor:CalculateMountScale(playerActor);
|
||||
local inverseScale = 1 / calcMountScale;
|
||||
playerActor:SetRequestedScale( inverseScale );
|
||||
mountActor:AttachToMount(playerActor, animID, spellVisualKitID);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function GetPlayerActorLabelTag(forceAlternateForm, overrideRaceFilename, overrideGender)
|
||||
local playerRaceName = overrideRaceFilename;
|
||||
local playerGender = overrideGender;
|
||||
local playerRaceNameActorTag = nil;
|
||||
local hasAlternateForm, inAlternateForm = false, false;
|
||||
if not overrideRaceFilename or not overrideGender then
|
||||
if C_Glue.IsOnGlueScreen() then
|
||||
local characterGuid = GetCharacterGUID(GetCharacterSelection());
|
||||
if characterGuid then
|
||||
local basicCharacterInfo = GetBasicCharacterInfo(characterGuid);
|
||||
playerRaceName = overrideRaceFilename or basicCharacterInfo.raceFilename;
|
||||
playerGender = overrideGender or basicCharacterInfo.genderEnum;
|
||||
end
|
||||
else
|
||||
hasAlternateForm, inAlternateForm = C_PlayerInfo.GetAlternateFormInfo();
|
||||
local _, raceFilename = UnitRace("player");
|
||||
playerRaceName = overrideRaceFilename or raceFilename;
|
||||
playerGender = overrideGender or UnitSex("player");
|
||||
end
|
||||
end
|
||||
if not playerRaceName or not playerGender then
|
||||
return playerRaceName, playerRaceNameActorTag;
|
||||
end
|
||||
playerRaceName = playerRaceName:lower();
|
||||
if hasAlternateForm and (inAlternateForm or forceAlternateForm) then
|
||||
playerRaceName = playerRaceName.."-alt";
|
||||
end
|
||||
playerGender = (playerGender == 2) and "male" or "female";
|
||||
playerRaceNameActorTag = playerRaceName.."-"..playerGender;
|
||||
return playerRaceName, playerRaceNameActorTag;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetPlayerActor(overrideActorName, forceAlternateForm, overrideRaceFilename, overrideGender)
|
||||
local actor;
|
||||
if overrideActorName then
|
||||
actor = self:GetActorByTag(overrideActorName);
|
||||
else
|
||||
local playerRaceName, playerRaceNameActorTag = GetPlayerActorLabelTag(forceAlternateForm, overrideRaceFilename, overrideGender);
|
||||
actor = self:GetActorByTag(playerRaceNameActorTag);
|
||||
if not actor then
|
||||
actor = self:GetActorByTag(playerRaceName);
|
||||
if not actor then
|
||||
actor = self:GetActorByTag("player");
|
||||
if not actor then
|
||||
actor = self:GetActorByTag("player-rider");
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return actor;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:ReleaseAllActors()
|
||||
if self.actorPool then
|
||||
self.actorPool:ReleaseAll();
|
||||
|
||||
if self.dropShadowPool then
|
||||
self.dropShadowPool:ReleaseAll();
|
||||
end
|
||||
|
||||
self.tagToActor = {};
|
||||
end
|
||||
end
|
||||
|
||||
local function OnReleased(actorPool, actor)
|
||||
actor:OnReleased();
|
||||
ActorPool_HideAndClearModel(actorPool, actor);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AcquireActor()
|
||||
if not self.actorPool then
|
||||
self.actorPool = CreateActorPool(self, self.actorTemplate, OnReleased);
|
||||
end
|
||||
return self.actorPool:Acquire();
|
||||
end
|
||||
|
||||
function ModelSceneMixin:ReleaseActor(actor)
|
||||
if not actor or not self.actorPool then
|
||||
return;
|
||||
end
|
||||
|
||||
for tag, taggedActor in pairs(self.tagToActor) do
|
||||
if actor == taggedActor then
|
||||
self.tagToActor[tag] = nil;
|
||||
break;
|
||||
end
|
||||
end
|
||||
|
||||
if actor.dropShadowTexture then
|
||||
self.dropShadowPool:Release(actor.dropShadowTexture);
|
||||
end
|
||||
|
||||
return self.actorPool:Release(actor);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AcquireAndInitializeActor(actorInfo)
|
||||
local actor = self:AcquireActor();
|
||||
self:InitializeActor(actor, actorInfo);
|
||||
return actor;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:ReleaseAllCameras()
|
||||
self:SetActiveCamera(nil);
|
||||
for i = #self.cameras, 1, -1 do
|
||||
self.cameras[i]:SetOwningScene(nil);
|
||||
self.cameras[i] = nil;
|
||||
end
|
||||
self.tagToCamera = {};
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetCameraByTag(tag)
|
||||
return self.tagToCamera[tag];
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AddCamera(camera)
|
||||
table.insert(self.cameras, camera);
|
||||
|
||||
camera:SetOwningScene(self);
|
||||
|
||||
if not self:HasActiveCamera() then
|
||||
self:SetActiveCamera(camera);
|
||||
end
|
||||
|
||||
return camera;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:HasActiveCamera()
|
||||
return self.activeCamera ~= nil;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetActiveCamera()
|
||||
return self.activeCamera;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:SetActiveCamera(camera)
|
||||
if camera ~= self.activeCamera then
|
||||
if self.activeCamera then
|
||||
self.activeCamera:OnDeactivated();
|
||||
end
|
||||
|
||||
self.activeCamera = camera;
|
||||
|
||||
if self.activeCamera then
|
||||
self.activeCamera:OnActivated();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:IsLeftMouseButtonDown()
|
||||
return self.isLeftButtonDown;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:IsRightMouseButtonDown()
|
||||
return self.isRightButtonDown;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:Transform3DPointTo2D(x, y, z)
|
||||
self:SynchronizeActiveCamera();
|
||||
return self:Project3DPointTo2D(x, y, z);
|
||||
end
|
||||
|
||||
-- "private" functions
|
||||
function ModelSceneMixin:OnUpdate(elapsed)
|
||||
if self.activeCamera then
|
||||
local yawDirection = self.yawDirection;
|
||||
-- ClassicAPI: retail turns by the increment every frame, which spins
|
||||
-- several times faster at this client's uncapped frame rates. The
|
||||
-- increment counts per 1/60 s here: 0.05 is 3 rad/s, the speed of
|
||||
-- 1.12's own rotate buttons (ROTATIONS_PER_SECOND, pi rad/s).
|
||||
local increment = self.increment and self.increment * elapsed * 60;
|
||||
if yawDirection == "left" then
|
||||
self.activeCamera:AdjustYaw(-1, -1, increment);
|
||||
elseif yawDirection == "right" then
|
||||
self.activeCamera:AdjustYaw(1, 1, increment);
|
||||
end
|
||||
|
||||
self.activeCamera:OnUpdate(elapsed);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:SynchronizeActiveCamera()
|
||||
if self.activeCamera then
|
||||
self.activeCamera:SynchronizeCamera();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:OnEnter(button)
|
||||
if self.ControlFrame then
|
||||
self.ControlFrame:Show();
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:OnLeave(button)
|
||||
if self.ControlFrame then
|
||||
if not self.ControlFrame:IsMouseOver()then
|
||||
self.ControlFrame:Hide();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:OnMouseDown(button)
|
||||
if button == "LeftButton" then
|
||||
self.isLeftButtonDown = true;
|
||||
elseif button == "RightButton" then
|
||||
self.isRightButtonDown = true;
|
||||
end
|
||||
|
||||
if self.activeCamera then
|
||||
self.activeCamera:OnMouseDown(button);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:OnMouseUp(button)
|
||||
if button == "LeftButton" then
|
||||
self.isLeftButtonDown = false;
|
||||
elseif button == "RightButton" then
|
||||
self.isRightButtonDown = false;
|
||||
end
|
||||
|
||||
if self.activeCamera then
|
||||
self.activeCamera:OnMouseUp(button);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:OnMouseWheel(delta)
|
||||
if self.activeCamera then
|
||||
self.activeCamera:OnMouseWheel(delta);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AdjustCameraYaw(direction, increment)
|
||||
if self.activeCamera then
|
||||
self.yawDirection = direction;
|
||||
self.increment = increment;
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:StopCameraYaw()
|
||||
self.yawDirection = nil;
|
||||
self.increment = nil;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:InitializeActor(actor, actorInfo)
|
||||
if actorInfo.scriptTag then
|
||||
self.tagToActor[actorInfo.scriptTag] = actor;
|
||||
end
|
||||
|
||||
actor:ApplyFromModelSceneActorInfo(actorInfo);
|
||||
|
||||
actor:Show();
|
||||
end
|
||||
|
||||
function ModelSceneMixin:CreateActorFromScene(actorID)
|
||||
local actorInfo = C_ModelInfo.GetModelSceneActorInfoByID(actorID);
|
||||
return self:AcquireAndInitializeActor(actorInfo);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:CreateOrTransitionActorFromScene(oldTagToActor, actorID)
|
||||
local actorInfo = C_ModelInfo.GetModelSceneActorInfoByID(actorID);
|
||||
local existingActor = oldTagToActor[actorInfo.scriptTag];
|
||||
if existingActor then
|
||||
self:InitializeActor(existingActor, actorInfo);
|
||||
return existingActor;
|
||||
end
|
||||
|
||||
return self:AcquireAndInitializeActor(actorInfo);
|
||||
end
|
||||
|
||||
function ModelSceneMixin:CreateCameraFromScene(modelSceneCameraID)
|
||||
local modelSceneCameraInfo = C_ModelInfo.GetModelSceneCameraInfoByID(modelSceneCameraID);
|
||||
if modelSceneCameraInfo then
|
||||
local camera = CameraRegistry:CreateCameraByType(modelSceneCameraInfo.cameraType);
|
||||
if camera then
|
||||
if modelSceneCameraInfo.scriptTag then
|
||||
self.tagToCamera[modelSceneCameraInfo.scriptTag] = camera;
|
||||
end
|
||||
self:AddCamera(camera);
|
||||
camera:ApplyFromModelSceneCameraInfo(modelSceneCameraInfo, CAMERA_TRANSITION_TYPE_IMMEDIATE, CAMERA_MODIFICATION_TYPE_DISCARD);
|
||||
return camera;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:CreateOrTransitionCameraFromScene(oldTagToCamera, cameraTransitionType, cameraModificationType, modelSceneCameraID)
|
||||
local modelSceneCameraInfo = C_ModelInfo.GetModelSceneCameraInfoByID(modelSceneCameraID);
|
||||
if modelSceneCameraInfo then
|
||||
local existingCamera = oldTagToCamera[modelSceneCameraInfo.scriptTag];
|
||||
if existingCamera and existingCamera:GetCameraType() == modelSceneCameraInfo.cameraType then
|
||||
self.tagToCamera[modelSceneCameraInfo.scriptTag] = existingCamera;
|
||||
|
||||
self:AddCamera(existingCamera);
|
||||
existingCamera:ApplyFromModelSceneCameraInfo(modelSceneCameraInfo, cameraTransitionType, cameraModificationType);
|
||||
return existingCamera;
|
||||
end
|
||||
|
||||
return self:CreateCameraFromScene(modelSceneCameraID);
|
||||
end
|
||||
end
|
||||
|
||||
-- actorSettings = {
|
||||
-- "actorTag" = { startDelay = 0, duration = 0, speed = 1 } -- duration 0 means no automatic stoppage
|
||||
-- }
|
||||
function ModelSceneMixin:ShowAndAnimateActors(actorSettings, onFinishedCallback)
|
||||
self:Show();
|
||||
local totalTime = 0;
|
||||
for actorTag, actorInfo in pairs(actorSettings) do
|
||||
local actor = self:GetActorByTag(actorTag);
|
||||
if actor then
|
||||
local runningTime = actorInfo.startDelay + actorInfo.duration;
|
||||
if actorInfo.startDelay > 0 then
|
||||
actor:SetAnimation(0, 0, 0, 0);
|
||||
C_Timer.After(actorInfo.startDelay,
|
||||
function()
|
||||
actor:SetAnimation(0, 0, actorInfo.speed, 0);
|
||||
end
|
||||
);
|
||||
else
|
||||
actor:SetAnimation(0, 0, actorInfo.speed, 0);
|
||||
end
|
||||
if actorInfo.duration > 0 then
|
||||
C_Timer.After(runningTime,
|
||||
function()
|
||||
actor:SetAnimation(0, 0, 0, 0);
|
||||
end
|
||||
);
|
||||
end
|
||||
if runningTime > totalTime then
|
||||
totalTime = runningTime;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if onFinishedCallback and totalTime > 0 then
|
||||
C_Timer.After(totalTime, onFinishedCallback);
|
||||
end
|
||||
end
|
||||
|
||||
function ModelSceneMixin:GetOrAcquireDropShadow(actor)
|
||||
if actor.dropShadowTexture then
|
||||
return actor.dropShadowTexture;
|
||||
end
|
||||
|
||||
if not self.dropShadowPool then
|
||||
self.dropShadowPool = CreateTexturePool(self, "BORDER", 1, "ModelSceneDropShadowTemplate");
|
||||
end
|
||||
|
||||
actor.dropShadowTexture = self.dropShadowPool:Acquire();
|
||||
return actor.dropShadowTexture;
|
||||
end
|
||||
|
||||
function ModelSceneMixin:AddOrUpdateDropShadow(actor, baseShadowScale)
|
||||
baseShadowScale = baseShadowScale or 1.0;
|
||||
|
||||
local dropShadowTexture = self:GetOrAcquireDropShadow(actor);
|
||||
local positionVector = CreateVector3D(actor:GetPosition());
|
||||
positionVector:ScaleBy(actor:GetScale());
|
||||
local x, y, depthScale = self:Transform3DPointTo2D(positionVector:GetXYZ());
|
||||
|
||||
if not x or not y or not depthScale then
|
||||
dropShadowTexture:Hide();
|
||||
return;
|
||||
end
|
||||
|
||||
dropShadowTexture:ClearAllPoints();
|
||||
depthScale = baseShadowScale * Lerp(.05, 1, ClampedPercentageBetween(depthScale, .8, 1))
|
||||
-- Scales down the texture depending on it's depthScale.
|
||||
dropShadowTexture:SetScale(depthScale);
|
||||
|
||||
-- Need to apply the effective scale to account for UI Scaling.
|
||||
local inverseScale = self:GetEffectiveScale() * depthScale;
|
||||
-- The position of the character can be found by the offset on the screen.
|
||||
dropShadowTexture:SetPoint("CENTER", self, "BOTTOMLEFT", (x / inverseScale) + 2, (y / inverseScale) - 4);
|
||||
dropShadowTexture:Show();
|
||||
end
|
||||
|
||||
|
||||
PanningModelSceneMixin = CreateFromMixins(ModelSceneMixin);
|
||||
function PanningModelSceneMixin:TransitionToModelSceneID(modelSceneID, cameraTransitionType, cameraModificationType, forceEvenIfSame)
|
||||
ModelSceneMixin.TransitionToModelSceneID(self, modelSceneID, cameraTransitionType, cameraModificationType, forceEvenIfSame);
|
||||
|
||||
local camera = self:GetActiveCamera();
|
||||
if camera then
|
||||
camera:SetRightMouseButtonXMode(ORBIT_CAMERA_MOUSE_PAN_HORIZONTAL, true);
|
||||
camera:SetRightMouseButtonYMode(ORBIT_CAMERA_MOUSE_PAN_VERTICAL, true);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
NoCameraControlModelSceneMixin = CreateFromMixins(ModelSceneMixin);
|
||||
function NoCameraControlModelSceneMixin:OnMouseDown(button)
|
||||
self.isLeftButtonDown = false;
|
||||
self.isRightButtonDown = false;
|
||||
end
|
||||
|
||||
function NoCameraControlModelSceneMixin:OnMouseUp(button)
|
||||
self.isLeftButtonDown = false;
|
||||
self.isRightButtonDown = false;
|
||||
end
|
||||
|
||||
function NoCameraControlModelSceneMixin:OnMouseWheel(delta)
|
||||
end
|
||||
NoZoomModelSceneMixin = CreateFromMixins(ModelSceneMixin);
|
||||
function NoZoomModelSceneMixin:OnMouseWheel(delta)
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
<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/
|
||||
UI.xsd">
|
||||
<ModelScene name="NonInteractableModelSceneMixinTemplate" mixin="ModelSceneMixin" virtual="true">
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad" />
|
||||
<OnUpdate method="OnUpdate" />
|
||||
</Scripts>
|
||||
</ModelScene>
|
||||
<ModelScene name="ModelSceneMixinTemplate" inherits="NonInteractableModelSceneMixinTemplate" virtual="true">
|
||||
<Scripts>
|
||||
<OnEnter method="OnEnter"/>
|
||||
<OnLeave method="OnLeave"/>
|
||||
<OnMouseUp method="OnMouseUp"/>
|
||||
<OnMouseDown method="OnMouseDown"/>
|
||||
<OnMouseWheel method="OnMouseWheel" />
|
||||
</Scripts>
|
||||
</ModelScene>
|
||||
<ModelScene name="StoreModelSceneMixinTemplate" inherits="ModelSceneMixinTemplate" virtual="true">
|
||||
<KeyValues>
|
||||
<!-- CLASS-44103: Automatically allow overlapped models for shop scenes -->
|
||||
<KeyValue key="allowOverlappedModels" value="true" type="boolean"/>
|
||||
</KeyValues>
|
||||
</ModelScene>
|
||||
<ModelScene name="NoZoomModelSceneMixinTemplate" inherits="ModelSceneMixinTemplate" mixin="NoZoomModelSceneMixin" virtual="true"/>
|
||||
<ModelScene name="PanningModelSceneMixinTemplate" inherits="ModelSceneMixinTemplate" mixin="PanningModelSceneMixin" virtual="true" />
|
||||
<ModelScene name="NoCameraControlModelSceneMixinTemplate" inherits="ModelSceneMixinTemplate" mixin="NoCameraControlModelSceneMixin" virtual="true" />
|
||||
<Texture name="ModelSceneDropShadowTemplate" atlas="scoreboard-charactermodels-shadow" useAtlasSize="true" virtual="true"/>
|
||||
</Ui>
|
||||
@@ -0,0 +1,640 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/ModelSceneCameras/OrbitCameraMixin.lua.
|
||||
|
||||
--[[
|
||||
OrbitCameraMixin - A simple camera that can orbit a target point at a fixed distance.
|
||||
|
||||
The orbit is represented using a distance from a target point in world space and yaw, pitch and roll.
|
||||
The yaw, pitch and roll represent the camera's orientation, or inversely the direction of the camera from the target point.
|
||||
|
||||
Optionally, the target, orientation and zoom can have splines that are added to the respective axes. The splines are fed the zoom percentage to calculate the point on the spline.
|
||||
This is ideal for changing view points based on zoom distance from the target. For example, target model's face on zoom in and center on the model on zoom out.
|
||||
]]
|
||||
|
||||
OrbitCameraMixin = CreateFromMixins(CameraBaseMixin);
|
||||
|
||||
local CAMERA_NAME = "OrbitCamera";
|
||||
CameraRegistry:AddCameraFactoryFromMixin(CAMERA_NAME, OrbitCameraMixin);
|
||||
|
||||
-- "public" functions
|
||||
function OrbitCameraMixin:GetCameraType() -- override
|
||||
return CAMERA_NAME;
|
||||
end
|
||||
|
||||
local function TryCreateZoomSpline(x, y, z, existingSpline)
|
||||
if x and y and z and (x ~= 0 or y ~= 0 or z ~= 0) then
|
||||
local spline = existingSpline or CreateCatmullRomSpline(3);
|
||||
spline:ClearPoints();
|
||||
spline:AddPoint(0, 0, 0);
|
||||
spline:AddPoint(x, y, z);
|
||||
|
||||
return spline;
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:ApplyFromModelSceneCameraInfo(modelSceneCameraInfo, transitionType, modificationType) -- override
|
||||
self.panningXOffset = 0;
|
||||
self.panningYOffset = 0;
|
||||
|
||||
local transitionalCameraInfo = self:CalculateTransitionalValues(self.modelSceneCameraInfo, modelSceneCameraInfo, modificationType);
|
||||
|
||||
self.modelSceneCameraInfo = modelSceneCameraInfo;
|
||||
|
||||
self:SetTarget(transitionalCameraInfo.target:GetXYZ());
|
||||
self:SetTargetSpline(TryCreateZoomSpline(transitionalCameraInfo.zoomedTargetOffset:GetXYZ()), self:GetTargetSpline());
|
||||
self:SetOrientationSpline(TryCreateZoomSpline(transitionalCameraInfo.zoomedYawOffset, transitionalCameraInfo.zoomedPitchOffset, transitionalCameraInfo.zoomedRollOffset), self:GetOrientationSpline());
|
||||
|
||||
self:SetMinZoomDistance(transitionalCameraInfo.minZoomDistance);
|
||||
self:SetMaxZoomDistance(transitionalCameraInfo.maxZoomDistance);
|
||||
|
||||
self:SetZoomDistance(transitionalCameraInfo.zoomDistance);
|
||||
|
||||
self:SetYaw(transitionalCameraInfo.yaw);
|
||||
self:SetPitch(transitionalCameraInfo.pitch);
|
||||
self:SetRoll(transitionalCameraInfo.roll);
|
||||
|
||||
if transitionType == CAMERA_TRANSITION_TYPE_IMMEDIATE then
|
||||
self:SnapAllInterpolatedValues();
|
||||
end
|
||||
self:UpdateCameraOrientationAndPosition();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetTarget(x, y, z)
|
||||
self.targetX, self.targetY, self.targetZ = x, y, z;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetTarget(x, y, z)
|
||||
return self.targetX, self.targetY, self.targetZ;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetYaw(yaw)
|
||||
self.yaw = yaw;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetYaw()
|
||||
return self.yaw;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetPitch(pitch)
|
||||
self.pitch = pitch;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetPitch()
|
||||
return self.pitch;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetRoll(roll)
|
||||
self.roll = roll;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetRoll()
|
||||
return self.roll;
|
||||
end
|
||||
|
||||
-- where 100% is fully zoomed out
|
||||
function OrbitCameraMixin:GetZoomPercent()
|
||||
return self.zoomPercent;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetZoomPercent(zoomPercent)
|
||||
self.zoomPercent = Saturate(zoomPercent);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:ZoomByPercent(percent)
|
||||
self:SetZoomPercent(self:GetZoomPercent() - percent);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetMaxZoomDistance(distance)
|
||||
self.maxZoomDistance = distance;
|
||||
if self:GetZoomPercent() then
|
||||
self:SetZoomDistance(self:GetZoomDistance());
|
||||
end
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetMaxZoomDistance()
|
||||
return self.maxZoomDistance;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetMinZoomDistance(distance)
|
||||
self.minZoomDistance = distance;
|
||||
if self:GetZoomPercent() then
|
||||
self:SetZoomDistance(self:GetZoomDistance());
|
||||
end
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetMinZoomDistance()
|
||||
return self.minZoomDistance;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetZoomDistance(distance)
|
||||
self:SetZoomPercent(self:CalculateZoomPercentFromDistance(distance));
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:CalculateZoomPercentFromDistance(distance)
|
||||
if self:GetMinZoomDistance() and self:GetMaxZoomDistance() then
|
||||
return PercentageBetween(distance, self:GetMinZoomDistance(), self:GetMaxZoomDistance());
|
||||
end
|
||||
return 0.0;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:CalculateZoomDistanceFromPercent(percent)
|
||||
if self:GetMinZoomDistance() and self:GetMaxZoomDistance() then
|
||||
return Lerp(self:GetMinZoomDistance(), self:GetMaxZoomDistance(), percent);
|
||||
end
|
||||
return 0.0;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetZoomDistance()
|
||||
return self:CalculateZoomDistanceFromPercent(self:GetZoomPercent());
|
||||
end
|
||||
|
||||
-- If min and max values are the same, zoom cannot really happen.
|
||||
function OrbitCameraMixin:GetZoomAvailable()
|
||||
if self:GetMinZoomDistance() and self:GetMaxZoomDistance() then
|
||||
return self:GetMinZoomDistance() ~= self:GetMaxZoomDistance();
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:ZoomBy(distance)
|
||||
self:SetZoomDistance(self:GetZoomDistance() - distance);
|
||||
end
|
||||
|
||||
-- Expects a three dimensional spline where a point of the curve represents a target position
|
||||
function OrbitCameraMixin:SetTargetSpline(targetSpline)
|
||||
self.targetSpline = targetSpline;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetTargetSpline()
|
||||
return self.targetSpline;
|
||||
end
|
||||
|
||||
-- Expects a three dimensional spline where a point of the curve represents the camera yaw, pitch, roll
|
||||
function OrbitCameraMixin:SetOrientationSpline(orientationSpline)
|
||||
self.orientationSpline = orientationSpline;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetOrientationSpline()
|
||||
return self.orientationSpline;
|
||||
end
|
||||
|
||||
-- Expects an one dimensional spline where a point of the curve represents the camera's distance from the target
|
||||
function OrbitCameraMixin:SetZoomSpline(zoomSpline)
|
||||
self.zoomSpline = zoomSpline;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetZoomSpline()
|
||||
return self.zoomSpline;
|
||||
end
|
||||
|
||||
--[[ These return the simple + spline values ]]--
|
||||
function OrbitCameraMixin:GetDerivedTarget()
|
||||
local targetX, targetY, targetZ = self:GetTarget();
|
||||
|
||||
local targetSpline = self:GetTargetSpline();
|
||||
if targetSpline then
|
||||
return Vector3D_Add(targetX, targetY, targetZ, targetSpline:CalculatePointOnGlobalCurve(1.0 - self:GetZoomPercent()));
|
||||
end
|
||||
|
||||
return targetX, targetY, targetZ;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetDerivedOrientation()
|
||||
local yaw, pitch, roll = self:GetYaw(), self:GetPitch(), self:GetRoll();
|
||||
|
||||
local orientationSpline = self:GetOrientationSpline();
|
||||
if orientationSpline then
|
||||
return Vector3D_Add(yaw, pitch, roll, orientationSpline:CalculatePointOnGlobalCurve(1.0 - self:GetZoomPercent()));
|
||||
end
|
||||
|
||||
return yaw, pitch, roll;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetDerivedZoomDistance()
|
||||
local zoomDistance = self:GetZoomDistance();
|
||||
|
||||
local zoomSpline = self:GetZoomSpline();
|
||||
if zoomSpline then
|
||||
return zoomDistance + zoomSpline:CalculatePointOnGlobalCurve(1.0 - self:GetZoomPercent());
|
||||
end
|
||||
|
||||
return zoomDistance;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetAlignLightToOrbitDelta(alignLightToOrbitDelta)
|
||||
if alignLightToOrbitDelta then
|
||||
self.modelSceneCameraInfo.flags = bit.bor(self.modelSceneCameraInfo.flags, Enum.ModelSceneSetting.AlignLightToOrbitDelta);
|
||||
else
|
||||
self.modelSceneCameraInfo.flags = bit.band(self.modelSceneCameraInfo.flags, bit.bnot(Enum.ModelSceneSetting.AlignLightToOrbitDelta));
|
||||
end
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:ShouldAlignLightToOrbitDelta()
|
||||
return bit.band(self.modelSceneCameraInfo.flags, Enum.ModelSceneSetting.AlignLightToOrbitDelta) == Enum.ModelSceneSetting.AlignLightToOrbitDelta;
|
||||
end
|
||||
|
||||
--[[
|
||||
Interpolation API
|
||||
For each API, "amount" is the percentage to approach in an "ideal" frame (60 fps), such that setting the value to >= 1 would snap to the desired target and <= 0 would freeze the interpolation.
|
||||
Setting to nil will disable the interpolation.
|
||||
]]
|
||||
|
||||
function OrbitCameraMixin:SetYawInterpolationAmount(yawInterpolationAmount)
|
||||
self.yawInterpolationAmount = yawInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetYawInterpolationAmount()
|
||||
return self.yawInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetPitchInterpolationAmount(pitchInterpolationAmount)
|
||||
self.pitchInterpolationAmount = pitchInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetPitchInterpolationAmount()
|
||||
return self.pitchInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetRollInterpolationAmount(rollInterpolationAmount)
|
||||
self.rollInterpolationAmount = rollInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetRollInterpolationAmount()
|
||||
return self.rollInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetTargetInterpolationAmount(targetInterpolationAmount)
|
||||
self.targetInterpolationAmount = targetInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetTargetInterpolationAmount()
|
||||
return self.targetInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetZoomInterpolationAmount(zoomInterpolationAmount)
|
||||
self.zoomInterpolationAmount = zoomInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetZoomInterpolationAmount()
|
||||
return self.zoomInterpolationAmount;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetInterpolatedTarget()
|
||||
if self.interpolatedTargetX then
|
||||
return self.interpolatedTargetX, self.interpolatedTargetY, self.interpolatedTargetZ;
|
||||
end
|
||||
return self:GetDerivedTarget();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetInterpolatedOrientation()
|
||||
if self.interpolatedYaw then
|
||||
return self.interpolatedYaw, self.interpolatedPitch, self.interpolatedRoll;
|
||||
end
|
||||
return self:GetDerivedOrientation();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetInterpolatedZoomDistance()
|
||||
if self.interpolatedZoomDistance then
|
||||
return self.interpolatedZoomDistance;
|
||||
end
|
||||
return self:GetDerivedZoomDistance();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapToTargetInterpolationYaw()
|
||||
self.interpolatedYaw = nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapToTargetInterpolationPitch()
|
||||
self.interpolatedPitch = nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapToTargetInterpolationRoll()
|
||||
self.interpolatedRoll = nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapToTargetInterpolationZoom()
|
||||
self.interpolatedZoomDistance = nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapToTargetInterpolationTarget()
|
||||
self.interpolatedTargetX = nil;
|
||||
self.interpolatedTargetY = nil;
|
||||
self.interpolatedTargetZ = nil;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SnapAllInterpolatedValues()
|
||||
self:SnapToTargetInterpolationYaw();
|
||||
self:SnapToTargetInterpolationPitch();
|
||||
self:SnapToTargetInterpolationRoll();
|
||||
|
||||
self:SnapToTargetInterpolationZoom();
|
||||
|
||||
self:SnapToTargetInterpolationTarget();
|
||||
end
|
||||
|
||||
ORBIT_CAMERA_MOUSE_MODE_NOTHING = 0;
|
||||
ORBIT_CAMERA_MOUSE_MODE_YAW_ROTATION = 1;
|
||||
ORBIT_CAMERA_MOUSE_MODE_PITCH_ROTATION = 2;
|
||||
ORBIT_CAMERA_MOUSE_MODE_ROLL_ROTATION = 3;
|
||||
ORBIT_CAMERA_MOUSE_MODE_TARGET_HORIZONTAL = 4;
|
||||
ORBIT_CAMERA_MOUSE_MODE_TARGET_VERTICAL = 5;
|
||||
ORBIT_CAMERA_MOUSE_MODE_ZOOM = 6;
|
||||
ORBIT_CAMERA_MOUSE_PAN_HORIZONTAL = 7;
|
||||
ORBIT_CAMERA_MOUSE_PAN_VERTICAL = 8;
|
||||
|
||||
function OrbitCameraMixin:SetLeftMouseButtonXMode(mouseMode, snap)
|
||||
self.buttonModes.leftX = mouseMode;
|
||||
self.buttonModes.leftXinterpolate = not snap;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetLeftMouseButtonXMode()
|
||||
return self.buttonModes.leftX, not self.buttonModes.leftXinterpolate;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetLeftMouseButtonYMode(mouseMode, snap)
|
||||
self.buttonModes.leftY = mouseMode;
|
||||
self.buttonModes.leftYinterpolate = not snap;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetLeftYMouseButtonYMode()
|
||||
return self.buttonModes.leftY, not self.buttonModes.leftYinterpolate;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetRightMouseButtonXMode(mouseMode, snap)
|
||||
self.buttonModes.rightX = mouseMode;
|
||||
self.buttonModes.rightXinterpolate = not snap;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetRightMouseButtonXMode()
|
||||
return self.buttonModes.rightX, not self.buttonModes.rightXinterpolate;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetRightMouseButtonYMode(mouseMode, snap)
|
||||
self.buttonModes.rightY = mouseMode;
|
||||
self.buttonModes.rightYinterpolate = not snap;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetRightMouseButtonYMode()
|
||||
return self.buttonModes.rightY, not self.buttonModes.rightYinterpolate;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SetMouseWheelMode(mouseMode, snap)
|
||||
self.buttonModes.wheel = mouseMode;
|
||||
self.buttonModes.wheelInterpolate = not snap;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetMouseWheelMode()
|
||||
return self.buttonModes.wheel, not self.buttonModes.wheelInterpolate;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:HandleMouseMovement(mode, delta, snapToValue)
|
||||
if mode == ORBIT_CAMERA_MOUSE_MODE_YAW_ROTATION then
|
||||
self:SetYaw(self:GetYaw() - delta);
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationYaw();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_PITCH_ROTATION then
|
||||
self:SetPitch(self:GetPitch() - delta);
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationPitch();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_ROLL_ROTATION then
|
||||
self:SetRoll(self:GetRoll() - delta);
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationRoll();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_ZOOM then
|
||||
self:ZoomByPercent(delta);
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationZoom();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_TARGET_HORIZONTAL then
|
||||
local rightX, rightY, rightZ = Vector3D_ScaleBy(delta, self:GetRightVector());
|
||||
self:SetTarget(Vector3D_Add(rightX, rightY, rightZ, self:GetTarget()));
|
||||
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationTarget();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_TARGET_VERTICAL then
|
||||
local upX, upY, upZ = Vector3D_ScaleBy(delta, self:GetUpVector());
|
||||
self:SetTarget(Vector3D_Add(upX, upY, upZ, self:GetTarget()));
|
||||
|
||||
if snapToValue then
|
||||
self:SnapToTargetInterpolationTarget();
|
||||
end
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_PAN_HORIZONTAL then
|
||||
self.panningXOffset = self.panningXOffset + delta;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_PAN_VERTICAL then
|
||||
self.panningYOffset = self.panningYOffset + delta;
|
||||
end
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:ResetDefaultInputModes()
|
||||
self:SetLeftMouseButtonXMode(ORBIT_CAMERA_MOUSE_MODE_YAW_ROTATION, true);
|
||||
self:SetLeftMouseButtonYMode(ORBIT_CAMERA_MOUSE_MODE_NOTHING);
|
||||
self:SetRightMouseButtonXMode(ORBIT_CAMERA_MOUSE_MODE_NOTHING);
|
||||
self:SetRightMouseButtonYMode(ORBIT_CAMERA_MOUSE_MODE_NOTHING);
|
||||
self:SetMouseWheelMode(ORBIT_CAMERA_MOUSE_MODE_ZOOM, false);
|
||||
end
|
||||
|
||||
-- "private" function
|
||||
function OrbitCameraMixin:OnAdded() -- override
|
||||
self.buttonModes = {};
|
||||
|
||||
self:SetTarget(0, 0, 0);
|
||||
|
||||
local targetSpline = CreateCatmullRomSpline(3);
|
||||
targetSpline:AddPoint(0, 0, 0);
|
||||
targetSpline:AddPoint(0, 0, .5);
|
||||
|
||||
self:SetTargetSpline(targetSpline);
|
||||
|
||||
self:SetMinZoomDistance(6);
|
||||
self:SetMaxZoomDistance(10);
|
||||
|
||||
self:SetZoomDistance(8);
|
||||
|
||||
self:SetYaw(math.pi);
|
||||
self:SetPitch(0);
|
||||
self:SetRoll(0);
|
||||
|
||||
self:SetZoomInterpolationAmount(.15);
|
||||
self:SetYawInterpolationAmount(.15);
|
||||
self:SetPitchInterpolationAmount(.15);
|
||||
self:SetRollInterpolationAmount(.15);
|
||||
self:SetTargetInterpolationAmount(.15);
|
||||
|
||||
self:ResetDefaultInputModes();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:GetDeltaModifierForCameraMode(mode)
|
||||
if mode == ORBIT_CAMERA_MOUSE_MODE_YAW_ROTATION then
|
||||
return .008;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_PITCH_ROTATION then
|
||||
return .008;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_ROLL_ROTATION then
|
||||
return .008;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_ZOOM then
|
||||
return .1;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_TARGET_HORIZONTAL then
|
||||
return .05;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_MODE_TARGET_VERTICAL then
|
||||
return .05;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_PAN_HORIZONTAL then
|
||||
return 0.93;
|
||||
elseif mode == ORBIT_CAMERA_MOUSE_PAN_VERTICAL then
|
||||
return 0.93;
|
||||
end
|
||||
return 0.0;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:OnUpdate(elapsed) -- override
|
||||
if self:IsLeftMouseButtonDown() then
|
||||
local deltaX, deltaY = InputUtil.GetCursorDelta(GetAppropriateTopLevelParent());
|
||||
self:HandleMouseMovement(self.buttonModes.leftX, deltaX * self:GetDeltaModifierForCameraMode(self.buttonModes.leftX), not self.buttonModes.leftXinterpolate);
|
||||
self:HandleMouseMovement(self.buttonModes.leftY, -deltaY * self:GetDeltaModifierForCameraMode(self.buttonModes.leftY), not self.buttonModes.leftYinterpolate);
|
||||
end
|
||||
|
||||
if self:IsRightMouseButtonDown() then
|
||||
local deltaX, deltaY = InputUtil.GetCursorDelta(GetAppropriateTopLevelParent());
|
||||
self:HandleMouseMovement(self.buttonModes.rightX, deltaX * self:GetDeltaModifierForCameraMode(self.buttonModes.rightX), not self.buttonModes.rightXinterpolate);
|
||||
self:HandleMouseMovement(self.buttonModes.rightY, -deltaY * self:GetDeltaModifierForCameraMode(self.buttonModes.rightY), not self.buttonModes.rightYinterpolate);
|
||||
end
|
||||
|
||||
self:UpdateInterpolationTargets(elapsed);
|
||||
self:SynchronizeCamera();
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:SynchronizeCamera() -- override
|
||||
self:UpdateCameraOrientationAndPosition();
|
||||
self:UpdateLight();
|
||||
end
|
||||
|
||||
local function InterpolateDimension(lastValue, targetValue, amount, elapsed)
|
||||
return lastValue and amount and DeltaLerp(lastValue, targetValue, amount, elapsed) or targetValue;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:UpdateInterpolationTargets(elapsed)
|
||||
local yaw, pitch, roll = self:GetDerivedOrientation();
|
||||
local targetX, targetY, targetZ = self:GetDerivedTarget();
|
||||
local zoomDistance = self:GetDerivedZoomDistance();
|
||||
|
||||
self.interpolatedYaw = InterpolateDimension(self.interpolatedYaw, yaw, self.yawInterpolationAmount, elapsed);
|
||||
self.interpolatedPitch = InterpolateDimension(self.interpolatedPitch, pitch, self.pitchInterpolationAmount, elapsed);
|
||||
self.interpolatedRoll = InterpolateDimension(self.interpolatedRoll, roll, self.rollInterpolationAmount, elapsed);
|
||||
|
||||
self.interpolatedZoomDistance = InterpolateDimension(self.interpolatedZoomDistance, zoomDistance, self.zoomInterpolationAmount, elapsed);
|
||||
|
||||
self.interpolatedTargetX = InterpolateDimension(self.interpolatedTargetX, targetX, self.targetInterpolationAmount, elapsed);
|
||||
self.interpolatedTargetY = InterpolateDimension(self.interpolatedTargetY, targetY, self.targetInterpolationAmount, elapsed);
|
||||
self.interpolatedTargetZ = InterpolateDimension(self.interpolatedTargetZ, targetZ, self.targetInterpolationAmount, elapsed);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:UpdateCameraOrientationAndPosition()
|
||||
local yaw, pitch, roll = self:GetInterpolatedOrientation();
|
||||
local axisAngleX, axisAngleY, axisAngleZ = Vector3D_CalculateNormalFromYawPitch(yaw, pitch);
|
||||
|
||||
local targetX, targetY, targetZ = self:GetInterpolatedTarget();
|
||||
|
||||
local zoomDistance = self:GetInterpolatedZoomDistance();
|
||||
|
||||
-- Panning start --
|
||||
-- We want the model to move 1-to-1 with the mouse.
|
||||
-- Panning formula: dx / hypotenuse * (zoomDistance - 1 / zoomDistance^3)
|
||||
-- It was experimentally determined that adding the additional fudge factor 1/z^3 resulted in better tracking.
|
||||
|
||||
local width = self:GetOwningScene():GetWidth();
|
||||
local height = self:GetOwningScene():GetHeight();
|
||||
local scaleFactor = (width ~= 0 and height ~= 0) and math.sqrt(width * width + height * height) or 1;
|
||||
local zoomFactor = 1;
|
||||
if zoomDistance > 1 then
|
||||
zoomFactor = zoomDistance - (1 / (zoomDistance * zoomDistance * zoomDistance));
|
||||
if zoomFactor < 1 then
|
||||
zoomFactor = 1;
|
||||
end
|
||||
end
|
||||
|
||||
local rightX, rightY, rightZ = Vector3D_ScaleBy((self.panningXOffset / scaleFactor) * zoomFactor, self:GetRightVector());
|
||||
local upX, upY, upZ = Vector3D_ScaleBy((self.panningYOffset / scaleFactor) * zoomFactor, self:GetUpVector());
|
||||
|
||||
-- Panning end --
|
||||
|
||||
targetX, targetY, targetZ = Vector3D_Add(targetX, targetY, targetZ, rightX, rightY, rightZ);
|
||||
targetX, targetY, targetZ = Vector3D_Add(targetX, targetY, targetZ, upX, upY, upZ);
|
||||
|
||||
self:SetPosition(self:CalculatePositionByDistanceFromTarget(targetX, targetY, targetZ, zoomDistance, axisAngleX, axisAngleY, axisAngleZ));
|
||||
self:GetOwningScene():SetCameraOrientationByYawPitchRoll(yaw, pitch, roll);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:UpdateLight()
|
||||
if self:ShouldAlignLightToOrbitDelta() then
|
||||
local owningScene = self:GetOwningScene();
|
||||
owningScene:SetLightDirection(self:GetForwardVector());
|
||||
end
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:CalculatePositionByDistanceFromTarget(targetX, targetY, targetZ, zoomDistance, axisAngleX, axisAngleY, axisAngleZ)
|
||||
local towardsPosX, towardsPosY, towardsPosZ = Vector3D_ScaleBy(-zoomDistance, axisAngleX, axisAngleY, axisAngleZ);
|
||||
return Vector3D_Add(towardsPosX, towardsPosY, towardsPosZ, targetX, targetY, targetZ);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:CalculateTransitionalValues(fromModelSceneCameraInfo, toModelSceneCameraInfo, modificationType)
|
||||
if fromModelSceneCameraInfo and modificationType == CAMERA_MODIFICATION_TYPE_MAINTAIN then
|
||||
local zoomedTargetOffsetX, zoomedTargetOffsetY, zoomedTargetOffsetZ = 0, 0, 0;
|
||||
|
||||
local targetSpline = self:GetTargetSpline();
|
||||
if targetSpline and targetSpline:GetNumPoints() > 0 then
|
||||
zoomedTargetOffsetX, zoomedTargetOffsetY, zoomedTargetOffsetZ = targetSpline:GetPoint(targetSpline:GetNumPoints());
|
||||
end
|
||||
|
||||
local zoomedYawOffset, zoomedPitchOffset, zoomedRollOffset = 0, 0, 0;
|
||||
local orientationSpline = self:GetOrientationSpline();
|
||||
if orientationSpline and orientationSpline:GetNumPoints() > 0 then
|
||||
zoomedYawOffset, zoomedPitchOffset, zoomedRollOffset = orientationSpline:GetPoint(orientationSpline:GetNumPoints());
|
||||
end
|
||||
|
||||
local fromZoomDistancePercent = PercentageBetween(fromModelSceneCameraInfo.zoomDistance, fromModelSceneCameraInfo.minZoomDistance, fromModelSceneCameraInfo.maxZoomDistance);
|
||||
local toZoomDistancePercent = PercentageBetween(toModelSceneCameraInfo.zoomDistance, toModelSceneCameraInfo.minZoomDistance, toModelSceneCameraInfo.maxZoomDistance);
|
||||
-- Maintain any modifications made by applying the scene delta
|
||||
return {
|
||||
target = CreateVector3D(
|
||||
(self.targetX - fromModelSceneCameraInfo.target.x) + toModelSceneCameraInfo.target.x,
|
||||
(self.targetY - fromModelSceneCameraInfo.target.y) + toModelSceneCameraInfo.target.y,
|
||||
(self.targetZ - fromModelSceneCameraInfo.target.z) + toModelSceneCameraInfo.target.z
|
||||
),
|
||||
|
||||
yaw = (self.yaw - fromModelSceneCameraInfo.yaw) + toModelSceneCameraInfo.yaw,
|
||||
pitch = (self.pitch - fromModelSceneCameraInfo.pitch) + toModelSceneCameraInfo.pitch,
|
||||
roll = (self.roll - fromModelSceneCameraInfo.roll) + toModelSceneCameraInfo.roll,
|
||||
|
||||
zoomDistance = Lerp(toModelSceneCameraInfo.minZoomDistance, toModelSceneCameraInfo.maxZoomDistance, (self.zoomPercent - fromZoomDistancePercent) + toZoomDistancePercent),
|
||||
minZoomDistance = (self.minZoomDistance - fromModelSceneCameraInfo.minZoomDistance) + toModelSceneCameraInfo.minZoomDistance,
|
||||
maxZoomDistance = (self.maxZoomDistance - fromModelSceneCameraInfo.maxZoomDistance) + toModelSceneCameraInfo.maxZoomDistance,
|
||||
|
||||
zoomedTargetOffset = CreateVector3D(
|
||||
(zoomedTargetOffsetX - fromModelSceneCameraInfo.zoomedTargetOffset.x) + toModelSceneCameraInfo.zoomedTargetOffset.x,
|
||||
(zoomedTargetOffsetY - fromModelSceneCameraInfo.zoomedTargetOffset.y) + toModelSceneCameraInfo.zoomedTargetOffset.y,
|
||||
(zoomedTargetOffsetZ - fromModelSceneCameraInfo.zoomedTargetOffset.z) + toModelSceneCameraInfo.zoomedTargetOffset.z
|
||||
),
|
||||
|
||||
zoomedYawOffset = (zoomedYawOffset - fromModelSceneCameraInfo.zoomedYawOffset) + toModelSceneCameraInfo.zoomedYawOffset,
|
||||
zoomedPitchOffset = (zoomedPitchOffset - fromModelSceneCameraInfo.zoomedPitchOffset) + toModelSceneCameraInfo.zoomedPitchOffset,
|
||||
zoomedRollOffset = (zoomedRollOffset - fromModelSceneCameraInfo.zoomedRollOffset) + toModelSceneCameraInfo.zoomedRollOffset;
|
||||
};
|
||||
end
|
||||
|
||||
-- Nothing to transition from, just go directly to the values
|
||||
return toModelSceneCameraInfo;
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:OnMouseWheel(delta) -- override
|
||||
self:HandleMouseMovement(self.buttonModes.wheel, delta * self:GetDeltaModifierForCameraMode(self.buttonModes.wheel), not self.buttonModes.wheelInterpolate);
|
||||
end
|
||||
|
||||
function OrbitCameraMixin:AdjustYaw(deltaX, deltaY, rotationScalar)
|
||||
local xRotation = rotationScalar or self:GetDeltaModifierForCameraMode(self.buttonModes.leftX);
|
||||
self:HandleMouseMovement(self.buttonModes.leftX, deltaX * xRotation, not self.buttonModes.leftXinterpolate);
|
||||
|
||||
local yRotatation = rotationScalar or self:GetDeltaModifierForCameraMode(self.buttonModes.leftY);
|
||||
self:HandleMouseMovement(self.buttonModes.leftY, -deltaY * yRotatation, not self.buttonModes.leftYinterpolate);
|
||||
end
|
||||
@@ -23,15 +23,15 @@
|
||||
-- - 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.
|
||||
-- - The stack and locals are retail's GetErrorData, taken in the SetText
|
||||
-- slot: GetErrorCallstackHeight finds the code that failed however many
|
||||
-- handlers sit between it and the slot. No |K strings to remove, and no
|
||||
-- C_StringUtil.EscapeDecimalNonPrintables.
|
||||
-- - No scriptErrors cvar (1.12 always shows errors), no LUA_WARNING, no
|
||||
-- gamepad legend.
|
||||
-- - Lua 5.0: no `...` expression, no # operator.
|
||||
|
||||
local function GetErrorStack()
|
||||
local function GetErrorData()
|
||||
-- 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)
|
||||
@@ -40,13 +40,20 @@ local function GetErrorStack()
|
||||
local errorCallStackHeight = GetErrorCallstackHeight();
|
||||
local errorStackOffset = errorCallStackHeight and (errorCallStackHeight - 1);
|
||||
local debugStackLevel = currentStackHeight - (errorStackOffset or 0);
|
||||
return debugstack(debugStackLevel);
|
||||
local skipFunctionsAndUserdata = true;
|
||||
|
||||
local stack = debugstack(debugStackLevel);
|
||||
local locals = debuglocals(debugStackLevel, skipFunctionsAndUserdata);
|
||||
|
||||
return stack, locals;
|
||||
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\n"
|
||||
.. "|cffffd200Locals:|r\n"
|
||||
.. "|cffffffff%s|r";
|
||||
|
||||
local INDEX_ORDER_FORMAT = "%d / %d";
|
||||
@@ -68,15 +75,16 @@ function ScriptErrorsFrame_OnLoad(self)
|
||||
ScrollUtil.RegisterScrollBoxWithScrollBar(scrollBox, self.ScrollBar);
|
||||
|
||||
ScriptErrors_Message.SetText = function(_, message)
|
||||
ScriptErrorsFrame_OnError(message, GetErrorStack());
|
||||
ScriptErrorsFrame_OnError(message, GetErrorData());
|
||||
end
|
||||
ScriptErrors.Show = function() end
|
||||
end
|
||||
|
||||
function ScriptErrorsFrame_OnError(message, stack)
|
||||
function ScriptErrorsFrame_OnError(message, stack, locals)
|
||||
local self = ScriptErrorsFrame;
|
||||
message = tostring(message);
|
||||
stack = stack or "";
|
||||
locals = locals or "<none>";
|
||||
|
||||
local messageKey = message .. "\n" .. stack;
|
||||
local index = self.seen[messageKey];
|
||||
@@ -84,7 +92,7 @@ function ScriptErrorsFrame_OnError(message, stack)
|
||||
local errorData = self.errorData[index];
|
||||
errorData.count = errorData.count + 1;
|
||||
else
|
||||
table.insert(self.errorData, { count = 1, message = message, time = date(), stack = stack });
|
||||
table.insert(self.errorData, { count = 1, message = message, time = date(), stack = stack, locals = locals });
|
||||
index = table.getn(self.errorData);
|
||||
self.seen[messageKey] = index;
|
||||
end
|
||||
@@ -106,7 +114,7 @@ function ScriptErrorsFrame_Update()
|
||||
-- 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:SetText(string.format(ERROR_FORMAT, errorData.message, errorData.time, errorData.count, errorData.stack, errorData.locals));
|
||||
self.ScrollingEditBox:GetEditBox():HighlightText(0);
|
||||
else
|
||||
self.ScrollingEditBox:ClearText();
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
-- ClassicAPI's own entry in the AddOn Options window
|
||||
-- (FrameXML\InterfaceOptions.lua): a ClassicAPI page with one page under it
|
||||
-- for each feature. Every cvar that ClassicAPI registers has a control here,
|
||||
-- and so does the player cache.
|
||||
-- and so do the player cache and ClassicAPI's key bindings.
|
||||
--
|
||||
-- Each control reads and writes exactly one setting, and writes it only when
|
||||
-- the player uses that control. So the pages have no okay, cancel or default:
|
||||
-- Okay, Cancel and Escape only close the window, and Defaults does nothing
|
||||
-- here. A page reads its settings again each time it is shown.
|
||||
-- here. A page reads its settings again each time it is shown. A key binding
|
||||
-- saves at once into the binding set in use (account or character).
|
||||
--
|
||||
-- A page's controls scroll under its heading in a ScrollBox, so a page holds
|
||||
-- as many as its feature has. Dropdowns and number boxes sit on the same row
|
||||
-- as their label.
|
||||
--
|
||||
-- Every string is a global from locales\enUS.lua: retail's GlobalStrings key
|
||||
-- where retail has the string (ENABLE_INTERACT_TEXT and the like), a
|
||||
-- CLASSICAPI_ one where it does not.
|
||||
-- Every string is a global: the client's own (KEY_BOUND, NOT_BOUND), or one
|
||||
-- from locales\enUS.lua - retail's GlobalStrings key where retail has the
|
||||
-- string (ENABLE_INTERACT_TEXT and the like), a CLASSICAPI_ one where it
|
||||
-- does not.
|
||||
--
|
||||
-- Lua 5.0: no `...` expression or select().
|
||||
|
||||
@@ -72,13 +74,20 @@ local rememberSetting = {
|
||||
local LABEL_X = 48
|
||||
local CONTROL_X = 200
|
||||
|
||||
-- pfUI's Interface Options skin finds the controls by these names.
|
||||
-- pfUI's Interface Options skin finds the controls and the page scroll bars
|
||||
-- by these names.
|
||||
local numControls = 0
|
||||
local function NextName()
|
||||
numControls = numControls + 1
|
||||
return "ClassicAPIOptionsControl" .. numControls
|
||||
end
|
||||
|
||||
local numScrollBars = 0
|
||||
local function NextScrollBarName()
|
||||
numScrollBars = numScrollBars + 1
|
||||
return "ClassicAPIOptionsScrollBar" .. numScrollBars
|
||||
end
|
||||
|
||||
local function RefreshControls(page)
|
||||
page.updating = true
|
||||
for _, control in ipairs(page.controls) do
|
||||
@@ -114,7 +123,7 @@ local function NewPage(name, parent, title, subtitle)
|
||||
scrollBox:SetPoint("TOPLEFT", page, "TOPLEFT", 0, top)
|
||||
scrollBox:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", -20, 8)
|
||||
scrollBox:EnableMouseWheel(true)
|
||||
local scrollBar = CreateFrame("EventFrame", nil, page, "MinimalScrollBar")
|
||||
local scrollBar = CreateFrame("EventFrame", NextScrollBarName(), page, "MinimalScrollBar")
|
||||
scrollBar:SetPoint("TOPLEFT", scrollBox, "TOPRIGHT", 4, 0)
|
||||
scrollBar:SetPoint("BOTTOMLEFT", scrollBox, "BOTTOMRIGHT", 4, 0)
|
||||
|
||||
@@ -302,6 +311,215 @@ local function AddSlider(page, label, tooltip, setting, minValue, maxValue, step
|
||||
AddControl(page, slider, setting, 56)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Key bindings
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- A key binding row, as on retail's settings pages: the binding's name, then
|
||||
-- a button for each of its two keys. Left click a button and press a key, a
|
||||
-- mouse button or the wheel to bind it there; Escape cancels. Right click a
|
||||
-- button to unbind its key.
|
||||
--
|
||||
-- While a button listens, `listener` covers the screen above the window and
|
||||
-- takes every key, click and wheel turn, as retail's KeybindListener and its
|
||||
-- input blocker do. A left or right click on it cancels. The binding itself
|
||||
-- is 1.12's Key Bindings window's: the same chord order, the same check for
|
||||
-- an action that loses its last key, the same put-back when SetBinding
|
||||
-- refuses a key.
|
||||
--
|
||||
-- Messages go on the page's output line, under its controls.
|
||||
|
||||
local LISTEN_BUTTONS = {
|
||||
MiddleButton = "BUTTON3",
|
||||
Button4 = "BUTTON4",
|
||||
Button5 = "BUTTON5",
|
||||
}
|
||||
|
||||
-- Keys that only modify another key, as the Key Bindings window skips them.
|
||||
local IGNORED_KEYS = { UNKNOWN = true, SHIFT = true, CTRL = true, ALT = true }
|
||||
|
||||
local listener = CreateFrame("Button", nil, UIParent)
|
||||
listener:SetAllPoints(UIParent)
|
||||
listener:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
listener:EnableMouse(true)
|
||||
listener:EnableMouseWheel(true)
|
||||
listener:EnableKeyboard(true)
|
||||
listener:RegisterForClicks("LeftButtonUp", "RightButtonUp", "MiddleButtonUp", "Button4Up", "Button5Up")
|
||||
listener:Hide()
|
||||
|
||||
local function BindingName(action)
|
||||
return GetBindingText(action, "BINDING_NAME_")
|
||||
end
|
||||
|
||||
-- GetBindingKey's keys as a list: a row's button `i` shows keys[i].
|
||||
local function BindingKeys(action)
|
||||
return { GetBindingKey(action) }
|
||||
end
|
||||
|
||||
local function StopListening()
|
||||
local row = listener.row
|
||||
if not row then return end
|
||||
listener.row = nil
|
||||
listener:Hide()
|
||||
row.page.output:SetText("")
|
||||
RefreshControls(row.page)
|
||||
end
|
||||
|
||||
local function StartListening(row, index)
|
||||
-- A number box that keeps the focus would take the key instead.
|
||||
for _, control in ipairs(row.page.controls) do
|
||||
if control.ClearFocus then control:ClearFocus() end
|
||||
end
|
||||
listener.row = row
|
||||
listener.index = index
|
||||
listener:Show()
|
||||
row.page.output:SetText(format(SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL, BindingName(row.action), GetBindingText("ESCAPE", "KEY_")))
|
||||
RefreshControls(row.page)
|
||||
end
|
||||
|
||||
-- 1.12's chord order: ALT-CTRL-SHIFT-key.
|
||||
local function Chord(key)
|
||||
if IsShiftKeyDown() then key = "SHIFT-" .. key end
|
||||
if IsControlKeyDown() then key = "CTRL-" .. key end
|
||||
if IsAltKeyDown() then key = "ALT-" .. key end
|
||||
return key
|
||||
end
|
||||
|
||||
local function Bind(key)
|
||||
if IGNORED_KEYS[key] then return end
|
||||
local chord = Chord(key)
|
||||
local current = GetBindingAction(chord)
|
||||
if current == "SCREENSHOT" then
|
||||
Screenshot()
|
||||
return
|
||||
end
|
||||
|
||||
local row, index = listener.row, listener.index
|
||||
StopListening()
|
||||
if key == "ESCAPE" and current == "TOGGLEGAMEMENU" then return end
|
||||
|
||||
local action = row.action
|
||||
local message = KEY_BOUND
|
||||
if current ~= "" and current ~= action then
|
||||
local old1, old2 = GetBindingKey(current)
|
||||
if (not old1 or old1 == chord) and (not old2 or old2 == chord) then
|
||||
message = format(KEY_UNBOUND_ERROR, BindingName(current))
|
||||
end
|
||||
end
|
||||
|
||||
-- Unbind both keys, then bind them back in order with the new key in
|
||||
-- the slot that was clicked.
|
||||
local keys = BindingKeys(action)
|
||||
for i = 1, 2 do
|
||||
if keys[i] then SetBinding(keys[i]) end
|
||||
end
|
||||
for i = 1, 2 do
|
||||
local bind = (i == index) and chord or keys[i]
|
||||
if bind and not SetBinding(bind, action) then
|
||||
if keys[i] then SetBinding(keys[i], action) end
|
||||
message = KEYBINDINGFRAME_MOUSEWHEEL_ERROR
|
||||
end
|
||||
end
|
||||
SaveBindings(GetCurrentBindingSet())
|
||||
|
||||
RefreshControls(row.page)
|
||||
row.page.output:SetText(message)
|
||||
end
|
||||
|
||||
listener:SetScript("OnKeyDown", function() Bind(arg1) end)
|
||||
listener:SetScript("OnMouseWheel", function()
|
||||
Bind(arg1 > 0 and "MOUSEWHEELUP" or "MOUSEWHEELDOWN")
|
||||
end)
|
||||
listener:SetScript("OnClick", function()
|
||||
local button = LISTEN_BUTTONS[arg1]
|
||||
if button then Bind(button) else StopListening() end
|
||||
end)
|
||||
|
||||
local function Unbind(row, index)
|
||||
local key = BindingKeys(row.action)[index]
|
||||
if key then
|
||||
SetBinding(key)
|
||||
SaveBindings(GetCurrentBindingSet())
|
||||
end
|
||||
GameTooltip:Hide()
|
||||
row.page.output:SetText("")
|
||||
RefreshControls(row.page)
|
||||
end
|
||||
|
||||
-- The page's output line, made with its first key binding row. It takes the
|
||||
-- bottom of the page from the ScrollBox.
|
||||
local function EnsureOutput(page)
|
||||
if page.output then return end
|
||||
local output = page:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
|
||||
output:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 16, 12)
|
||||
output:SetPoint("RIGHT", page, "RIGHT", -16, 0)
|
||||
output:SetJustifyH("LEFT")
|
||||
page.scrollBox:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", -20, 30)
|
||||
page.output = output
|
||||
page:SetScript("OnHide", function()
|
||||
if listener.row and listener.row.page == this then StopListening() end
|
||||
this.output:SetText("")
|
||||
end)
|
||||
end
|
||||
|
||||
local function AddKeybinding(page, action)
|
||||
EnsureOutput(page)
|
||||
AddRowLabel(page, BindingName(action))
|
||||
|
||||
local row = { page = page, action = action, buttons = {} }
|
||||
for i = 1, 2 do
|
||||
local button = CreateFrame("Button", NextName(), page.content, "UIPanelButtonTemplate2")
|
||||
button:SetWidth(86)
|
||||
button:SetHeight(22)
|
||||
if i == 1 then
|
||||
button:SetPoint("TOPLEFT", page.content, "TOPLEFT", CONTROL_X, page.y - 5)
|
||||
else
|
||||
button:SetPoint("LEFT", row.buttons[1], "RIGHT", 0, 0)
|
||||
end
|
||||
button:SetTextFontObject(GameFontHighlightSmall)
|
||||
button:SetHighlightFontObject(GameFontHighlightSmall)
|
||||
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
button.index = i
|
||||
button:SetScript("OnClick", function()
|
||||
if arg1 == "RightButton" then
|
||||
Unbind(row, this.index)
|
||||
else
|
||||
StartListening(row, this.index)
|
||||
end
|
||||
end)
|
||||
button:SetScript("OnEnter", function()
|
||||
local key = BindingKeys(action)[this.index]
|
||||
if not key then return end
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(format(KEY_BINDING_NAME_AND_KEY, BindingName(action), GetBindingText(key, "KEY_")),
|
||||
HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
|
||||
GameTooltip:AddLine(KEY_BINDING_TOOLTIP, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1)
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
button:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
row.buttons[i] = button
|
||||
end
|
||||
|
||||
row.Update = function(self)
|
||||
local keys = BindingKeys(action)
|
||||
for i, button in ipairs(self.buttons) do
|
||||
if keys[i] then
|
||||
button:SetText(GetBindingText(keys[i], "KEY_"))
|
||||
button:SetAlpha(1)
|
||||
else
|
||||
button:SetText(NORMAL_FONT_COLOR_CODE .. NOT_BOUND .. FONT_COLOR_CODE_CLOSE)
|
||||
button:SetAlpha(0.8)
|
||||
end
|
||||
if listener.row == self and listener.index == i then
|
||||
button:LockHighlight()
|
||||
else
|
||||
button:UnlockHighlight()
|
||||
end
|
||||
end
|
||||
end
|
||||
AddControl(page, row, nil, 32)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- ClassicAPI
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -316,6 +534,8 @@ local main = NewPage("ClassicAPI", nil, "ClassicAPI " .. version, CLASSICAPI_OPT
|
||||
local interact = NewPage(CLASSICAPI_INTERACT_KEY, main.name)
|
||||
|
||||
AddCheckbox(interact, ENABLE_INTERACT_TEXT, OPTION_TOOLTIP_ENABLE_INTERACT, SoftTargetSetting("SoftTargetInteract"))
|
||||
AddKeybinding(interact, "INTERACTTARGET")
|
||||
AddKeybinding(interact, "INTERACTMOUSEOVER")
|
||||
AddDropDown(interact, CLASSICAPI_INTERACT_DIRECTION, CLASSICAPI_INTERACT_DIRECTION_TOOLTIP, NumberCVarSetting("SoftTargetInteractArc"), ARC_CHOICES)
|
||||
AddNumberBox(interact, CLASSICAPI_INTERACT_RANGE, CLASSICAPI_RANGE_TOOLTIP, NumberCVarSetting("SoftTargetInteractRange"))
|
||||
AddCheckbox(interact, CLASSICAPI_STRICT_INTERACT_RANGE, CLASSICAPI_STRICT_INTERACT_RANGE_TOOLTIP, CVarSetting("SoftTargetInteractRangeIsHard"))
|
||||
@@ -378,12 +598,26 @@ AddCheckbox(bags, CLASSICAPI_IGNORE_BACKPACK, CLASSICAPI_IGNORE_BACKPACK_TOOLTIP
|
||||
AddCheckbox(bags, CLASSICAPI_IGNORE_BANK, CLASSICAPI_IGNORE_BANK_TOOLTIP, CVarSetting("bankAutosortDisabled"))
|
||||
AddCheckbox(bags, CLASSICAPI_HIDE_EQUIPMENT_SETS, CLASSICAPI_HIDE_EQUIPMENT_SETS_TOOLTIP, CVarSetting("dontShowEquipmentSetsOnItems"))
|
||||
|
||||
AddHeader(bags, LOOT)
|
||||
AddKeybinding(bags, "AOELOOT")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Interface
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local interface = NewPage(CLASSICAPI_INTERFACE, main.name)
|
||||
|
||||
-- The label ends with some of the cursors the setting shows. The DLL serves
|
||||
-- each image at its Interface\Cursor path, and the markup stays out of the
|
||||
-- translated string.
|
||||
local expandedCursorIcons = ""
|
||||
for _, name in ipairs({ "RepairNPC", "Innkeeper", "StableMaster", "Quest", "QuestTurnIn", "Fishing" }) do
|
||||
expandedCursorIcons = expandedCursorIcons .. " |TInterface\\Cursor\\" .. name .. ":16|t"
|
||||
end
|
||||
|
||||
AddHeader(interface, CLASSICAPI_CURSOR)
|
||||
AddCheckbox(interface, CLASSICAPI_EXPANDED_CURSORS .. expandedCursorIcons, CLASSICAPI_EXPANDED_CURSORS_TOOLTIP, CVarSetting("expandedCursors"))
|
||||
|
||||
AddHeader(interface, CLASSICAPI_NAMEPLATES)
|
||||
AddSlider(interface, CLASSICAPI_TARGET_NAMEPLATE_ALPHA, CLASSICAPI_TARGET_NAMEPLATE_ALPHA_TOOLTIP, NumberCVarSetting("nameplateSelectedAlpha"), 0, 1, 0.05)
|
||||
AddSlider(interface, CLASSICAPI_OTHER_NAMEPLATE_ALPHA, CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP, NumberCVarSetting("nameplateNotSelectedAlpha"), 0, 1, 0.05)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user