Compare commits

...
2 Commits
Author SHA1 Message Date
Brues 3009c6d8d2 fix(addon): splice the AddOn Options menu button on first show
Addons that load after us insert their own game menu buttons by taking
Key Bindings' anchor too. ShaguTweaks anchors "Advanced Options" under
Interface Options, which put it on top of our button. Waiting for the
menu's first show lets every addon place its buttons first, so ours goes
right above Key Bindings wherever it ended up.
2026-10-22 23:05:22 -05:00
Brues b83bb5b52b feat(frame): draw the Cooldown sweep natively, as 4.3.4's CGCooldown
A Cooldown was vanilla's indicator Model with CooldownFrameTemplate loaded
into it, so SetReverse and SetDrawEdge stored flags that changed nothing,
and every cooldown paid a Model's per-frame cost.

Cooldown is now a CSimpleFrame subclass built the way CGCooldown::Create
builds it: one SMemAlloc block, the engine's frame constructor, and copies
of CSimpleFrame's two vtables with 4.3.4's overrides in their 1.12 slots
(update, layer emit, alpha, size change, LoadXML, identity, destructor).
It runs 4.3.4's sweep / flash / reverse state machine and queues the star
fan, flash and edge as layer-batch strips animated in place.

- The five methods live in a class method hash of their own, so only a
  Cooldown has them; it is released after CGGameUI's unregister pass.
- GetObjectType / GetFrameType answer through the engine's virtuals; the
  Lua re-registrations are gone.
- star4.blp ships with 1.12; edge.blp does not, so its 16x16 image is
  created in memory through TextureCreateWithCallback.
- A Cooldown inheriting the stock Model CooldownFrameTemplate applies
  4.3.4's template (setAllPoints, hidden); reverse / drawEdge XML
  attributes are read.
- CooldownFrame_SetTimer calls SetCooldown on a Cooldown and keeps 1.12's
  body for the Models stock buttons use.
- Mask.cpp's path loader moves to texture/Load; frame/RenderBatch queues
  quads into a layer batch.

