mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
feat(texture): add static creature portraits by display ID (#123)
* feat(texture): add static creature portraits by display ID * docs(api): document static creature portrait functions * Update API.md * refactor(model): bake display-ID portraits on the engine's own path SetPortraitTextureFromCreatureDisplayID now replays 1.12.1's unit portrait bake, BakePortrait (0x00524F60), call for call on a display model of its own, instead of driving a hidden PlayerModel through Lua. The display model is built the way AttachDisplayModel (0x00505A70) builds one, without a frame: M2CreateScene (0x00706E10), then CreateModelFromPath (0x00707350) on the CreatureModelData path, then ApplyCreatureDisplay (0x004797B0). A character display is dressed through the engine compositor with the same code that dresses PlayerModel:SetDisplayInfo, now shared as Model::DisplayInfo:: CreateDresser / PumpDresser / DestroyDresser. The bake's own gate, IsReady(model, 0, 1) (0x00710450), only passes for a model that something has drawn: a BLP texture gets its device texture only when GetTextureDeviceObject (0x0044ACF0) is asked with wait set. The shared M2 still loads on its own, so the gate here is IsLoaded(model, 0, 1) (0x007103D0) plus the dress, and the bake's IsReady(copy, 1, 1) makes the device textures, as it does for the copy of a unit's model. The bake itself is the engine's sequence: copy into a fresh scene, DetachAllItemModels, camera 0, PlayAnimation, save projection, view, viewport and fog (GxMasterEnable 0x0058A210 / 0x0058A1E0), the 64x64 DDC box in the bake's own float order, CameraSetDeviceTransforms, clear colour 0, ClearDevice(3), PortraitLighting, both scene passes, read back through 0x00589CF0, clear, cut the alpha mask in (0x005258A0), restore. It runs at the head of the UI render callback, the same point as an OnUpdate script that calls SetPortraitTexture, behind the bake's device gate (0x0058A260 window active, 0x0058A280 device alive). Each display ID owns one callback texture (TextureCreateWithCallback, which makes its device texture at creation). It is bound with SetImage (0x00770300) at call time and filled in place with GxTexMarkRectDirty (0x0058ACD0), the bake's own path for re-baking a cached portrait. A region the caller re-points before the bake finishes keeps what it was given, so no region is tracked: the request map, the region-destruction subscription and ClassicAPI_CancelPortraitTextureRequest are gone. The function returns nothing, as retail's does. Portraits are kept until logout, the lifetime of the engine's display-ID portrait cache (cleared in ShutdownGame). A display with no portrait camera is done once and stays transparent. Offsets.h: the portrait block is renamed to engine names with calling conventions and sources; the duplicates of FUN_M2SCENE_ANIMATE, FUN_M2MODEL_SET_ANIMATED and GetCameraById are removed, as are the made-up draw-flag and scene-count fields and the raw device-vtable slots. Model::Camera::CameraById is exposed instead of being copied. --------- Co-authored-by: z1xel <ramirezthiago@71gmail.com> Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
This commit is contained in:
@@ -131,7 +131,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` |
|
||||
|
||||
+31
@@ -1033,6 +1033,7 @@ build instructions.
|
||||
- [`C_Texture.GetAtlasElements()`](#c_texturegetatlaselements)
|
||||
- [`C_Texture.RegisterAtlas(name, file, width, height, left, right, top, bottom [, tilesHorizontally, tilesVertically])`](#c_textureregisteratlasname-file-width-height-left-right-top-bottom--tileshorizontally-tilesvertically)
|
||||
- [Atlas markup](#atlas-markup)
|
||||
- [`SetPortraitTextureFromCreatureDisplayID(textureObject, creatureDisplayID)`](#setportraittexturefromcreaturedisplayidtextureobject-creaturedisplayid)
|
||||
|
||||
- [Time](#time)
|
||||
- [`GetServerTime()`](#getservertime)
|
||||
@@ -23898,6 +23899,36 @@ in text.
|
||||
Text typed by other players never draws atlas art. This matches how a typed
|
||||
texture path behaves.
|
||||
|
||||
### `SetPortraitTextureFromCreatureDisplayID(textureObject, creatureDisplayID)`
|
||||
|
||||
Shows the round portrait of a creature on a texture, by its display ID (a
|
||||
`CreatureDisplayInfo` ID). A unit is not necessary. The function returns
|
||||
nothing.
|
||||
|
||||
The texture changes at once. The portrait is transparent until the creature
|
||||
model loads, and then it shows, a short time after the call.
|
||||
|
||||
A character-based display gets the same appearance as a spawned NPC of that
|
||||
display. Like the portrait of a unit, the portrait does not show helms,
|
||||
shoulders, or weapons.
|
||||
|
||||
A display ID that is not in `CreatureDisplayInfo` does not change the
|
||||
texture. Some models have no portrait view. For these models, the portrait
|
||||
stays transparent.
|
||||
|
||||
Each display ID is drawn one time. Later calls for the same display ID show
|
||||
the finished portrait at once. The portraits stay until you log out.
|
||||
|
||||
```lua
|
||||
local frame = CreateFrame("Frame", nil, UIParent)
|
||||
frame:SetWidth(64)
|
||||
frame:SetHeight(64)
|
||||
frame:SetPoint("CENTER", 0, 0)
|
||||
local portrait = frame:CreateTexture(nil, "ARTWORK")
|
||||
portrait:SetAllPoints(frame)
|
||||
SetPortraitTextureFromCreatureDisplayID(portrait, 3254)
|
||||
```
|
||||
|
||||
## Time
|
||||
|
||||
### `GetServerTime()`
|
||||
|
||||
@@ -12635,6 +12635,83 @@ enum Offsets {
|
||||
// the per-stage applies clamp against (FUN_005a29d0/FUN_005a2b40).
|
||||
VAR_GX_DEVICE = 0x00C0ED38,
|
||||
OFF_GXDEV_STAGE_CAP = 0x23C,
|
||||
// ---- Unit portrait bake (BakePortrait 0x00524F60, benilla ui/portraits)
|
||||
// Every entry below is a call the bake makes, read off its disassembly;
|
||||
// calling convention from the call site. `Model::Portrait` replays the
|
||||
// bake on a display model of its own.
|
||||
// M2CreateScene — `void *()`: a fresh scene on the model cache.
|
||||
FUN_M2_CREATE_SCENE = 0x00706E10,
|
||||
// `__thiscall(scene)`: drops one scene reference; at zero the destructor
|
||||
// releases every instance on the scene's model list.
|
||||
FUN_M2SCENE_RELEASE = 0x00707320,
|
||||
// CreateModelFromPath — `void *__thiscall(scene, const char *path,
|
||||
// uint32 flags)`, `ret 8`: a new instance of the file's shared model
|
||||
// (Spells\ErrorCube.mdx when the file is missing). AttachDisplayModel
|
||||
// passes the CreatureModelData path and 0. The caller owns one reference.
|
||||
FUN_M2SCENE_CREATE_MODEL = 0x00707350,
|
||||
// CreateModelFromModel — `void *__thiscall(scene, source, uint32 flags)`:
|
||||
// a new instance of the source's shared model, with its attachments.
|
||||
FUN_M2SCENE_COPY_MODEL = 0x00707400,
|
||||
// M2SceneDraw — `__thiscall(scene, pass)`; the bake draws 0 then 1.
|
||||
FUN_M2SCENE_DRAW = 0x00708900,
|
||||
// `__fastcall(model)`: drops one instance reference (HandleClose2).
|
||||
FUN_MODEL_INSTANCE_RELEASE = 0x007103A0,
|
||||
// CM2Model::IsReady — `__thiscall(model, wait, recurseChildren)`:
|
||||
// loaded AND every texture has its device texture. `wait` blocks on the
|
||||
// reads and creates the device textures (GetTextureDeviceObject(.., 1));
|
||||
// the bake's gate passes (0, 1), the bake itself (1, 1).
|
||||
FUN_MODEL_INSTANCE_READY = 0x00710450,
|
||||
// CM2Model::SetVisible — `__thiscall(model, bool)`.
|
||||
FUN_M2MODEL_SET_VISIBLE = 0x00710C50,
|
||||
// CM2Model::PlayAnimation — `__thiscall(model, 7 args)`, `ret 0x1C`. The
|
||||
// bake passes (-1, 0, 0, 0, 1.0f, 0, 1).
|
||||
FUN_M2MODEL_PLAY_ANIMATION = 0x007121A0,
|
||||
// ApplyCreatureDisplay — `__fastcall(model, displayRec, modelDataRec)`,
|
||||
// `ret 4`: the CreatureDisplayInfo row's three skins as the model's
|
||||
// replaceable textures 11..13 (AttachDisplayModel 0x00505AE6).
|
||||
FUN_APPLY_CREATURE_DISPLAY = 0x004797B0,
|
||||
// DetachAllItemModels — `__fastcall(model)`: the bake strips the copy's
|
||||
// attachments (helm, shoulders, held items) before it draws.
|
||||
FUN_DETACH_ALL_ITEM_MODELS = 0x0047A230,
|
||||
// CameraSetDeviceTransforms — `__fastcall(camera, const float rect[4],
|
||||
// int)`: sets the device projection and view from the camera over the
|
||||
// rect; the bake passes {0, 0, 1, aspect} and 0.
|
||||
FUN_CAMERA_SET_DEVICE_TRANSFORMS = 0x007ADA40,
|
||||
// PortraitLighting — the bake's lighting callback (ambient 0.45 grey and a
|
||||
// white light along (-1, 0, -1)), set with FUN_MODEL_INSTANCE_SET_RENDER_CB
|
||||
// and a null argument.
|
||||
FUN_PORTRAIT_LIGHTING = 0x00525B10,
|
||||
// GetPortraitAlphaMask — `void *__fastcall(uint32 size)`: an array whose
|
||||
// byte data (pointer at +8) is the round mask's alpha, one per texel.
|
||||
FUN_PORTRAIT_ALPHA_MASK = 0x005258A0,
|
||||
// GxCapsScreenSize — `__fastcall(float rect[4])`, {top, left, bottom,
|
||||
// right}; see FUN_GX_CAPS_WINDOW_SIZE.
|
||||
FUN_GX_CAPS_SCREEN_SIZE = 0x0058A250,
|
||||
// The bake's device gate: `int ()` each. WINDOW_ACTIVE reads the device's
|
||||
// active flag (+0xF30, cleared while minimised), DEVICE_ALIVE reads
|
||||
// OFF_GXDEVD3D_ALIVE (+0xF2C).
|
||||
FUN_GX_WINDOW_ACTIVE = 0x0058A260,
|
||||
FUN_GX_DEVICE_ALIVE = 0x0058A280,
|
||||
// GxMasterEnable get / set — `bool __fastcall(cap)` / `__fastcall(cap,
|
||||
// value)`. The bake saves cap 1 (fog), turns it off, and restores it.
|
||||
FUN_GX_MASTER_ENABLE_GET = 0x0058A210,
|
||||
FUN_GX_MASTER_ENABLE_SET = 0x0058A1E0,
|
||||
GX_MASTER_FOG = 1,
|
||||
// SetClearColorDevice — `__stdcall(uint32 argb)`; the bake sets 0.
|
||||
FUN_GX_SET_CLEAR_COLOR = 0x0058A920,
|
||||
// ClearDevice — `__fastcall(uint32 mask)`; the bake clears 3 before the
|
||||
// draw and again after the readback.
|
||||
FUN_GX_CLEAR = 0x0058A970,
|
||||
// `__fastcall(const int rect[4], GrowableArray<uint32> *out)`: reads a
|
||||
// back-buffer rectangle (device slot +0x48); the bake reads {0,0,64,64}.
|
||||
FUN_GX_READ_PIXELS = 0x00589CF0,
|
||||
// GxTexMarkRectDirty — `__fastcall(CGxTex *, const int rect[4], uint32
|
||||
// immediate)`, `ret 4`: re-latches a callback texture's texels; the bake's
|
||||
// re-draw of a cached portrait uploads its new pixels with immediate 1.
|
||||
FUN_GX_TEX_MARK_RECT_DIRTY = 0x0058ACD0,
|
||||
// CSimpleTexture::SetImage — `__thiscall(texture, HTEXTURE)`, `ret 4`:
|
||||
// binds a texture handle (taking a reference) and notifies the layer.
|
||||
FUN_SIMPLETEXTURE_SET_IMAGE = 0x00770300,
|
||||
// The vertex-stream primitive every textured region draws through
|
||||
// (FUN_0076fb00 calls it per region). Decompiler signature (13 params):
|
||||
// (count, positions, posStride, s3, s3Stride, colors, colorStride, drop8,
|
||||
|
||||
+19
-14
@@ -37,6 +37,8 @@
|
||||
// both model cameras; vanilla's ConfigureCamera does not subtract it from
|
||||
// camera 1, so the portrait camera is used as is, in the same space.
|
||||
|
||||
#include "model/Camera.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
@@ -68,6 +70,23 @@ using UpdateModel_t = void(__thiscall *)(void *frame);
|
||||
using ModelLoaded_t = int(__thiscall *)(void *model, int tryLoad, int recurseChildren);
|
||||
using ConfigureCamera_t = void(__thiscall *)(void *pane);
|
||||
|
||||
} // namespace
|
||||
|
||||
// GetCameraById's lookup, bounded the way 4.3.4 null-checks its result.
|
||||
void *CameraById(void *model, uint32_t id) {
|
||||
const uint32_t count =
|
||||
reinterpret_cast<GetCameraCount_t>(Offsets::FUN_M2MODEL_GET_CAMERA_COUNT)(model);
|
||||
const uint8_t *data = Game::Read<const uint8_t *>(
|
||||
Game::Read<const uint8_t *>(model, Offsets::OFF_M2MODEL_SHARED), Offsets::OFF_M2SHARED_DATA);
|
||||
const uint16_t index = reinterpret_cast<LookupIndex_t>(Offsets::FUN_M2_LOOKUP_INDEX)(
|
||||
id, data + Offsets::OFF_M2DATA_CAMERA_ID_LOOKUP);
|
||||
if (index >= count)
|
||||
return nullptr;
|
||||
return reinterpret_cast<GetCamera_t>(Offsets::FUN_M2MODEL_GET_CAMERA)(model, index);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct Vec3 {
|
||||
float x, y, z;
|
||||
};
|
||||
@@ -90,20 +109,6 @@ void SetScalar(void *camera, uint32_t id, float value) {
|
||||
reinterpret_cast<SetScalar_t>(Offsets::FUN_CAMERA_SET_SCALAR)(camera, id, value);
|
||||
}
|
||||
|
||||
// The model's camera with id `id` (0 = portrait), or null when it has none —
|
||||
// GetCameraById's lookup, bounded the way 4.3.4 null-checks its result.
|
||||
void *CameraById(void *model, uint32_t id) {
|
||||
const uint32_t count =
|
||||
reinterpret_cast<GetCameraCount_t>(Offsets::FUN_M2MODEL_GET_CAMERA_COUNT)(model);
|
||||
const uint8_t *data = Game::Read<const uint8_t *>(
|
||||
Game::Read<const uint8_t *>(model, Offsets::OFF_M2MODEL_SHARED), Offsets::OFF_M2SHARED_DATA);
|
||||
const uint16_t index = reinterpret_cast<LookupIndex_t>(Offsets::FUN_M2_LOOKUP_INDEX)(
|
||||
id, data + Offsets::OFF_M2DATA_CAMERA_ID_LOOKUP);
|
||||
if (index >= count)
|
||||
return nullptr;
|
||||
return reinterpret_cast<GetCamera_t>(Offsets::FUN_M2MODEL_GET_CAMERA)(model, index);
|
||||
}
|
||||
|
||||
// ConfigureCamera's own evaluation pass: facing 0, rebuild the root
|
||||
// transform, animate the scene, restore the facing.
|
||||
void AnimateUnrotated(void *pane, void *model) {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Model::Camera {
|
||||
|
||||
// The M2 model's camera with id `id` (0 = portrait), or null when it has none.
|
||||
void *CameraById(void *model, uint32_t id);
|
||||
|
||||
} // namespace Model::Camera
|
||||
+79
-61
@@ -118,8 +118,7 @@ DressJob g_jobs[8];
|
||||
constexpr int kMaxDressTicks = 600;
|
||||
|
||||
void CancelJob(DressJob &job) {
|
||||
if (job.builder != nullptr)
|
||||
reinterpret_cast<CharCompDestroy_t>(Offsets::FUN_CHARCOMP_DESTROY)(job.builder);
|
||||
DestroyDresser(job.builder);
|
||||
job = DressJob{};
|
||||
}
|
||||
|
||||
@@ -153,14 +152,8 @@ DressJob *ClaimJob(void *frame) {
|
||||
return slot;
|
||||
}
|
||||
|
||||
// Queue a dress for a character display just loaded into `frame`. Mirrors the
|
||||
// engine's own early-out: an Extra record with no bake name gets no
|
||||
// customization at all (FUN_005fb200 returns before creating the builder).
|
||||
// Queue a dress for a character display just loaded into `frame`.
|
||||
void ScheduleDress(void *frame, const uint8_t *extraRec) {
|
||||
const char *bake = Game::Read<const char *>(
|
||||
extraRec, Offsets::OFF_CREATUREDISPLAYINFOEXTRA_BAKE_NAME);
|
||||
if (bake == nullptr || bake[0] == '\0')
|
||||
return;
|
||||
DressJob *job = ClaimJob(frame);
|
||||
job->frame = frame;
|
||||
job->instance = Game::Read<void *>(frame,
|
||||
@@ -184,49 +177,13 @@ void StepJob(DressJob &job) {
|
||||
if (!job.configured) {
|
||||
if (!reinterpret_cast<ModelLoaded_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(instance, 0, 0))
|
||||
return;
|
||||
|
||||
void *builder = reinterpret_cast<CharCompCreate_t>(Offsets::FUN_CHARCOMP_CREATE)();
|
||||
if (builder == nullptr) {
|
||||
job.builder = CreateDresser(instance, job.extraRec);
|
||||
if (job.builder == nullptr) {
|
||||
CancelJob(job);
|
||||
return;
|
||||
}
|
||||
job.builder = builder;
|
||||
|
||||
const uint8_t *extra = job.extraRec;
|
||||
auto field = [extra](int off) {
|
||||
return Game::Read<uint32_t>(extra, off);
|
||||
};
|
||||
CharCompInfo info = {};
|
||||
info.race = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_RACE);
|
||||
info.sex = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_SEX);
|
||||
info.hairColor = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_HAIR_COLOR);
|
||||
info.skin = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_SKIN);
|
||||
info.face = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_FACE);
|
||||
info.facialHair = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_FACIAL_HAIR);
|
||||
info.hairStyle = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_HAIR_STYLE);
|
||||
info.model = instance;
|
||||
info.useBake = 1;
|
||||
const char *bake = Game::Read<const char *>(
|
||||
extra, Offsets::OFF_CREATUREDISPLAYINFOEXTRA_BAKE_NAME);
|
||||
std::snprintf(info.bakePath, sizeof(info.bakePath), "%s%s",
|
||||
"Textures\\BakedNpcTextures\\", bake);
|
||||
std::memcpy(info.geosetDefaults, kGeosetGroupDefaults, sizeof(kGeosetGroupDefaults));
|
||||
|
||||
// The builder owns this ref (releases it on destroy / re-configure).
|
||||
reinterpret_cast<ModelAddRef_t>(Offsets::FUN_MODEL_INSTANCE_ADDREF)(instance);
|
||||
if (!reinterpret_cast<CharCompSetInfo_t>(Offsets::FUN_CHARCOMP_SET_INFO)(builder, &info)) {
|
||||
CancelJob(job); // destroy releases the model ref already copied in
|
||||
return;
|
||||
}
|
||||
auto setItem = reinterpret_cast<CharCompSetItem_t>(Offsets::FUN_CHARCOMP_SET_ITEM);
|
||||
for (int slot = 0; slot < 10; ++slot) {
|
||||
const auto displayID = static_cast<int>(
|
||||
field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_EQUIP + slot * 4));
|
||||
if (displayID != 0)
|
||||
setItem(builder, slot, displayID);
|
||||
}
|
||||
|
||||
// The helm/shoulder feeds above attach child model instances. The frame
|
||||
// The helm/shoulder feeds attach child model instances. The frame
|
||||
// only lights its OWN instance (pre-render callback registered by
|
||||
// SetModelInstance), so the children would render unlit — pure black.
|
||||
// Give each child the frame's own light/fog callback; it reads the
|
||||
@@ -245,7 +202,7 @@ void StepJob(DressJob &job) {
|
||||
job.configured = true;
|
||||
}
|
||||
|
||||
if (reinterpret_cast<CharCompPump_t>(Offsets::FUN_CHARCOMP_PUMP)(job.builder, nullptr))
|
||||
if (PumpDresser(job.builder))
|
||||
job.done = true; // builder kept — it owns the model ref for the dressed model
|
||||
}
|
||||
|
||||
@@ -274,18 +231,8 @@ void __fastcall AttachDisplayModel_h(void *pane, void * /*edx*/, const uint8_t *
|
||||
|
||||
if (record == nullptr)
|
||||
return;
|
||||
const uint8_t *displayRec = DBC::Record(
|
||||
Offsets::VAR_CREATUREDISPLAYINFO_RECORDS, Offsets::VAR_CREATUREDISPLAYINFO_COUNT,
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_CREATURE_DISPLAYID));
|
||||
if (displayRec == nullptr)
|
||||
return;
|
||||
const uint32_t extID =
|
||||
Game::Read<uint32_t>(displayRec, Offsets::OFF_CREATUREDISPLAYINFO_EXTENDED_ID);
|
||||
if (extID == 0)
|
||||
return;
|
||||
const uint8_t *extraRec = DBC::Record(Offsets::VAR_CREATUREDISPLAYINFOEXTRA_RECORDS,
|
||||
Offsets::VAR_CREATUREDISPLAYINFOEXTRA_COUNT, extID);
|
||||
if (extraRec != nullptr)
|
||||
if (const uint8_t *extraRec =
|
||||
DressRecord(Game::Read<uint32_t>(record, Offsets::OFF_CREATURE_DISPLAYID)))
|
||||
ScheduleDress(pane, extraRec);
|
||||
}
|
||||
|
||||
@@ -374,6 +321,77 @@ const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The engine's own early-out: an Extra record with no bake name gets no
|
||||
// customization at all (FUN_005fb200 returns before creating the builder).
|
||||
const uint8_t *DressRecord(uint32_t displayID) {
|
||||
const uint8_t *displayRec = DBC::Record(Offsets::VAR_CREATUREDISPLAYINFO_RECORDS,
|
||||
Offsets::VAR_CREATUREDISPLAYINFO_COUNT, displayID);
|
||||
if (displayRec == nullptr)
|
||||
return nullptr;
|
||||
const uint32_t extID =
|
||||
Game::Read<uint32_t>(displayRec, Offsets::OFF_CREATUREDISPLAYINFO_EXTENDED_ID);
|
||||
if (extID == 0)
|
||||
return nullptr;
|
||||
const uint8_t *extraRec = DBC::Record(Offsets::VAR_CREATUREDISPLAYINFOEXTRA_RECORDS,
|
||||
Offsets::VAR_CREATUREDISPLAYINFOEXTRA_COUNT, extID);
|
||||
if (extraRec == nullptr)
|
||||
return nullptr;
|
||||
const char *bake =
|
||||
Game::Read<const char *>(extraRec, Offsets::OFF_CREATUREDISPLAYINFOEXTRA_BAKE_NAME);
|
||||
return bake != nullptr && bake[0] != '\0' ? extraRec : nullptr;
|
||||
}
|
||||
|
||||
// Configure + feed equipment, the engine's order: SetInfo, then the 10 Extra
|
||||
// equipment slots. Field-for-field mirror of FUN_005fb200's NPC branch.
|
||||
void *CreateDresser(void *instance, const uint8_t *extraRec) {
|
||||
void *builder = reinterpret_cast<CharCompCreate_t>(Offsets::FUN_CHARCOMP_CREATE)();
|
||||
if (builder == nullptr)
|
||||
return nullptr;
|
||||
|
||||
auto field = [extraRec](int off) {
|
||||
return Game::Read<uint32_t>(extraRec, off);
|
||||
};
|
||||
CharCompInfo info = {};
|
||||
info.race = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_RACE);
|
||||
info.sex = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_SEX);
|
||||
info.hairColor = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_HAIR_COLOR);
|
||||
info.skin = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_SKIN);
|
||||
info.face = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_FACE);
|
||||
info.facialHair = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_FACIAL_HAIR);
|
||||
info.hairStyle = field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_HAIR_STYLE);
|
||||
info.model = instance;
|
||||
info.useBake = 1;
|
||||
const char *bake =
|
||||
Game::Read<const char *>(extraRec, Offsets::OFF_CREATUREDISPLAYINFOEXTRA_BAKE_NAME);
|
||||
std::snprintf(info.bakePath, sizeof(info.bakePath), "%s%s", "Textures\\BakedNpcTextures\\",
|
||||
bake);
|
||||
std::memcpy(info.geosetDefaults, kGeosetGroupDefaults, sizeof(kGeosetGroupDefaults));
|
||||
|
||||
// The builder owns this ref (releases it on destroy / re-configure).
|
||||
reinterpret_cast<ModelAddRef_t>(Offsets::FUN_MODEL_INSTANCE_ADDREF)(instance);
|
||||
if (!reinterpret_cast<CharCompSetInfo_t>(Offsets::FUN_CHARCOMP_SET_INFO)(builder, &info)) {
|
||||
DestroyDresser(builder); // releases the model ref already copied in
|
||||
return nullptr;
|
||||
}
|
||||
auto setItem = reinterpret_cast<CharCompSetItem_t>(Offsets::FUN_CHARCOMP_SET_ITEM);
|
||||
for (int slot = 0; slot < 10; ++slot) {
|
||||
const auto displayID =
|
||||
static_cast<int>(field(Offsets::OFF_CREATUREDISPLAYINFOEXTRA_EQUIP + slot * 4));
|
||||
if (displayID != 0)
|
||||
setItem(builder, slot, displayID);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
bool PumpDresser(void *dresser) {
|
||||
return reinterpret_cast<CharCompPump_t>(Offsets::FUN_CHARCOMP_PUMP)(dresser, nullptr) != 0;
|
||||
}
|
||||
|
||||
void DestroyDresser(void *dresser) {
|
||||
if (dresser != nullptr)
|
||||
reinterpret_cast<CharCompDestroy_t>(Offsets::FUN_CHARCOMP_DESTROY)(dresser);
|
||||
}
|
||||
|
||||
void PrepareForReload() {
|
||||
for (auto &job : g_jobs)
|
||||
CancelJob(job);
|
||||
|
||||
@@ -10,8 +10,23 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Model::DisplayInfo {
|
||||
|
||||
// The CreatureDisplayInfoExtra record the engine dresses `displayID` from, or
|
||||
// null when the display gets no character customization.
|
||||
const uint8_t *DressRecord(uint32_t displayID);
|
||||
|
||||
// The engine character compositor, configured to dress the LOADED model
|
||||
// `instance` from `extraRec` (a DressRecord) and fed its equipment, or null
|
||||
// when the engine pool is empty or rejects the info. It holds a reference on
|
||||
// the model. Pump it with PumpDresser until that returns true; DestroyDresser
|
||||
// releases it (null is a no-op).
|
||||
void *CreateDresser(void *instance, const uint8_t *extraRec);
|
||||
bool PumpDresser(void *dresser);
|
||||
void DestroyDresser(void *dresser);
|
||||
|
||||
// The reload teardown destroys every Model frame (and with it the model
|
||||
// instances our pending character-dress jobs point at). Drop all jobs and
|
||||
// destroy their engine compositors before the engine starts tearing down.
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
// 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/>.
|
||||
|
||||
// `SetPortraitTextureFromCreatureDisplayID(texture, displayID)` — a creature
|
||||
// portrait from a display ID alone, with no unit.
|
||||
//
|
||||
// Vanilla bakes unit portraits in BakePortrait (0x00524F60, benilla
|
||||
// ui/portraits): it copies the unit's live, dressed model into a fresh scene,
|
||||
// draws it with the model's portrait camera into the 64x64 top-left corner of
|
||||
// the back buffer, reads that back, cuts the round alpha mask in, and keeps
|
||||
// the texture by display ID. A display ID has no live model, so this builds
|
||||
// one the way AttachDisplayModel does for a PlayerModel — CreateModelFromPath
|
||||
// on the CreatureModelData path, then ApplyCreatureDisplay — in a scene of its
|
||||
// own, dresses it through the engine compositor the way a unit is dressed
|
||||
// (Model::DisplayInfo), and runs the bake's own sequence on it, call for call.
|
||||
//
|
||||
// The texture is bound at once and its pixels arrive later. Each display ID
|
||||
// owns one callback texture, transparent until baked; the bake writes the
|
||||
// pixels and re-latches them with GxTexMarkRectDirty, the engine's own path
|
||||
// for re-baking a cached portrait in place. A region the caller points
|
||||
// somewhere else before the bake finishes keeps what it was given, so no
|
||||
// region is tracked.
|
||||
//
|
||||
// The display model loads asynchronously. The engine bakes from Lua, before
|
||||
// anything draws over the corner it borrows; pending displays are stepped at
|
||||
// the head of the UI render callback, the same point in the frame as an
|
||||
// OnUpdate script that calls SetPortraitTexture. The bake saves and restores
|
||||
// exactly what BakePortrait saves (projection, view, viewport, fog).
|
||||
//
|
||||
// The bake's ready gate is IsReady(model, 0, 1), which only a DRAWN model
|
||||
// passes: a model's device textures are made the first time something asks
|
||||
// for them with wait set (GetTextureDeviceObject), and nothing draws this one.
|
||||
// So the gate here is the load (IsLoaded) plus the dress, and the bake's own
|
||||
// IsReady(copy, 1, 1) makes the device textures, as it does for the copy of a
|
||||
// unit's model.
|
||||
//
|
||||
// Portraits are kept until the client leaves the world, the lifetime of the
|
||||
// engine's display-ID portrait cache (ClearPortraitCaches, from ShutdownGame).
|
||||
// A model with no portrait camera stays transparent; the engine binds an empty
|
||||
// texture for one.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "model/Camera.h"
|
||||
#include "model/DisplayInfo.h"
|
||||
#include "texture/Load.h"
|
||||
#include "tick/FrameTick.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Model::Portrait {
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kSize = 64;
|
||||
constexpr uint32_t kTexels = kSize * kSize;
|
||||
|
||||
using CreateScene_t = void *(__cdecl *)();
|
||||
using ReleaseScene_t = void(__thiscall *)(void *scene);
|
||||
using CreateModel_t = void *(__thiscall *)(void *scene, const char *path, uint32_t flags);
|
||||
using CopyModel_t = void *(__thiscall *)(void *scene, void *source, uint32_t flags);
|
||||
using ApplyDisplay_t = void(__fastcall *)(void *model, const uint8_t *display,
|
||||
const uint8_t *modelData);
|
||||
using ReleaseModel_t = void(__fastcall *)(void *model);
|
||||
using ModelState_t = int(__thiscall *)(void *model, int wait, int recurseChildren);
|
||||
using SetFlag_t = void(__thiscall *)(void *model, int on);
|
||||
using PlayAnimation_t = void(__thiscall *)(void *model, int, int, int, int, float, int, int);
|
||||
using SetRenderCb_t = void(__thiscall *)(void *model, const void *cb, void *userData);
|
||||
using DetachAll_t = void(__fastcall *)(void *model);
|
||||
using AnimateScene_t = void(__thiscall *)(void *scene, const float *cameraPos);
|
||||
using DrawScene_t = void(__thiscall *)(void *scene, uint32_t pass);
|
||||
using GetVector_t = void(__fastcall *)(void *camera, uint32_t id, float *out);
|
||||
using SetTransforms_t = void(__fastcall *)(void *camera, const float *rect, uint32_t unused);
|
||||
using GetMatrix_t = void(__fastcall *)(float *m);
|
||||
using SetMatrix_t = void(__fastcall *)(const float *m);
|
||||
using GetViewport_t = void(__fastcall *)(float *, float *, float *, float *, float *, float *);
|
||||
using SetViewport_t = void(__stdcall *)(float, float, float, float, float, float);
|
||||
using ScreenSize_t = void(__fastcall *)(float *rect);
|
||||
using DdcToNdc_t = void(__fastcall *)(float *x, float *y, float inX, float inY);
|
||||
using DeviceGate_t = int(__cdecl *)();
|
||||
using MasterGet_t = bool(__fastcall *)(uint32_t cap);
|
||||
using MasterSet_t = void(__fastcall *)(uint32_t cap, uint32_t on);
|
||||
using SetClearColor_t = void(__stdcall *)(uint32_t argb);
|
||||
using Clear_t = void(__fastcall *)(uint32_t mask);
|
||||
using AlphaMask_t = const uint8_t *(__fastcall *)(uint32_t size);
|
||||
using GetDeviceTexture_t = void *(__fastcall *)(void *texture, uint32_t wait, void *callback);
|
||||
using MarkDirty_t = void(__fastcall *)(void *gxTex, const int32_t *rect, uint32_t immediate);
|
||||
using SetImage_t = void(__thiscall *)(void *region, void *texture);
|
||||
|
||||
// The engine's GrowableArray<uint32>: capacity, count, data, growth step.
|
||||
// FUN_GX_READ_PIXELS sets the count to the rectangle's texels, which fit the
|
||||
// capacity given, so it writes them into `data` in place.
|
||||
struct TexelArray {
|
||||
uint32_t capacity;
|
||||
uint32_t count;
|
||||
uint32_t *data;
|
||||
uint32_t increment;
|
||||
};
|
||||
// GetPortraitAlphaMask's result: one alpha byte per texel, from +8.
|
||||
struct AlphaMask {
|
||||
uint32_t reserved[2];
|
||||
const uint8_t *alpha;
|
||||
};
|
||||
|
||||
struct Portrait {
|
||||
std::array<uint32_t, kTexels> texels{}; // the texture's texel source, kept as long
|
||||
void *texture = nullptr; // HTEXTURE, one reference
|
||||
// The display model while it loads and dresses.
|
||||
void *scene = nullptr;
|
||||
void *model = nullptr;
|
||||
const uint8_t *dressRecord = nullptr;
|
||||
void *dresser = nullptr;
|
||||
};
|
||||
std::unordered_map<uint32_t, std::unique_ptr<Portrait>> g_portraits;
|
||||
std::vector<Portrait *> g_loading;
|
||||
|
||||
void ReleaseModel(Portrait &p) {
|
||||
Model::DisplayInfo::DestroyDresser(p.dresser);
|
||||
reinterpret_cast<ReleaseModel_t>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(p.model);
|
||||
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(p.scene);
|
||||
p.dresser = p.model = p.scene = nullptr;
|
||||
p.dressRecord = nullptr;
|
||||
}
|
||||
|
||||
// BakePortrait from the copy on, against `p.model`. False when it could not
|
||||
// run this frame (no scene / copy), so the next frame tries again.
|
||||
bool Bake(Portrait &p) {
|
||||
void *source = p.model;
|
||||
if (Model::Camera::CameraById(source, 0) == nullptr)
|
||||
return true;
|
||||
|
||||
// The 64x64 pixel box at the top-left, in DDC, in the bake's own order
|
||||
// and precision: x87 operations, a float store after each (0x005251B7).
|
||||
float screen[4] = {};
|
||||
reinterpret_cast<ScreenSize_t>(Offsets::FUN_GX_CAPS_SCREEN_SIZE)(screen);
|
||||
const float width = static_cast<float>(static_cast<double>(screen[3]) - screen[1]);
|
||||
const float height = static_cast<float>(static_cast<double>(screen[2]) - screen[0]);
|
||||
const double scaleX = Game::Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE);
|
||||
const double scaleY = Game::Read<float>(Offsets::VAR_COORD_DDC_HEIGHT_SCALE);
|
||||
float x0 = 0.0f;
|
||||
float y1 = static_cast<float>(scaleY);
|
||||
float x1 = static_cast<float>(kSize / static_cast<double>(width) * scaleX);
|
||||
float y0 = static_cast<float>(scaleY - scaleY / height * kSize);
|
||||
|
||||
void *scene = reinterpret_cast<CreateScene_t>(Offsets::FUN_M2_CREATE_SCENE)();
|
||||
if (scene == nullptr)
|
||||
return false;
|
||||
void *copy = reinterpret_cast<CopyModel_t>(Offsets::FUN_M2SCENE_COPY_MODEL)(scene, source, 0);
|
||||
if (copy == nullptr) {
|
||||
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(scene);
|
||||
return false;
|
||||
}
|
||||
reinterpret_cast<DetachAll_t>(Offsets::FUN_DETACH_ALL_ITEM_MODELS)(copy);
|
||||
void *camera = Model::Camera::CameraById(copy, 0);
|
||||
reinterpret_cast<PlayAnimation_t>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(copy, -1, 0, 0, 0,
|
||||
1.0f, 0, 1);
|
||||
|
||||
float projection[16];
|
||||
float view[16];
|
||||
float viewport[6];
|
||||
reinterpret_cast<GetMatrix_t>(Offsets::FUN_GX_GET_PROJECTION)(projection);
|
||||
reinterpret_cast<GetMatrix_t>(Offsets::FUN_GX_GET_VIEW)(view);
|
||||
reinterpret_cast<GetViewport_t>(Offsets::FUN_GX_GET_VIEWPORT)(
|
||||
&viewport[0], &viewport[1], &viewport[2], &viewport[3], &viewport[4], &viewport[5]);
|
||||
const bool fog = reinterpret_cast<MasterGet_t>(Offsets::FUN_GX_MASTER_ENABLE_GET)(
|
||||
Offsets::GX_MASTER_FOG);
|
||||
auto setMaster = reinterpret_cast<MasterSet_t>(Offsets::FUN_GX_MASTER_ENABLE_SET);
|
||||
setMaster(Offsets::GX_MASTER_FOG, 0);
|
||||
|
||||
const float aspect = static_cast<float>((static_cast<double>(x1) - x0) /
|
||||
(static_cast<double>(y1) - y0));
|
||||
auto toNdc = reinterpret_cast<DdcToNdc_t>(Offsets::FUN_COORD_DDC_TO_NDC);
|
||||
toNdc(&x0, &y0, x0, y0);
|
||||
toNdc(&x1, &y1, x1, y1);
|
||||
|
||||
// First collection: evaluate the portrait camera.
|
||||
auto isLoaded = reinterpret_cast<ModelState_t>(Offsets::FUN_MODEL_INSTANCE_LOADED);
|
||||
auto isReady = reinterpret_cast<ModelState_t>(Offsets::FUN_MODEL_INSTANCE_READY);
|
||||
auto setAnimated = reinterpret_cast<SetFlag_t>(Offsets::FUN_M2MODEL_SET_ANIMATED);
|
||||
auto animate = reinterpret_cast<AnimateScene_t>(Offsets::FUN_M2SCENE_ANIMATE);
|
||||
auto getVector = reinterpret_cast<GetVector_t>(Offsets::FUN_CAMERA_GET_VECTOR);
|
||||
isLoaded(copy, 1, 1);
|
||||
setAnimated(copy, 1);
|
||||
const float origin[3] = {0.0f, 0.0f, 0.0f};
|
||||
animate(scene, origin);
|
||||
float eye[3] = {};
|
||||
float target[3] = {};
|
||||
getVector(camera, Offsets::CAMERA_PROP_EYE, eye);
|
||||
getVector(camera, Offsets::CAMERA_PROP_TARGET, target);
|
||||
const float rect[4] = {0.0f, 0.0f, 1.0f, aspect};
|
||||
reinterpret_cast<SetTransforms_t>(Offsets::FUN_CAMERA_SET_DEVICE_TRANSFORMS)(camera, rect, 0);
|
||||
reinterpret_cast<SetViewport_t>(Offsets::FUN_GX_SET_VIEWPORT)(x0, x1, y0, y1, 0.0f, 1.0f);
|
||||
reinterpret_cast<SetClearColor_t>(Offsets::FUN_GX_SET_CLEAR_COLOR)(0);
|
||||
auto clear = reinterpret_cast<Clear_t>(Offsets::FUN_GX_CLEAR);
|
||||
clear(3);
|
||||
|
||||
// Second collection from the portrait eye, then both passes.
|
||||
isReady(copy, 1, 1);
|
||||
setAnimated(copy, 1);
|
||||
reinterpret_cast<SetFlag_t>(Offsets::FUN_M2MODEL_SET_VISIBLE)(copy, 1);
|
||||
reinterpret_cast<SetRenderCb_t>(Offsets::FUN_MODEL_INSTANCE_SET_RENDER_CB)(
|
||||
copy, reinterpret_cast<const void *>(Offsets::FUN_PORTRAIT_LIGHTING), nullptr);
|
||||
animate(scene, eye);
|
||||
auto draw = reinterpret_cast<DrawScene_t>(Offsets::FUN_M2SCENE_DRAW);
|
||||
draw(scene, 0);
|
||||
draw(scene, 1);
|
||||
|
||||
const int32_t box[4] = {0, 0, static_cast<int32_t>(kSize), static_cast<int32_t>(kSize)};
|
||||
TexelArray out{kTexels, kTexels, p.texels.data(), kTexels};
|
||||
reinterpret_cast<void(__fastcall *)(const int32_t *, TexelArray *)>(
|
||||
Offsets::FUN_GX_READ_PIXELS)(box, &out);
|
||||
clear(3);
|
||||
const auto *mask = reinterpret_cast<const AlphaMask *>(
|
||||
reinterpret_cast<AlphaMask_t>(Offsets::FUN_PORTRAIT_ALPHA_MASK)(kSize));
|
||||
if (out.data == p.texels.data()) {
|
||||
for (uint32_t i = 0; i < kTexels; ++i)
|
||||
p.texels[i] = (p.texels[i] & 0x00FFFFFFu) | static_cast<uint32_t>(mask->alpha[i]) << 24;
|
||||
void *gxTex = reinterpret_cast<GetDeviceTexture_t>(Offsets::FUN_TEXTURE_GET_RENDERABLE)(
|
||||
p.texture, 1, nullptr);
|
||||
reinterpret_cast<MarkDirty_t>(Offsets::FUN_GX_TEX_MARK_RECT_DIRTY)(gxTex, box, 1);
|
||||
}
|
||||
|
||||
reinterpret_cast<SetMatrix_t>(Offsets::FUN_GX_SET_PROJECTION)(projection);
|
||||
reinterpret_cast<SetMatrix_t>(Offsets::FUN_GX_SET_VIEW)(view);
|
||||
reinterpret_cast<SetViewport_t>(Offsets::FUN_GX_SET_VIEWPORT)(
|
||||
viewport[0], viewport[1], viewport[2], viewport[3], viewport[4], viewport[5]);
|
||||
setMaster(Offsets::GX_MASTER_FOG, fog ? 1 : 0);
|
||||
reinterpret_cast<ReleaseModel_t>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(copy);
|
||||
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(scene);
|
||||
return true;
|
||||
}
|
||||
|
||||
// One frame for one pending display; true once it is baked (or cannot be).
|
||||
bool Step(Portrait &p) {
|
||||
if (!reinterpret_cast<ModelState_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(p.model, 0, 1))
|
||||
return false;
|
||||
if (p.dressRecord != nullptr) {
|
||||
// A dress the engine rejects bakes undressed, as a unit with no
|
||||
// compositor does.
|
||||
if (p.dresser == nullptr)
|
||||
p.dresser = Model::DisplayInfo::CreateDresser(p.model, p.dressRecord);
|
||||
if (p.dresser == nullptr)
|
||||
p.dressRecord = nullptr;
|
||||
else if (!Model::DisplayInfo::PumpDresser(p.dresser))
|
||||
return false;
|
||||
}
|
||||
if (!reinterpret_cast<DeviceGate_t>(Offsets::FUN_GX_WINDOW_ACTIVE)() ||
|
||||
!reinterpret_cast<DeviceGate_t>(Offsets::FUN_GX_DEVICE_ALIVE)())
|
||||
return false;
|
||||
return Bake(p);
|
||||
}
|
||||
|
||||
void Pump() {
|
||||
for (size_t i = 0; i < g_loading.size();) {
|
||||
Portrait &p = *g_loading[i];
|
||||
if (!Step(p)) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
ReleaseModel(p);
|
||||
g_loading[i] = g_loading.back();
|
||||
g_loading.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::FrameTick::AutoSubscribeBefore _tick{&Pump};
|
||||
|
||||
// The display's portrait, started on first use. Null for a display ID with no
|
||||
// CreatureDisplayInfo / CreatureModelData row (AttachDisplayModel's gate).
|
||||
Portrait *Acquire(uint32_t displayID) {
|
||||
auto found = g_portraits.find(displayID);
|
||||
if (found != g_portraits.end())
|
||||
return found->second.get();
|
||||
const uint8_t *display = DBC::Record(Offsets::VAR_CREATUREDISPLAYINFO_RECORDS,
|
||||
Offsets::VAR_CREATUREDISPLAYINFO_COUNT, displayID);
|
||||
if (display == nullptr)
|
||||
return nullptr;
|
||||
const uint8_t *modelData = DBC::Record(
|
||||
Offsets::VAR_CREATUREMODELDATA_RECORDS, Offsets::VAR_CREATUREMODELDATA_COUNT,
|
||||
Game::Read<uint32_t>(display, Offsets::OFF_CREATUREDISPLAYINFO_MODEL_ID));
|
||||
if (modelData == nullptr)
|
||||
return nullptr;
|
||||
|
||||
void *scene = reinterpret_cast<CreateScene_t>(Offsets::FUN_M2_CREATE_SCENE)();
|
||||
if (scene == nullptr)
|
||||
return nullptr;
|
||||
void *model = reinterpret_cast<CreateModel_t>(Offsets::FUN_M2SCENE_CREATE_MODEL)(
|
||||
scene, Game::Read<const char *>(modelData, Offsets::OFF_CREATUREMODELDATA_MODEL_PATH), 0);
|
||||
if (model == nullptr) {
|
||||
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(scene);
|
||||
return nullptr;
|
||||
}
|
||||
reinterpret_cast<ApplyDisplay_t>(Offsets::FUN_APPLY_CREATURE_DISPLAY)(model, display,
|
||||
modelData);
|
||||
|
||||
// The bake's own texture: 64x64, one mip, ARGB8888, flags (1, 0, 0, 0, 0,
|
||||
// 0, 1, 0), "Portrait1" (0x005257D3).
|
||||
auto p = std::make_unique<Portrait>();
|
||||
p->scene = scene;
|
||||
p->model = model;
|
||||
p->dressRecord = Model::DisplayInfo::DressRecord(displayID);
|
||||
p->texture = Texture::CreateFromPixels("Portrait1", kSize, kSize, p->texels.data(),
|
||||
Texture::Flags(1, false, false));
|
||||
if (p->texture == nullptr) {
|
||||
ReleaseModel(*p);
|
||||
return nullptr;
|
||||
}
|
||||
g_loading.push_back(p.get());
|
||||
return g_portraits.emplace(displayID, std::move(p)).first->second.get();
|
||||
}
|
||||
|
||||
int __fastcall Script_SetPortraitTextureFromCreatureDisplayID(void *L) {
|
||||
void *texture = Game::Lua::ResolveTexture(L, 1);
|
||||
if (texture == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: SetPortraitTextureFromCreatureDisplayID(texture, displayID)");
|
||||
return 0;
|
||||
}
|
||||
if (Portrait *p = Acquire(static_cast<uint32_t>(Game::Lua::ToNumber(L, 2))))
|
||||
reinterpret_cast<SetImage_t>(Offsets::FUN_SIMPLETEXTURE_SET_IMAGE)(texture, p->texture);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("textureObject", "SimpleTexture"),
|
||||
Game::Doc::Req("creatureDisplayID", "number", "A CreatureDisplayInfo ID."),
|
||||
};
|
||||
const Game::Doc::Function kDoc{
|
||||
"Shows a creature's portrait in a texture, by its display ID.", kArgs, {}, "UnitGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("SetPortraitTextureFromCreatureDisplayID",
|
||||
&Script_SetPortraitTextureFromCreatureDisplayID, &kDoc);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _register{&RegisterLuaFunctions};
|
||||
|
||||
// Leaving the world: drop every portrait, as ShutdownGame drops the engine's.
|
||||
// The world UI and every region holding one of these textures are gone by
|
||||
// the time the glue state boots.
|
||||
void ClearOnLogout() {
|
||||
for (Portrait *p : g_loading)
|
||||
ReleaseModel(*p);
|
||||
g_loading.clear();
|
||||
for (auto &entry : g_portraits)
|
||||
Texture::ReleaseHandle(entry.second->texture);
|
||||
g_portraits.clear();
|
||||
}
|
||||
|
||||
const Game::GlueModuleAutoRegister _clearOnLogout{&ClearOnLogout};
|
||||
|
||||
} // namespace
|
||||
} // namespace Model::Portrait
|
||||
Reference in New Issue
Block a user