Verified in-game: sweep, flash and hide; reverse with the edge line;
stock action-button cooldowns unchanged; repeated /reload.
2026-10-22 22:15:17 -05:00
14 changed files with 1260 additions and 342 deletions
+1
View File
@@ -49,6 +49,7 @@ Util\Vector4D.lua
Util\UiMapPoint.lua
Util\UIParent.lua
Util\ActionButton.lua
Util\Cooldown.lua
Util\WorldMap.lua
Util\AuraDurationModifiers.lua
Util\AuraUtil.lua
@@ -347,14 +347,25 @@ end
-- Goes right above Key Bindings. Stock anchors Key Bindings under Interface
-- Options and Turtle under its single Options button, so the new button takes
-- over whatever anchor Key Bindings has and Key Bindings moves under it.
-- Buttons other addons add at the top of the menu are unaffected.
--
-- The splice waits for the menu's first show. Other addons insert their own
-- buttons the same way -- ShaguTweaks anchors "Advanced Options" under
-- Interface Options and moves Key Bindings under it -- and they load after
-- us. Splicing at load time would claim the slot first, and the later addon
-- would anchor its button on the same spot as ours. By the first show every
-- addon has placed its buttons, so ours goes into the chain as it stands.
local menuButton = CreateFrame("Button", "GameMenuButtonAddOnOptions", GameMenuFrame, "GameMenuButtonTemplate")
menuButton:SetText(ADDON_OPTIONS)
local point, relativeTo, relativePoint, x, y = GameMenuButtonKeybindings:GetPoint()
menuButton:SetPoint(point, relativeTo, relativePoint, x, y)
GameMenuButtonKeybindings:ClearAllPoints()
GameMenuButtonKeybindings:SetPoint(point, menuButton, relativePoint, x, y)
GameMenuFrame:SetHeight(GameMenuFrame:GetHeight() + menuButton:GetHeight() - y)
local spliced
GameMenuFrame:HookScript("OnShow", function()
if spliced then return end
spliced = true
local point, relativeTo, relativePoint, x, y = GameMenuButtonKeybindings:GetPoint()
menuButton:SetPoint(point, relativeTo, relativePoint, x, y)
GameMenuButtonKeybindings:ClearAllPoints()
GameMenuButtonKeybindings:SetPoint(point, menuButton, relativePoint, x, y)
GameMenuFrame:SetHeight(GameMenuFrame:GetHeight() + menuButton:GetHeight() - y)
end)
menuButton:SetScript("OnClick", function()
PlaySound("igMainMenuOption")
ShowUIPanel(frame)
+27
View File
@@ -0,0 +1,27 @@
-- CooldownFrame_SetTimer for both kinds of cooldown frame. A Cooldown frame
-- (CreateFrame("Cooldown") or <Cooldown>) takes 4.3.4's body (Cooldown.lua),
-- which hands the timer to the frame's own SetCooldown. Every other frame is
-- one of 1.12's indicator Models (CooldownFrameTemplate, as the action, bag,
-- spellbook and paper-doll buttons build theirs) and keeps 1.12's body, which
-- drives the model's sweep from CooldownFrame_OnUpdateModel.
function CooldownFrame_SetTimer(this, start, duration, enable)
if ( this:IsObjectType("Cooldown") ) then
if ( start and start > 0 and duration > 0 and enable > 0 ) then
this:SetCooldown(start, duration);
this:Show();
else
this:Hide();
end
return;
end
if ( start > 0 and duration > 0 and enable > 0) then
this.start = start;
this.duration = duration;
this.stopping = 0;
this:SetSequence(0);
this:Show();
else
this:Hide();
end
end
+23 -21
View File
@@ -6496,7 +6496,8 @@ ignores a negative `id`.
### `CreateFrame("Cooldown" [, name, parent, template])`
Creates a cooldown frame. The frame shows a cooldown sweep.
Creates a cooldown frame. The frame shows a dark sweep that moves clockwise
over its area, then a short flash, then hides itself.
```lua
local cd = CreateFrame("Cooldown", nil, button, "CooldownFrameTemplate")
@@ -6505,43 +6506,44 @@ cd:IsObjectType("Cooldown") -- true
cd:SetCooldown(GetTime(), 10) -- start, duration in seconds
cd:SetReverse(false)
cd:GetReverse() -- false
cd:SetDrawEdge(false)
cd:GetDrawEdge() -- false
cd:SetDrawEdge(true)
cd:GetDrawEdge() -- true
```
| Method | Notes |
| --- | --- |
| `cooldown:SetCooldown(start, duration)` | Sets the sweep. `start` and `duration` are numbers in seconds. `start` is a time value, for example the value that `GetTime()` returns. The frame shows the sweep. If `start` or `duration` is not positive, the frame hides. |
| `cooldown:SetReverse(reverse)` / `cooldown:GetReverse()` | Sets and returns the reverse flag. The flag is `false` when the frame is created. |
| `cooldown:SetDrawEdge(drawEdge)` / `cooldown:GetDrawEdge()` | Sets and returns the draw-edge flag. The flag is `false` when the frame is created. |
| `cooldown:SetCooldown(start, duration)` | Starts the sweep and shows the frame. `start` and `duration` are numbers in seconds. `start` is a time value, for example the value that `GetTime()` returns. When `duration` is over, the frame shows the flash for one second and then hides. |
| `cooldown:SetReverse(reverse)` / `cooldown:GetReverse()` | Sets and returns the reverse flag. With the flag set, the sweep starts full and becomes empty. At the end, the frame stays shown with no flash. The flag is `false` when the frame is created. |
| `cooldown:SetDrawEdge(drawEdge)` / `cooldown:GetDrawEdge()` | Sets and returns the draw-edge flag. With the flag set, a bright line moves along the edge of the sweep. The flag is `false` when the frame is created. |
If `start` or `duration` is not a number, `SetCooldown` gives the error
`Usage: <name>:SetCooldown(start, duration)`.
You can also use `CooldownFrame_SetTimer(cd, start, duration, 1)` to set
the sweep.
The draw-edge flag applies from the next `SetCooldown` call.
The reverse flag and the draw-edge flag do not change the sweep picture.
The getter methods return the values that the setter methods set.
You can also use `CooldownFrame_SetTimer(cd, start, duration, enable)`.
This function calls `SetCooldown` on a cooldown frame. If `start`,
`duration`, or `enable` is not more than 0, it hides the frame.
Use these methods on a cooldown frame. A call on a frame of another type
gives an error.
Only cooldown frames have these five methods.
XML files can also create a cooldown frame with a `<Cooldown>` element:
A new cooldown frame has a sweep that is already over. If the frame is
shown before the first `SetCooldown` call, it hides itself after one
second.
XML files can also create a cooldown frame with a `<Cooldown>` element.
The `reverse` and `drawEdge` attributes set the two flags:
```xml
<Frames>
<Cooldown name="$parentCooldown" inherits="CooldownFrameTemplate"/>
<Cooldown name="$parentCooldown" inherits="CooldownFrameTemplate" drawEdge="true"/>
</Frames>
```
Every cooldown frame shows the sweep, also without a template. Its
`template` and `inherits` templates apply after the sweep, so they can
change it.
A cooldown frame starts without anchors. Give it a position with
`SetPoint`, `SetAllPoints`, or `<Anchors>`. `CooldownFrameTemplate` sets
the frame to the full size of its parent.
A cooldown frame needs no template. A cooldown frame starts without
anchors. Give it a position with `SetPoint`, `SetAllPoints`, or
`<Anchors>`. `CooldownFrameTemplate` sets the frame to the full size of
its parent, and the frame starts hidden.
### `frame:SetShown(shown)`
+131 -14
View File
@@ -3817,16 +3817,6 @@ enum Offsets {
// GetEffectiveAlpha calls it for its `self` type check, then reads the
// byte.
FUN_SCRIPT_FRAME_GETALPHA = 0x00774DC0,
// Region GetObjectType / IsObjectType (Region table 0x0087C9B8 entries
// 0/1, ctx VAR_REGION_METHOD_REGISTRY) and Frame GetFrameType /
// IsFrameType (Frame table 0x00878EC0 entries 0/1, ctx
// VAR_FRAME_METHOD_REGISTRY), per docs/raw_methods.txt.
// `Frame::Cooldown` re-registers these names on the Model registry and
// delegates to them for every frame that is not a marked Cooldown.
FUN_SCRIPT_REGION_GETOBJECTTYPE = 0x007A11D0,
FUN_SCRIPT_REGION_ISOBJECTTYPE = 0x007A1290,
FUN_SCRIPT_FRAME_GETFRAMETYPE = 0x00773640,
FUN_SCRIPT_FRAME_ISFRAMETYPE = 0x00773700,
// Hyperlink scripts. ScrollingMessageFrame and SimpleHTML share a base
// whose GetScriptByName override (0x007A3700, in the vtables at slots
@@ -10840,10 +10830,123 @@ enum Offsets {
// FUN_0048FBF0 after LoadScriptFunctions and before FrameXML loads — and
// the glue UI's FUN_0046A400.
FUN_FRAMEXML_REGISTER_FACTORY = 0x006EE880,
// The "Model" factory from that default list: allocates the 0x3DC-byte
// CSimpleModel and constructs it on `parent` (0x0076C8E0).
// `CSimpleFrame *__fastcall(CSimpleFrame *parent)`.
FUN_FRAMEXML_CREATE_MODEL = 0x006EECF0,
// ---- Subclassing CSimpleFrame (Frame::Cooldown) -------------------------
// The "Frame" factory of the default list (0x006EEC10) is
// `SMemAlloc(SIMPLEFRAME_SIZE, file, line, 0)` then the constructor
// `CSimpleFrame::CSimpleFrame` — `__thiscall(frame, parent)`, `ret 4` —
// which stores VTBL_SIMPLEFRAME at +0 and VTBL_SIMPLEFRAME_LAYOUT at
// OFF_FRAME_LAYOUT_SUBOBJECT (0x007692E5 / 0x007692EB). Every derived
// factory (Model 0x006EECF0: 0x3DC bytes, CSimpleModel ctor) has the same
// shape. Slot 0's deleting destructor (0x00769380) runs the destructor
// body FUN_FRAME_DESTRUCTOR and SMemFrees the block, so a larger block
// from SMemAlloc frees cleanly. The vtables are 36 and 11 slots: the main
// one ends where the layout one's successor (title region, 0x0081C528)
// begins, and the layout one ends at VTBL_SIMPLEFRAME (benilla-re
// ui/frames, dumped from the exe).
SIMPLEFRAME_SIZE = 0x314,
FUN_SIMPLEFRAME_CONSTRUCTOR = 0x00769090,
VTBL_SIMPLEFRAME = 0x0081C498,
VTBL_SIMPLEFRAME_SLOTS = 36,
VTBL_SIMPLEFRAME_LAYOUT = 0x0081C46C,
VTBL_SIMPLEFRAME_LAYOUT_SLOTS = 11,
// Main-vtable slots, all __thiscall (CSimpleFrame's own in brackets):
// +0x00 deleting destructor (this, flags), ret 4 [0x00769380]
// +0x08 GetScriptMethod (this, L, name) -> int, ret 8 [0x00778590]:
// FUN_FRAMESCRIPT_PUSH_METHOD on the class hash, else the base
// class's lookup — the `__index` dispatcher's per-class step
// +0x10 IsA (this, int typeId) -> bool, ret 4 [0x00484780]
// +0x18 IsObjectType (this, const char *) -> bool, ret 4 [0x004847B0]
// +0x1C GetObjectType (this) -> const char * [0x00484800]:
// what Script GetObjectType (0x007A11D0) and GetFrameType
// (0x00773640) push, after IsA(Region/Frame id) via +0x10
// +0x38 OnFrameUpdate (this, float elapsedSec), ret 4 [0x0076B2C0]:
// the update walk's per-frame call (CSimpleTop 0x00765707);
// runs OnUpdate
// +0x44 EmitLayer (this, batch, int layer), ret 8 [0x0076B3A0]:
// the level rebuild's per-frame call (0x007659ED) — 4.3.4's
// OnFrameRender(batch, layer)
// +0x84 Hide (this) / +0x88 Show (this): the visibility half of 1.12's
// Show/Hide; Script Show/Hide (0x00775750/0x00775810) and the XML
// `hidden` attribute (0x007698A6/0x007698BA) first write
// OFF_FRAME_SHOWN 1/0, then call these.
// (+0x28 is VTBL_FRAME_SET_FRAME_ALPHA.)
VTBL_FRAME_DELETING_DESTRUCTOR = 0x00,
VTBL_FRAME_GET_SCRIPT_METHOD = 0x08,
VTBL_FRAME_IS_A = 0x10,
VTBL_FRAME_IS_OBJECT_TYPE = 0x18,
VTBL_FRAME_GET_OBJECT_TYPE = 0x1C,
VTBL_FRAME_ON_FRAME_UPDATE = 0x38,
VTBL_FRAME_EMIT_LAYER = 0x44,
VTBL_FRAME_HIDE = 0x84,
VTBL_FRAME_SHOW = 0x88,
// Layout-vtable slots, `this` = the layout sub-object:
// +0x00 OnFrameSizeChanged (this, const float *oldRect), ret 4
// [0x0076B580]
// +0x04 deleting destructor thunk: `sub ecx, 0x24; jmp 0x00769380`
// (0x0076C410) — it does NOT go through slot 0
// +0x08 VTBL_XML_NODE_LOADER (FUN_XML_FRAME_LOAD)
// +0x0C GetLayoutParent (this) -> layout or 0 [0x0076C6E0]
VTBL_LAYOUT_ON_FRAME_SIZE_CHANGED = 0x00,
VTBL_LAYOUT_DELETING_DESTRUCTOR = 0x04,
VTBL_LAYOUT_GET_LAYOUT_PARENT = 0x0C,
// CLayoutFrame::LoadXML's `setAllPoints="true"` (0x00767800) is
// FUN_LAYOUT_SET_ALL_POINTS(layout, GetLayoutParent(), 1).
//
// CSimpleFrame::NotifyLayerChanged — `void __thiscall(frame, int layer)`,
// `ret 4`: marks one draw layer for a rebuild, in the frame's own mask
// when it draws itself (FRAME_FLAG_SCROLLED), else in its level's when
// the frame is visible. Nothing to mark when it is not: showing it
// joins the level, which rebuilds it.
FUN_FRAME_NOTIFY_LAYER_CHANGED = 0x0076A990,
// ---- Per-class method hashes --------------------------------------------
// Every frame class keeps its Lua methods in a Storm hash of 10 dwords
// (0x28 bytes): +0x00 the node allocator vtable VTBL_METHOD_HASH_ALLOCATOR,
// +0x04 the node link offset 0xC, +0x08/+0x0C an empty list (head = &+0x08,
// tail = &+0x08 | 1), +0x10..+0x20 zero (the bucket array), +0x24 the
// bucket mask, -1 until the first registration allocates 4 buckets.
// That is the whole static initializer (Model's 0x0076F5F0, Frame's
// 0x00778310 — both store 0x00806CB8). FUN_REGISTER_FRAME_METHODS fills
// one (it always appends a node: re-registering a name it holds without
// unregistering first leaves the old node and its stale ref), and
// FrameScriptUnregisterMethods — `void __fastcall(hash)` — unrefs each
// function in the live Lua state and frees the nodes, keeping the
// buckets for the next registration.
// FrameScriptPushMethod — `int __fastcall(L, const char *name, hash)`,
// callee pops `hash`: pushes registry[ref] and answers 1 when found.
VTBL_METHOD_HASH_ALLOCATOR = 0x00806CB8,
METHOD_HASH_SIZE = 0x28,
FUN_FRAMESCRIPT_PUSH_METHOD = 0x00702000,
FUN_FRAMESCRIPT_UNREGISTER_METHODS = 0x00701F20,
// CGGameUI::UnregisterScriptFunctions — `void __cdecl(void)`: the mirror
// of LoadScriptFunctions, called from the game UI's shutdown (0x00490CBA)
// on /reload and logout while the game Lua state is still open. Releases
// the class hashes (GameTooltip, Minimap, PlayerModel, ...) with
// FUN_FRAMESCRIPT_UNREGISTER_METHODS and unregisters the globals.
FUN_GAMEUI_UNREGISTER_SCRIPT_FUNCTIONS = 0x00490CE0,
// ---- Layer batch quads ----------------------------------------------------
// A layer batch (0x30 bytes, benilla-re ui/draworder) counts its entries
// at +0x04 and keeps its quads in an array at +0x08: capacity, count
// (+0x0C), records (+0x10), growth step. A record is 0x1C bytes: device
// texture (FUN_TEXTURE_GET_RENDERABLE), float[4][3] positions, float[4][2]
// texcoords, blend mode, colour pointer or 0, colour stride (4 = one
// colour per vertex, 0 = one for all), pixel shader or 0. The texture
// emit FUN_00772FD0 writes exactly that after reserving one record with
// FUN_RENDERBATCH_RESERVE_QUADS — `void __thiscall(array, extra, round)`,
// `ret 8`, a no-op while there is room. The batch keeps the POINTERS and
// re-reads them on every draw (FUN_0076FB00: a 4-vertex strip with the
// indices {0,1,2,3} at 0x00878CDC, so the triangles are 0-1-2 and
// 1-2-3); it is rebuilt only when a layer is marked changed. A record
// with a colour and a blend mode below 2 draws with blend 2 (alpha), the
// same promotion 3.3.5's SortByTexture makes.
FUN_RENDERBATCH_RESERVE_QUADS = 0x00773280,
OFF_RENDERBATCH_ENTRY_COUNT = 0x04,
OFF_RENDERBATCH_QUADS = 0x08,
OFF_RENDERBATCH_QUAD_COUNT = 0x0C,
OFF_RENDERBATCH_QUAD_RECORDS = 0x10,
RENDERBATCH_QUAD_SIZE = 0x1C,
// CLayoutFrame::ClearAllPoints — `void __fastcall(layout)`, layout =
// object + OFF_FRAME_LAYOUT_SUBOBJECT. Frees all 9 anchor slots and
// unlinks the frame from each relative frame. Script ClearAllPoints
@@ -11029,6 +11132,20 @@ enum Offsets {
// load — calling it each frame IS the residency reference (what the
// minimap's masked draw does for its mask at 0x004ec65c).
FUN_TEXTURE_LOAD_BY_PATH = 0x00449D90,
// TextureCreateWithCallback — `HTEXTURE __fastcall(uint32 w, uint32 h,
// uint32 mips, uint32 format, uint32 flags, void *owner, callback, const
// char *name, int force)`: a named texture whose texels `callback`
// supplies, never deduplicated by name. The portrait cache (0x00525F58:
// 64x64, 1 mip, GXTEX_FORMAT_ARGB8888, FUN_GX_TEXFLAGS_INIT(1, 0,0, 0,0,0,
// 1, 0), "Portrait2") is the model. The callback is `void
// __fastcall(uint32 command, uint32 w, uint32 h, uint32 face, uint32
// level, void *owner, uint32 *stride, const void **texels)`, `ret 0x18`;
// on GXTEX_CMD_LATCH it answers stride = w * 4 and the texels, which it
// keeps (the portrait's 0x00525AE0 hands its cached array over without a
// copy), and ignores every other command.
FUN_TEXTURE_CREATE_WITH_CALLBACK = 0x00449BF0,
GXTEX_FORMAT_ARGB8888 = 2,
GXTEX_CMD_LATCH = 1,
PTR_TEXLOAD_DESC_VTBL = 0x007FFA10,
FUN_GX_TEXFLAGS_INIT = 0x0058A980,
FUN_TEXTURE_GET_RENDERABLE = 0x0044ACF0,
+12
View File
@@ -167,10 +167,16 @@ const WidgetSystem kWidgetSystems[] = {
{Offsets::VAR_MODEL_METHOD_REGISTRY, "SimpleModelAPI"},
};
// Hashes of the frame classes this DLL adds (NameWidgetSystem).
std::vector<WidgetSystem> g_ownWidgetSystems;
const char *WidgetSystemFor(const void *registry) {
for (const WidgetSystem &w : kWidgetSystems)
if (reinterpret_cast<const void *>(w.registry) == registry)
return w.system;
for (const WidgetSystem &w : g_ownWidgetSystems)
if (reinterpret_cast<const void *>(w.registry) == registry)
return w.system;
return nullptr;
}
@@ -659,6 +665,12 @@ void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry
}
}
void NameWidgetSystem(const void *registry, const char *system) {
if (WidgetSystemFor(registry) != nullptr)
return;
g_ownWidgetSystems.push_back({reinterpret_cast<uintptr_t>(registry), system});
}
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
int count, const char *system) {
if (g_pass == ENV_NONE || sub == nullptr)
+3
View File
@@ -34,6 +34,9 @@ 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);
// 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);
+791 -251
View File
File diff suppressed because it is too large Load Diff
+62
View File
@@ -0,0 +1,62 @@
// 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 "frame/RenderBatch.h"
#include "Game.h"
#include "Offsets.h"
namespace Frame::RenderBatch {
namespace {
using GetRenderable_t = void *(__fastcall *)(void *texture, int load, void *callback);
using ReserveQuads_t = void(__thiscall *)(void *array, uint32_t extra, int round);
struct QuadRecord {
void *deviceTexture;
const C3Vector *positions;
const C2Vector *texCoords;
uint32_t blend;
const uint32_t *color;
uint32_t colorStride;
void *shader;
};
static_assert(sizeof(QuadRecord) == Offsets::RENDERBATCH_QUAD_SIZE, "quad record layout");
} // namespace
void QueueStrip(void *batch, void *texture, Blend blend, const C3Vector *positions,
const C2Vector *texCoords, const uint32_t *color) {
// Loading on demand, as the texture emit does: a texture still streaming
// is finished here, so the first rebuild already draws it.
void *device = reinterpret_cast<GetRenderable_t>(Offsets::FUN_TEXTURE_GET_RENDERABLE)(
texture, 1, nullptr);
if (device == nullptr)
return;
reinterpret_cast<ReserveQuads_t>(Offsets::FUN_RENDERBATCH_RESERVE_QUADS)(
Game::Ptr<uint8_t>(batch, Offsets::OFF_RENDERBATCH_QUADS), 1, 1);
uint32_t &count = Game::Ref<uint32_t>(batch, Offsets::OFF_RENDERBATCH_QUAD_COUNT);
QuadRecord &r = Game::Ref<QuadRecord *>(batch, Offsets::OFF_RENDERBATCH_QUAD_RECORDS)[count];
r.deviceTexture = device;
r.positions = positions;
r.texCoords = texCoords;
r.blend = blend;
r.color = color;
r.colorStride = 0;
r.shader = nullptr;
++count;
++Game::Ref<uint32_t>(batch, Offsets::OFF_RENDERBATCH_ENTRY_COUNT);
}
} // namespace Frame::RenderBatch
+50
View File
@@ -0,0 +1,50 @@
// 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
// Geometry a frame draws itself, queued into the layer batch its EmitLayer
// slot is handed — 4.3.4's CRenderBatch::Queue, in the one shape 1.12's batch
// draws: a four-vertex strip (triangles 0-1-2 and 1-2-3). See the Offsets.h
// "Layer batch quads" block.
#include <cstdint>
namespace Frame::RenderBatch {
struct C3Vector {
float x, y, z;
};
struct C2Vector {
float x, y;
};
// Blend modes (EGxBlend).
enum Blend : uint32_t {
BLEND_OPAQUE = 0,
BLEND_ALPHA_KEY = 1,
BLEND_ALPHA = 2,
BLEND_ADD = 3,
};
// Appends one strip of `texture` (an HTEXTURE) to `batch`, the way a
// texture region emits itself (FUN_00772FD0). `color` is one BGRA colour for
// all four vertices. The batch keeps the three pointers and reads them on
// every draw until its layer is rebuilt, so they must stay valid and may be
// rewritten in place to animate. Adds nothing when the texture has no device
// texture.
void QueueStrip(void *batch, void *texture, Blend blend, const C3Vector *positions,
const C2Vector *texCoords, const uint32_t *color);
} // namespace Frame::RenderBatch
+100
View File
@@ -0,0 +1,100 @@
// 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 "texture/Load.h"
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
namespace Texture {
namespace {
using HandleRelease_t = void(__fastcall *)(void *handle);
using SysMsgAdd_t = int(__fastcall *)(void *status, uint32_t level);
using StatusDestroy_t = void(__thiscall *)(void *status);
using TexFlagsInit_t = void *(__thiscall *)(void *self, uint32_t blend, int, int, int, int, int,
uint32_t, int);
using TextureLoad_t = uint32_t(__fastcall *)(const char *path, void *desc, uint32_t flags, int,
int);
using CreateWithCallback_t = void *(__fastcall *)(uint32_t w, uint32_t h, uint32_t mips,
uint32_t format, uint32_t flags, void *owner,
void *callback, const char *name, int force);
struct TexLoadDesc {
void *vtbl;
int32_t field4;
void *self8;
uint32_t fieldC;
int32_t field10;
};
// SetTexture's flags: the default filter, clamped, one mip level.
uint32_t DefaultFlags() {
uint32_t flags = 0;
const uint32_t blend = Game::Read<uint32_t>(Offsets::VAR_TEXTURE_BLEND_DEFAULT);
reinterpret_cast<TexFlagsInit_t>(Offsets::FUN_GX_TEXFLAGS_INIT)(&flags, blend, 0, 0, 0, 0, 0,
1, 0);
return flags;
}
// The texel source of CreateFromPixels, the portrait cache's 0x00525AE0 with
// the array as the owner.
void __fastcall LatchPixels(uint32_t command, uint32_t width, uint32_t /*height*/,
uint32_t /*face*/, uint32_t /*level*/, void *owner, uint32_t *stride,
const void **texels) {
if (command != Offsets::GXTEX_CMD_LATCH)
return;
*stride = width * 4;
*texels = owner;
}
} // namespace
void ReleaseHandle(void *handle) {
if (handle != nullptr)
reinterpret_cast<HandleRelease_t>(Offsets::FUN_HANDLE_RELEASE)(handle);
}
// Same failure test and cleanup as SetTexture: a status level of 2 or more
// closes the green fallback and passes the status to the (silent)
// system-message printer; the status is destroyed either way.
void *LoadByPath(const char *path) {
TexLoadDesc desc;
desc.vtbl = reinterpret_cast<void *>(Offsets::PTR_TEXLOAD_DESC_VTBL);
desc.field4 = 8;
desc.self8 = &desc.self8;
desc.fieldC = reinterpret_cast<uintptr_t>(&desc.self8) | 1u;
desc.field10 = 0;
void *handle = reinterpret_cast<void *>(reinterpret_cast<TextureLoad_t>(
Offsets::FUN_TEXTURE_LOAD_BY_PATH)(path, &desc, DefaultFlags(), 0, 1));
if (desc.field10 >= 2) {
ReleaseHandle(handle);
handle = nullptr;
reinterpret_cast<SysMsgAdd_t>(Offsets::FUN_SYSMSG_ADD_STATUS)(&desc, 4);
}
reinterpret_cast<StatusDestroy_t>(Offsets::FUN_FRAMESCRIPT_STATUS_RESET)(&desc);
return handle;
}
void *CreateFromPixels(const char *name, uint32_t width, uint32_t height,
const uint32_t *texels) {
return reinterpret_cast<CreateWithCallback_t>(Offsets::FUN_TEXTURE_CREATE_WITH_CALLBACK)(
width, height, 1, Offsets::GXTEX_FORMAT_ARGB8888, DefaultFlags(),
const_cast<uint32_t *>(texels), reinterpret_cast<void *>(&LatchPixels), name, 0);
}
} // namespace Texture
+38
View File
@@ -0,0 +1,38 @@
// 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
// Path -> HTEXTURE, the load CSimpleTexture::SetTexture (FUN_00770200) makes.
#include <cstdint>
namespace Texture {
// The loaded handle, or nullptr when the file does not load. Linear filter,
// clamped, the flags SetTexture's own call builds. The caller owns one
// reference and drops it with ReleaseHandle.
void *LoadByPath(const char *path);
// Drops one reference on a handle (FUN_HANDLE_RELEASE). Null is a no-op.
void ReleaseHandle(void *handle);
// A one-mip `width` x `height` texture of ARGB8888 `texels`, through the
// engine's own procedural path (FUN_TEXTURE_CREATE_WITH_CALLBACK) with the
// same flags as LoadByPath. The engine reads `texels` whenever it uploads the
// texture, so they must outlive the handle. Never deduplicated: each call is
// a new texture.
void *CreateFromPixels(const char *name, uint32_t width, uint32_t height,
const uint32_t *texels);
} // namespace Texture
+3 -48
View File
@@ -54,6 +54,7 @@
#include "Game.h"
#include "Offsets.h"
#include "texture/Load.h"
#include "texture/Mask.h"
#include "texture/Transform.h"
#include "ui/FrameObject.h"
@@ -96,56 +97,10 @@ std::unordered_map<void *, std::vector<void *>> g_baseMasks;
// Regions minted by CreateMaskTexture — a mask-source allow-list.
std::unordered_set<void *> g_maskRegions;
// --- path -> HTEXTURE loader (mirrors FUN_00770200's SetTexture load) --------
using HandleRelease_t = void(__fastcall *)(void *handle);
using SysMsgAdd_t = int(__fastcall *)(void *status, uint32_t level);
using StatusDestroy_t = void(__thiscall *)(void *status);
void ReleaseHandle(void *handle) {
if (handle != nullptr)
reinterpret_cast<HandleRelease_t>(Offsets::FUN_HANDLE_RELEASE)(handle);
}
using TexFlagsInit_t = void *(__thiscall *)(void *self, uint32_t blend, int, int, int, int, int,
uint32_t, int);
using TextureLoad_t = uint32_t(__fastcall *)(const char *path, void *desc, uint32_t flags, int,
int);
using GetRenderable_t = void *(__fastcall *)(void *hTex, int force, void *);
struct TexLoadDesc {
void *vtbl;
int32_t field4;
void *self8;
uint32_t fieldC;
int32_t field10;
};
// The loaded handle, or nullptr when the file does not load. Same failure
// test and cleanup as SetTexture: a status level of 2 or more closes the
// green fallback and passes the status to the (silent) system-message
// printer; the status is destroyed either way.
void *LoadByPath(const char *path) {
TexLoadDesc desc;
desc.vtbl = reinterpret_cast<void *>(Offsets::PTR_TEXLOAD_DESC_VTBL);
desc.field4 = 8;
desc.self8 = &desc.self8;
desc.fieldC = reinterpret_cast<uintptr_t>(&desc.self8) | 1u;
desc.field10 = 0;
uint32_t flags = 0;
const uint32_t blend = Game::Read<uint32_t>(Offsets::VAR_TEXTURE_BLEND_DEFAULT);
reinterpret_cast<TexFlagsInit_t>(Offsets::FUN_GX_TEXFLAGS_INIT)(&flags, blend, 0, 0, 0, 0, 0,
1, 0);
void *handle = reinterpret_cast<void *>(reinterpret_cast<TextureLoad_t>(
Offsets::FUN_TEXTURE_LOAD_BY_PATH)(path, &desc, flags, 0, 1));
if (desc.field10 >= 2) {
ReleaseHandle(handle);
handle = nullptr;
reinterpret_cast<SysMsgAdd_t>(Offsets::FUN_SYSMSG_ADD_STATUS)(&desc, 4);
}
reinterpret_cast<StatusDestroy_t>(Offsets::FUN_FRAMESCRIPT_STATUS_RESET)(&desc);
return handle;
}
using Texture::LoadByPath;
using Texture::ReleaseHandle;
// Drops a region's path mask and its handle reference.
void ClearPathMask(void *region) {
+2 -2
View File
@@ -24,8 +24,8 @@
// `Frame::Attributes`' `<Attributes>`), so — like `Frame::RunnerHook` and
// `Tick::WorldTick` — this module owns the hook and fans out. Subscribers run
// after the engine has loaded the node, in static-init order, which must not
// matter; `AutoSubscribeBefore` ones run before it (`Frame::Cooldown` loads
// its base template there). The hook also runs the loader through `Xml::Inherits::LoadFrame`,
// matter; `AutoSubscribeBefore` ones run before it (`Xml::Mixin` applies a
// frame's mixins there). The hook also runs the loader through `Xml::Inherits::LoadFrame`,
// which reads `inherits` as a list. Declare a file-scope
// static const Xml::FrameLoad::AutoSubscribe _sub{&OnFrameNode};