mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 12:27:35 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7103de564 | ||
|
|
c00eb24a8d | ||
|
|
b65c9e0319 | ||
|
|
de033896b1 | ||
|
|
dc8d4caf12 | ||
|
|
20069f3815 | ||
|
|
f2fa112f0a | ||
|
|
fca967c827 | ||
|
|
24404b5a23 | ||
|
|
a106d86969 | ||
|
|
fac3be3a0b | ||
|
|
b49c107778 | ||
|
|
53b7f79440 | ||
|
|
7b21453469 | ||
|
|
ebfd9d034d | ||
|
|
d825bcbdda | ||
|
|
6b36bb10e6 | ||
|
|
6628dcca19 |
@@ -221,7 +221,7 @@ open the game menu and click AddOn Options, then ClassicAPI.
|
||||
| [Combat log](docs/API.md#kept-entries) | `combatLogRetentionTime` | `300` | The number of seconds that the combat log keeps an entry. |
|
||||
| [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, talk to an NPC, use an object or an item, or open the flight map. |
|
||||
| | `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. |
|
||||
|
||||
+1035
-937
File diff suppressed because it is too large
Load Diff
+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();
|
||||
|
||||
+16
-11
@@ -252,6 +252,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.
|
||||
@@ -361,11 +367,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
|
||||
@@ -431,17 +437,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) {
|
||||
|
||||
+33
-16
@@ -147,6 +147,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 +184,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 +205,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 +246,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;
|
||||
@@ -479,6 +487,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
|
||||
@@ -523,10 +533,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
|
||||
@@ -557,14 +568,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
|
||||
@@ -777,6 +788,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
|
||||
|
||||
+79
-43
@@ -2894,23 +2894,14 @@ enum Offsets {
|
||||
UNIT_POWER_MIN_TYPE = 0,
|
||||
UNIT_POWER_MAX_TYPE = 4, // happiness; types 5/6 only valid post-WotLK
|
||||
|
||||
// Per-power-type display divisor. Raw descriptor values get
|
||||
// divided by this before they're surfaced through Lua. Vanilla
|
||||
// 1.12 stores rage as 0..1000 (internally) and divides by 10 to
|
||||
// show 0..100; happiness is stored at 1000x scale and divides
|
||||
// by 1000. Engine reads from `Script_UnitMana` at
|
||||
// `0x006E7130 + type * 4` (= `0x0086F978`). Same trick the
|
||||
// 3.3.5/4.x clients use; the table is per-client, the design is
|
||||
// stable.
|
||||
//
|
||||
// [0] = 1 MANA
|
||||
// [1] = 10 RAGE
|
||||
// [2] = 1 FOCUS
|
||||
// [3] = 1 ENERGY
|
||||
// [4] = 1000 HAPPINESS
|
||||
VAR_UNIT_POWER_DIVISOR_TABLE = 0x0086F978,
|
||||
// GetPowerDisplayDivisor — `int __fastcall(int powerType)`, the reader of
|
||||
// the table above (1 for a negative type). benilla-re spell symbols.
|
||||
// GetPowerDisplayDivisor — `uint32_t __fastcall(int powerType)`: the
|
||||
// divisor that turns a stored power amount (descriptor POWER/MAXPOWER,
|
||||
// Spell.dbc ManaCost, the cost helper's answer) into displayed units.
|
||||
// Returns `[0x0086F978 + 4·type]`, or 1 for a negative type (verified by
|
||||
// disassembly). Table {1, 10, 1, 1, 1000}: rage is stored in tenths
|
||||
// (Shield Slam's 20 rage is 200), happiness at 1000x. Same table as
|
||||
// 3.3.5's `s_powerDisplayMod`. Read it only through
|
||||
// `Unit::Power::DisplayDivisor`.
|
||||
FUN_POWER_DISPLAY_DIVISOR = 0x006E7130,
|
||||
OFF_UNIT_FIELD_LEVEL = 0x70,
|
||||
// UNIT_FIELD_BYTES_0 dword at +0x78; byte 3 (= +0x7B) is the unit's
|
||||
@@ -3259,13 +3250,6 @@ enum Offsets {
|
||||
// 4.3.4 `GetSpellInfo` reports this instead of the cost (with
|
||||
// isFunnel = true) when it is positive.
|
||||
FUN_GET_SPELL_POWER_PER_SECOND = 0x006E3300,
|
||||
// Divisor that turns a stored power amount into its displayed units:
|
||||
// `__fastcall uint32_t(int powerType)` → `[0x0086F978 + 4·type]`, 1 for a
|
||||
// negative type. Table {1, 10, 1, 1, 1000} — rage is stored in tenths
|
||||
// (Heroic Strike's 15 rage is 150 in Spell.dbc). Verified by disassembly;
|
||||
// same table as 3.3.5's `s_powerDisplayMod`.
|
||||
FUN_GET_POWER_DISPLAY_DIVISOR = 0x006E7130,
|
||||
|
||||
// The engine's spell-castability verdict for the LOCAL player — the
|
||||
// function behind `IsUsableAction` for a player spell slot (the spell
|
||||
// branch of the per-slot recompute `FUN_004E5050`) and behind the
|
||||
@@ -3361,6 +3345,19 @@ enum Offsets {
|
||||
CALL_CAST_CHECK_TARGET_RANGE = 0x006E4F26,
|
||||
FUN_SPELL_TARGET_ARM = 0x006E5250,
|
||||
CALL_CAST_TARGET_ARM = 0x006E5038,
|
||||
// SendCast — `void __fastcall(void *pendingCast /*ecx*/, uint8_t flag
|
||||
// /*dl*/)`, plain ret: builds and sends CMSG_CAST_SPELL / CMSG_USE_ITEM /
|
||||
// CMSG_PET_CAST_SPELL (4.3.4's Spell_C_SendPendingCast). Its five callers
|
||||
// (xrefs) all pass the global pending cast (`mov ecx,0xCEAC48`): the
|
||||
// target arm's two immediate sends, and the object / location / trade-
|
||||
// slot binders the targeting cursor's click also runs. The arm's one
|
||||
// caller is the cast request, after the caster check.
|
||||
FUN_SPELL_SEND_CAST = 0x006E54F0,
|
||||
CALL_TARGET_ARM_SEND_CAST = 0x006E5349,
|
||||
CALL_TARGET_ARM_SEND_CAST_GUID = 0x006E5467,
|
||||
CALL_BIND_TARGET_OBJECT_SEND_CAST = 0x006E60D7,
|
||||
CALL_BIND_TARGET_LOCATION_SEND_CAST = 0x006E6177,
|
||||
CALL_BIND_TARGET_TRADE_SLOT_SEND_CAST = 0x006E694F,
|
||||
// GetSpellHostility — `int __thiscall(const uint8_t *record)`: 2 harmful,
|
||||
// 1 helpful, 0 neither, from the Targets word and the implicit targets
|
||||
// (benilla-re spell/handlers#hostility-classifier).
|
||||
@@ -3461,6 +3458,16 @@ enum Offsets {
|
||||
// 0x005D9139); the stock 1.12.1 exe reaches it.
|
||||
SITE_ITEM_USE_FORM_REFUSAL = 0x005D914D,
|
||||
ITEM_USE_FORM_PASSED = 0x005D9157,
|
||||
// Past the form test, the item use's toggle test (4.3.4 runs it before
|
||||
// the form cancel): a use spell with an activeIconID whose aura is on
|
||||
// the player is cancelled instead of cast, `mov ecx,[ebp-0xC]` (spell)
|
||||
// then CALL_ITEM_USE_TOGGLE_CANCEL → FUN_CANCEL_AURA_SEND, return 0.
|
||||
// Every other path joins at SITE_ITEM_USE_BIND_TEST, the bind-on-use
|
||||
// test: `mov ecx,[ebp-8]; cmp dword [ecx+0x194],3` (8B 4D F8 83 B9 94
|
||||
// 01 00 00 03), then `jne`. There EDI = the spell (read after), EBX and
|
||||
// EBP live; EAX, ECX, EDX are reloaded before use (disassembled).
|
||||
CALL_ITEM_USE_TOGGLE_CANCEL = 0x005D9237,
|
||||
SITE_ITEM_USE_BIND_TEST = 0x005D91D3,
|
||||
// The mount reads in 4.3.4's CGUnit_C::CanAttackNow, inlined in 1.12
|
||||
// (each followed by `test; jg refuse`):
|
||||
// the unit right-click's attack step (8B 82 FC 01 00 00), player ESI,
|
||||
@@ -4143,6 +4150,13 @@ enum Offsets {
|
||||
// Slot 36 of the Model and pane vtables; DressUpModel's own slot 36
|
||||
// (0x00504350) calls it first.
|
||||
FUN_SIMPLEMODEL_SET_MODEL_INSTANCE = 0x0076CD30,
|
||||
// Every Model frame's member teardown (fastcall frame), called by the
|
||||
// CSimpleModel deleting dtor 0x0076CA20 and CGCharacterModelBase's dtor
|
||||
// 0x00505870 (PlayerModel, DressUpModel). It releases the frame's M2
|
||||
// scene (+0x314), which frees the models in it even when something else
|
||||
// still holds a reference (seen live: a kept compositor's release faulted
|
||||
// on the freed model after /reload).
|
||||
FUN_SIMPLEMODEL_DESTROY = 0x0076CA50,
|
||||
// The Model frame's per-paint update — `__thiscall(frame)`, slot 38.
|
||||
FUN_SIMPLEMODEL_UPDATE_MODEL = 0x0076D1A0,
|
||||
|
||||
@@ -6161,15 +6175,37 @@ enum Offsets {
|
||||
// (21) set only the main-hand bit, off-hand-only (22) only the off-hand.
|
||||
VAR_INVTYPE_SLOT_MASKS = 0x00809200,
|
||||
|
||||
// The two `m_inventoryType` values that mean "consumed when used", so
|
||||
// both the item-consumable test (`C_Item.IsConsumableItem`) and the
|
||||
// action-slot one (`IsConsumableAction`) single them out. Indices into
|
||||
// the table above: 24 = `INVTYPE_AMMO`, 25 = `INVTYPE_THROWN`.
|
||||
// `m_inventoryType` (`OFF_ITEMSTATS_INVENTORY_TYPE`) values, named by
|
||||
// their key in the string table above (read from WoW.exe).
|
||||
INVTYPE_NON_EQUIP = 0,
|
||||
INVTYPE_HEAD = 1,
|
||||
INVTYPE_NECK = 2,
|
||||
INVTYPE_SHOULDER = 3,
|
||||
INVTYPE_BODY = 4,
|
||||
INVTYPE_CHEST = 5,
|
||||
INVTYPE_WAIST = 6,
|
||||
INVTYPE_LEGS = 7,
|
||||
INVTYPE_FEET = 8,
|
||||
INVTYPE_WRIST = 9,
|
||||
INVTYPE_HAND = 10,
|
||||
INVTYPE_FINGER = 11,
|
||||
INVTYPE_TRINKET = 12,
|
||||
INVTYPE_WEAPON = 13,
|
||||
INVTYPE_SHIELD = 14,
|
||||
INVTYPE_RANGED = 15,
|
||||
INVTYPE_CLOAK = 16,
|
||||
INVTYPE_2HWEAPON = 17,
|
||||
INVTYPE_BAG = 18,
|
||||
INVTYPE_TABARD = 19,
|
||||
INVTYPE_ROBE = 20,
|
||||
INVTYPE_WEAPONMAINHAND = 21,
|
||||
INVTYPE_WEAPONOFFHAND = 22,
|
||||
INVTYPE_HOLDABLE = 23,
|
||||
INVTYPE_AMMO = 24,
|
||||
INVTYPE_THROWN = 25,
|
||||
// Index 16 of the table above (read from WoW.exe). The bag search skips
|
||||
// a cloak's subclass name ("Cloth"), as 4.3.4's does.
|
||||
INVTYPE_CLOAK = 16,
|
||||
INVTYPE_RANGEDRIGHT = 26,
|
||||
INVTYPE_QUIVER = 27,
|
||||
INVTYPE_RELIC = 28,
|
||||
|
||||
// Faction "displayed list" — the engine maintains a sorted/visible list
|
||||
// of factions the player has rep with. `Script_GetNumFactions` (at
|
||||
@@ -8643,17 +8679,20 @@ enum Offsets {
|
||||
// unit-event observers. Matches nampower's `CGRaidInfo_m_raidtargets`.
|
||||
VAR_RAID_TARGET_GUIDS = 0x00B71368,
|
||||
|
||||
VAR_LOCALE_INDEX = 0x00C0E080, // 0..8, picks one of the 9 localized strings
|
||||
// 0..7: start-up stores the Wow.ini Language+Country tag's position in
|
||||
// VAR_LOCALE_NAME_TABLE (0x402562), 0 when it matches none (enGB).
|
||||
// Picks a DBC localized-string column. NOT 4.3.4's nine-slot locale
|
||||
// order — language rules go by name (Localization::Locale).
|
||||
VAR_LOCALE_INDEX = 0x00C0E080,
|
||||
|
||||
// Locale-name string table — a parallel `const char *[8]` indexed by
|
||||
// VAR_LOCALE_INDEX, holding the exact codes `GetLocale()` returns:
|
||||
// enUS / koKR / frFR / deDE / zhCN / zhTW / esES / xxYY. This is the
|
||||
// source `Script_GetLocale` (FUN_0048d8b0) reads:
|
||||
// enUS / koKR / frFR / deDE / zhCN / zhTW / esES / xxYY on a stock
|
||||
// client. This is the source `Script_GetLocale` (FUN_0048d8b0) reads:
|
||||
// lua_pushstring(L, (&PTR_DAT_008558a4)[DAT_00c0e080]);
|
||||
// Verified by resolving all 8 entries from the binary. Index range is
|
||||
// 0..7 (VAR_LOCALE_INDEX is documented 0..8, but slot 8 overreads into
|
||||
// the string pool — clamp to 0..7 before reading). Used by the TOC
|
||||
// `[TextLocale]` / `[AllowLoadTextLocale]` directives (AddOns::TocRewrite).
|
||||
// and the list SetAddOnMetadata (0x51D580) matches a TOC key's
|
||||
// `-xxXX` suffix against. Verified by resolving all 8 entries from the
|
||||
// binary. Read through Localization::Locale::Client().
|
||||
VAR_LOCALE_NAME_TABLE = 0x008558A4,
|
||||
|
||||
LUA_IS_NUMBER = 0x6F34D0,
|
||||
@@ -10928,10 +10967,6 @@ enum Offsets {
|
||||
INVSLOT_OFFHAND = 17,
|
||||
INVSLOT_MAINHAND = 16,
|
||||
|
||||
// INVTYPE (`OFF_ITEMSTATS_INVENTORY_TYPE`) of a two-handed weapon,
|
||||
// which occupies the main-hand slot and blocks the off-hand.
|
||||
INVTYPE_2HWEAPON = 17,
|
||||
|
||||
// Character-pane equipment slot range: 1..19 (head=1, neck=2, …,
|
||||
// tabard=19). Used by `C_Item.IsEquippedItem` to walk every slot
|
||||
// looking for a matching item.
|
||||
@@ -12036,7 +12071,8 @@ enum Offsets {
|
||||
// column 11: the parsed 1.12 DBC gives 0x7 for the warrior stances, 0x1
|
||||
// Stealth, 0x9 Shadowform, matching 3.3.5's column. 0x100 is the form
|
||||
// flag 3.3.5 / 4.3.4 `CGUnit_C::CanAutoUnshift` refuses on (set there
|
||||
// only on Spirit of Redemption and the Ghoul forms); no 1.12 row has it.
|
||||
// only on Spirit of Redemption and the Ghoul forms); no 1.12 row has it,
|
||||
// so Spell::AutoPrepare adds it to Spirit of Redemption (form 32).
|
||||
VAR_SPELL_SHAPESHIFT_FORM_RECORDS = 0x00C0D774,
|
||||
VAR_SPELL_SHAPESHIFT_FORM_COUNT = 0x00C0D778,
|
||||
OFF_SPELL_SHAPESHIFT_FORM_FLAGS = 0x2C,
|
||||
|
||||
+1
-3
@@ -170,9 +170,7 @@ static void RegisterLuaFunctions() {
|
||||
&Script_HasSpellActionButtons, &kHasSpellActionButtons);
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "IsOnBarOrSpecialBar",
|
||||
&Script_IsOnBarOrSpecialBar, &kIsOnBarOrSpecialBar);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ActionBarSet", kActionBarSet,
|
||||
static_cast<int>(sizeof(kActionBarSet) / sizeof(kActionBarSet[0])),
|
||||
"ActionBar");
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ActionBarSet", kActionBarSet, "ActionBar");
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+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};
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/TocRewrite.h"
|
||||
#include "addons/Toc.h"
|
||||
#include "localization/Locale.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "turtle/Detect.h"
|
||||
@@ -34,17 +35,7 @@ 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 +57,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 +149,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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
+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};
|
||||
|
||||
+2
-14
@@ -45,6 +45,7 @@
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Focus.h"
|
||||
#include "unit/Identity.h"
|
||||
#include "unit/Power.h"
|
||||
|
||||
#include <ctime>
|
||||
|
||||
@@ -617,17 +618,6 @@ uint32_t SpellSchoolMask(const uint8_t *spell) {
|
||||
: 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;
|
||||
}
|
||||
|
||||
Entry Begin(uint64_t source, uint64_t dest, int event, const uint8_t *spell) {
|
||||
Entry e;
|
||||
e.event = event;
|
||||
@@ -684,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -167,10 +167,6 @@ uint32_t SchoolMask(uint32_t school);
|
||||
// The spell's school as a mask (4.3.4's SpellRec m_schoolMask), 0 for nullptr.
|
||||
uint32_t SpellSchoolMask(const uint8_t *spell);
|
||||
|
||||
// Whether one of the spell's three effects applies the aura `auraName`
|
||||
// (EffectApplyAuraName).
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName);
|
||||
|
||||
// GetCombatLogEntry: an entry for `event` from `source` to `dest` (either may
|
||||
// be 0), with the spell prefix when `spell` is a record.
|
||||
Entry Begin(uint64_t source, uint64_t dest, int event, const uint8_t *spell);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
+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
|
||||
|
||||
@@ -17,7 +17,7 @@ CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell, use an objec
|
||||
CLASSICAPI_AUTO_STAND = "Auto Stand";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "Stand up when you cast a spell, loot, or open the flight map while you sit.";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "Auto Cancel Form";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it, talk to an NPC, use an object or an item, or open the flight map.";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it, use an item, or open the flight map. Also lets you talk to NPCs and use objects in druid forms and Ghost Wolf.";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "Bags and Items";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "Casting and Attacking";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "Combat Log Retention";
|
||||
|
||||
@@ -4,17 +4,109 @@ ACTION_BUTTON_USE_KEY_DOWN = "Способности при нажатии кл
|
||||
ACTION_TARGETING_OPTION = "Разрешить динамическое выделение";
|
||||
ADDON_MEM_KB_ABBR = "(%.0f КБ) %s";
|
||||
ADDON_MEM_MB_ABBR = "(%.2f МБ) %s";
|
||||
ADDON_OPTIONS = "Параметры модификаций";
|
||||
BINDING_NAME_AOELOOT = "Сбор добычи по области";
|
||||
BINDING_NAME_FOCUSTARGET = "Запомненная цель";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Взаимодействовать указателем мыши";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Взаимодействовать с объектом под курсором";
|
||||
BINDING_NAME_INTERACTTARGET = "Взаимодействовать с целью";
|
||||
BINDING_NAME_TARGETFOCUS = "Восстановить цель";
|
||||
CLASSICAPI_ACTION_BARS = "Панели команд";
|
||||
CLASSICAPI_ACTION_TARGETING = "Динамическое выделение";
|
||||
CLASSICAPI_ADVANCED = "Дополнительно";
|
||||
CLASSICAPI_ALL_SOFT_TARGET_ICONS = "Значки для всех мягких целей";
|
||||
CLASSICAPI_AUTO_DISMOUNT = "Автоматически спешиваться";
|
||||
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Спешиваться при применении заклинания, использовании объекта, сборе добычи, атаке или открытии карты полетов, если вы едете верхом.";
|
||||
CLASSICAPI_AUTO_STAND = "Автоматически вставать";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "Вставать при применении заклинания, сборе добычи или открытии карты полетов, если вы сидите.";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "Автоматически снимать облик";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Покидать свой облик при применении заклинания, которое в нем недоступно, использовании предмета, а также при открытии карты полетов. Также позволяет говорить с NPC и использовать объекты в обликах друида и призрачного волка.";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "Сумки и предметы";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "Заклинания и атаки";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "Хранение журнала боя";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION_TOOLTIP = "Сколько секунд журнал боя хранит записи для модификаций.";
|
||||
CLASSICAPI_CURSOR = "Курсор";
|
||||
CLASSICAPI_DIRECTION_ANY = "Любое направление";
|
||||
CLASSICAPI_DIRECTION_CONE = "В пределах 30 градусов";
|
||||
CLASSICAPI_DIRECTION_FRONT = "Прямо перед собой";
|
||||
CLASSICAPI_ENEMIES = "Противники";
|
||||
CLASSICAPI_ENEMY_DIRECTION = "Направление на противника";
|
||||
CLASSICAPI_ENEMY_ICON = "Показывать значок противника";
|
||||
CLASSICAPI_ENEMY_ICON_TOOLTIP = "Показывает значок над противником, которого выбирает динамическое выделение.";
|
||||
CLASSICAPI_ENEMY_RANGE = "Дистанция до противников";
|
||||
CLASSICAPI_EXPANDED_CURSORS = "Показывать расширенные курсоры";
|
||||
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "При наведении на NPC или объект курсор показывает, что с ним можно сделать. Например, наковальню для ремонта, ! или ? для заданий и крючок для вашего поплавка.";
|
||||
CLASSICAPI_FRIEND_DIRECTION = "Направление на союзника";
|
||||
CLASSICAPI_FRIEND_ICON = "Показывать значок союзника";
|
||||
CLASSICAPI_FRIEND_ICON_TOOLTIP = "Показывает значок над дружественной целью, которую выбирает дружественное динамическое выделение.";
|
||||
CLASSICAPI_FRIEND_RANGE = "Дистанция до союзников";
|
||||
CLASSICAPI_FRIEND_TARGETING = "Включить дружественное динамическое выделение";
|
||||
CLASSICAPI_FRIEND_TARGETING_TOOLTIP = "Выбирает ближайшую дружественную цель в поле зрения. Если у вас нет цели, полезные заклинания применяются к ней.";
|
||||
CLASSICAPI_FRIENDS = "Союзники";
|
||||
CLASSICAPI_GAME_OBJECT_ICON = "Показывать значок взаимодействия на объектах";
|
||||
CLASSICAPI_GAME_OBJECT_ICON_TOOLTIP = "Также показывает значок взаимодействия над объектами, которые нельзя выбрать целью, например почтовыми ящиками и сундуками.";
|
||||
CLASSICAPI_HIDE_EQUIPMENT_SETS = "Скрывать комплекты экипировки на предметах";
|
||||
CLASSICAPI_HIDE_EQUIPMENT_SETS_TOOLTIP = "Убирает строку с комплектами экипировки из подсказок предметов.";
|
||||
CLASSICAPI_ICON_SIZE = "Размер значков";
|
||||
CLASSICAPI_ICON_SIZE_TOOLTIP = "Размер значков взаимодействия, противников и союзников.";
|
||||
CLASSICAPI_IGNORE_BACKPACK = "Не сортировать рюкзак";
|
||||
CLASSICAPI_IGNORE_BACKPACK_TOOLTIP = "При сортировке сумок предметы в рюкзаке остаются на своих местах.";
|
||||
CLASSICAPI_IGNORE_BANK = "Не сортировать основной банк";
|
||||
CLASSICAPI_IGNORE_BANK_TOOLTIP = "При сортировке банка предметы в основных ячейках банка остаются на своих местах.";
|
||||
CLASSICAPI_INTERACT_DIRECTION = "Направление взаимодействия";
|
||||
CLASSICAPI_INTERACT_DIRECTION_TOOLTIP = "Где должен находиться NPC или объект, чтобы его выбрала клавиша взаимодействия. Направление определяется камерой, а не персонажем.";
|
||||
CLASSICAPI_INTERACT_ICON = "Показывать значок взаимодействия";
|
||||
CLASSICAPI_INTERACT_ICON_TOOLTIP = "Показывает значок над NPC или объектом, которые использует клавиша взаимодействия.";
|
||||
CLASSICAPI_INTERACT_KEY = "Клавиша взаимодействия";
|
||||
CLASSICAPI_INTERACT_RANGE = "Дистанция взаимодействия";
|
||||
CLASSICAPI_INTERFACE = "Интерфейс";
|
||||
CLASSICAPI_KEEP_ACTION_TARGETING = "Сохранять динамическое выделение";
|
||||
CLASSICAPI_KEEP_ACTION_TARGETING_TOOLTIP = "Динамическое выделение продолжает выбирать противников и союзников, пока у вас есть цель, выбранная вручную.";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS = "За кем следует обычная цель";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_ENEMY = "За мягкой целью противника";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_FRIEND = "За мягкой целью союзника";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_NONE = "Ни за кем";
|
||||
CLASSICAPI_LOOSE_TARGET_FOLLOWS_TOOLTIP = "Цель, выбранная динамическим выделением, меняется вместе с мягкой целью противника или союзника. Цель, выбранная вручную, сохраняется.";
|
||||
CLASSICAPI_LOW_PRIORITY_ICONS = "Показывать значки низкого приоритета";
|
||||
CLASSICAPI_LOW_PRIORITY_ICONS_TOOLTIP = "Также показывает значок над целью, с которой нельзя ничего сделать, например над членом группы. Значок выглядит как обычный указатель.";
|
||||
CLASSICAPI_MATCH_INSIDE_NAMES = "Учитывать правки внутри имени";
|
||||
CLASSICAPI_MATCH_INSIDE_NAMES_TOOLTIP = "Когда вы вводите текст в середине имени, предлагаемые имена также учитывают буквы после курсора.";
|
||||
CLASSICAPI_MATCH_THE_COMMAND = "Учитывать команду";
|
||||
CLASSICAPI_MATCH_THE_COMMAND_TOOLTIP = "Предлагает только имена, подходящие для команды, например участников гильдии для /gpromote. Если выключено, предлагаются все известные вам имена.";
|
||||
CLASSICAPI_MOTION = "Движение";
|
||||
CLASSICAPI_NAME_SUGGESTIONS = "Подсказки имен";
|
||||
CLASSICAPI_NAMEPLATES = "Таблички с именами";
|
||||
CLASSICAPI_NUMBERS = "Числа";
|
||||
CLASSICAPI_OPTIONS_ABOUT = "Настройки возможностей, которые ClassicAPI добавляет в игру. Каждый параметр меняет одну настройку, и изменение применяется сразу.";
|
||||
CLASSICAPI_OTHER_NAMEPLATE_ALPHA = "Непрозрачность чужих табличек";
|
||||
CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP = "Непрозрачность табличек с именами целей, которые не являются вашей целью.";
|
||||
CLASSICAPI_PREFER_YOUR_TARGET = "Отдавать предпочтение вашей цели";
|
||||
CLASSICAPI_PREFER_YOUR_TARGET_TOOLTIP = "Если выбранную вами цель можно взять, динамическое выделение выбирает ее. Если параметр выключен, всегда выбирается ближайшая цель.";
|
||||
CLASSICAPI_RANGE_TOOLTIP = "Максимальное расстояние в ярдах. Оно не может превышать значение параметра targetNearestDistance.";
|
||||
CLASSICAPI_RECENT_WHISPERS_FIRST = "Сначала недавние шепоты";
|
||||
CLASSICAPI_RECENT_WHISPERS_FIRST_TOOLTIP = "Предлагаемые имена начинаются с игроков, которым вы шептали совсем недавно.";
|
||||
CLASSICAPI_REMEMBER_PLAYERS = "Запоминать игроков";
|
||||
CLASSICAPI_REMEMBER_PLAYERS_TOOLTIP = "Сохраняет имя, класс, расу и пол встречавшихся вам игроков, чтобы модификации могли показывать их в следующих сеансах. Данные общие для всех ваших персонажей на этом сервере.";
|
||||
CLASSICAPI_SCRIPT_PROFILE = "Профилирование скриптов";
|
||||
CLASSICAPI_SCRIPT_PROFILE_TOOLTIP = "Измеряет время процессора, которое используют модификации. Изменение вступает в силу после перезагрузки интерфейса.";
|
||||
CLASSICAPI_SEPARATE_LARGE_NUMBERS = "Разделять большие числа";
|
||||
CLASSICAPI_SEPARATE_LARGE_NUMBERS_TOOLTIP = "Ставит разделитель между группами из трех цифр, когда модификации форматируют числа с помощью BreakUpLargeNumbers.";
|
||||
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT = "Сортировать сумки справа налево";
|
||||
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT_TOOLTIP = "При сортировке предметы перемещаются в конец последней сумки, а хлам — в начало рюкзака.";
|
||||
CLASSICAPI_STRICT_INTERACT_RANGE = "Строгая дистанция взаимодействия";
|
||||
CLASSICAPI_STRICT_INTERACT_RANGE_TOOLTIP = "Никогда не выбирает NPC или объект за пределами дистанции взаимодействия. Если параметр выключен, объект, доступный для использования сейчас, выбирается даже на большем расстоянии.";
|
||||
CLASSICAPI_TARGET_NAMEPLATE_ALPHA = "Непрозрачность таблички цели";
|
||||
CLASSICAPI_TARGET_NAMEPLATE_ALPHA_TOOLTIP = "Непрозрачность таблички с именем вашей цели.";
|
||||
CLASSICAPI_UI_SHAKE_STRENGTH = "Сила дрожания интерфейса";
|
||||
CLASSICAPI_UI_SHAKE_STRENGTH_TOOLTIP = "Определяет, насколько сильно эффекты дрожания смещают рамки. 0 отключает дрожание.";
|
||||
CLASSICAPI_UNIT_DIRECTION_TOOLTIP = "Где должна находиться цель, чтобы ее выбрало динамическое выделение. Направление определяется камерой, а не персонажем.";
|
||||
CLASSICAPI_WHEN_YOU_HAVE_A_TARGET = "Когда у вас есть цель";
|
||||
COMBAT_LABEL = "Бой";
|
||||
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Отсчет времени восстановления";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Вы действительно хотите удалить комплект экипировки \"%s\"?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Хотите сохранить комплект экипировки \"%s\"?";
|
||||
COPPER_AMOUNT = "%d |4медная монета:медные монеты:медных монет;";
|
||||
COPPER_AMOUNT_SYMBOL = "м";
|
||||
COPPER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:2:3:0:1|t";
|
||||
DEBUFF_SYMBOL_CURSE = "Пр";
|
||||
DEBUFF_SYMBOL_DISEASE = "Бз";
|
||||
DEBUFF_SYMBOL_MAGIC = "Мг";
|
||||
@@ -24,74 +116,66 @@ ENABLE_INTERACT_TEXT = "Включить клавишу взаимодейств
|
||||
EQUIPMENT_MANAGER = "Комплекты экипировки";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Пропустить ячейку";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "Управление экипировкой отключено.";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(предмет для ячейки \"%d\" отсутствует)|r";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(не хватает: %d)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "Поместить в сумку";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "Учитывать ячейку";
|
||||
EQUIPMENT_SETS = "Комплекты экипировки: |cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "Это название уже используется для другого комплекта.";
|
||||
EQUIPMENT_SETS_TOO_MANY = "Вы не можете создавать новые комплекты экипировки.";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Выбрать специализацию:";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Назначить специализации:";
|
||||
EQUIPMENT_SET_EDIT = "Изменить название/значок";
|
||||
EQUIPMENT_SET_SETTINGS = "Параметры";
|
||||
EQUIPSET_EQUIP = "Надеть";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Не удалось заменить комплект обмундирования. Инвентарь переполнен.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Некоторые предметы из набора \"%s\" не будут экипированы, поскольку вы участвуете в бою.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Недостает одного или более предметов из набора \"%s\".";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Не удалось сменить экипировку. Инвентарь переполнен.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Некоторые предметы из комплекта \"%s\" нельзя сменить, поскольку вы участвуете в бою.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Один или несколько предметов из комплекта \"%s\" отсутствуют и не были надеты.";
|
||||
EVENTTRACE_APPLY_DEFAULT_FILTER = "Применить фильтр по умолчанию";
|
||||
EVENTTRACE_ARG_FMT = "Арг. %d:";
|
||||
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Снять выделение";
|
||||
EVENTTRACE_ARG_FMT = "Аргумент %d:";
|
||||
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Снять все";
|
||||
EVENTTRACE_BUTTON_DISCARD_FILTER = "Сбросить все";
|
||||
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Выделить все";
|
||||
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Выбрать все";
|
||||
EVENTTRACE_BUTTON_MARKER = "Маркер";
|
||||
EVENTTRACE_BUTTON_PAUSE = "Пауза";
|
||||
EVENTTRACE_BUTTON_PLAY = "Играть";
|
||||
EVENTTRACE_BUTTON_PLAY = "Продолжить";
|
||||
EVENTTRACE_FILTER_HEADER = "Фильтр";
|
||||
EVENTTRACE_HEADER = "Журнал события";
|
||||
EVENTTRACE_LOG_CR_EVENTS = "Отображать глобальные события по регистрации";
|
||||
EVENTTRACE_HEADER = "Журнал событий";
|
||||
EVENTTRACE_LOG_CR_EVENTS = "Показывать события глобального реестра";
|
||||
EVENTTRACE_LOG_DISCARD = "Журнал сброшен";
|
||||
EVENTTRACE_LOG_HEADER = "Журнал";
|
||||
EVENTTRACE_LOG_PAUSE = "Регистрация участников события приостановлена";
|
||||
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Фиксация событий прекращается в свернутом состоянии";
|
||||
EVENTTRACE_LOG_START = "Регистрация участников события началась";
|
||||
EVENTTRACE_LOG_WHEN_HIDDEN = "Фиксировать события в свернутом состоянии";
|
||||
EVENTTRACE_LOG_PAUSE = "Запись событий приостановлена";
|
||||
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Запись событий приостановлена, пока окно скрыто";
|
||||
EVENTTRACE_LOG_START = "Запись событий начата";
|
||||
EVENTTRACE_LOG_WHEN_HIDDEN = "Записывать события, когда окно скрыто";
|
||||
EVENTTRACE_MARKER = "Маркер";
|
||||
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
|
||||
EVENTTRACE_OPTIONS = "Параметры";
|
||||
EVENTTRACE_RESULTS = "Результаты: %d";
|
||||
EVENTTRACE_SHOW_ARGUMENTS = "Отображать аргументы события";
|
||||
EVENTTRACE_SHOW_TIMESTAMP = "Отображать метку времени события";
|
||||
EVENTTRACE_RESULTS = "Результатов: %d";
|
||||
EVENTTRACE_SHOW_ARGUMENTS = "Показывать аргументы событий";
|
||||
EVENTTRACE_SHOW_TIMESTAMP = "Показывать время событий";
|
||||
EVENTTRACE_TIMESTAMP = "Время:";
|
||||
CLEAR_FOCUS = "Отменить запоминание цели";
|
||||
INTERACT_ICONS_DEFAULT = "Только NPC (по умолчанию)";
|
||||
INTERACT_ICONS_OPTION = "Значки клавиш взаимодействия";
|
||||
INTERACT_ICONS_SHOW_ALL = "Показать все";
|
||||
INTERACT_ICONS_OPTION = "Значки клавиши взаимодействия";
|
||||
INTERACT_ICONS_SHOW_ALL = "Показывать все";
|
||||
INTERACT_ICONS_SHOW_NONE = "Не показывать";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Способности будут задействоваться, когда пользователь нажимает, а не отпускает клавишу.";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "Система захвата целей, динамически выбирающая цели в поле зрения. Работает параллельно со стандартной системой наведения.";
|
||||
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Отображение обратного отсчета для времени восстановления на панели команд.";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Позволяет взаимодействовать с NPC и объектами по нажатию клавиши.";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Определяет, какие значки появляются над NPC и объектами, если включен параметр клавиши взаимодействия.";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Прекращение автоматической атаки при смене цели.";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Способности на панели команд срабатывают при нажатии клавиши, а не при ее отпускании.";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "Система выделения, которая динамически выбирает противников в зависимости от направления взгляда. Работает вместе со стандартной системой выделения.";
|
||||
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Показывать числовой отсчет времени восстановления на панели команд.";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Позволяет взаимодействовать с NPC и объектами нажатием клавиши.";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Определяет, какие значки появляются над NPC и объектами, когда включена клавиша взаимодействия.";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Прекращать автоатаку при смене цели.";
|
||||
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
|
||||
KEY_BINDING_TOOLTIP = "<ПКМ, чтобы отвязать>";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Назначьте клавишу \"%s\"или нажмите %s для отмены";
|
||||
KEY_BINDING_TOOLTIP = "<ПКМ, чтобы снять привязку>";
|
||||
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Назначьте клавишу для \"%s\" или нажмите %s для отмены";
|
||||
SET_FOCUS = "Запомнить цель";
|
||||
FOCUS_TOKEN_NOT_FOUND = "<фокус отсутствует>";
|
||||
FOCUSTARGET = "Запомнить цель";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4предмет:предмета:предметов;";
|
||||
ITEMS_EQUIPPED = "%d |4предмет экипирован:предмета экипировано:предметов экипировано;";
|
||||
ITEMS_IN_INVENTORY = "%d |4предмет:предмета:предметов; в сумках";
|
||||
ITEM_SLOTS_IGNORED = "%d |4ячейка пропущена:ячейки пропущены:ячейки пропущены;";
|
||||
ITEM_MISSING = "Предмет для ячейки %s отсутствует";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4предмет отсутствует:предмета отсутствует:предметов отсутствуют;";
|
||||
STOP_AUTO_ATTACK = "Прекращение автоатаки";
|
||||
TARGETFOCUS = "Восстановить цель";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT = "%d |4медная монета:медные монеты:медных монет;";
|
||||
COPPER_AMOUNT_SYMBOL = "м";
|
||||
GEARSETS_POPUP_TEXT = "Введите название комплекта (не более 16 символов):";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT = "%d |4золотая:золотые:золотых;";
|
||||
GOLD_AMOUNT_SYMBOL = "з";
|
||||
GOLD_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:0:1:0:1|t";
|
||||
GOLD_AMOUNT_TEXTURE_STRING = "%s|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:0:1:0:1|t";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "Все значки";
|
||||
ICON_FILTER_ITEM = "Предметы";
|
||||
@@ -109,17 +193,17 @@ ITEM_MOD_ATTACK_POWER_SHORT = "к силе атаки";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "к силе атаки дальнего боя";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "к показателю критического удара в ближнем бою";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "к показателю критического удара в дальнем бою";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "к меткости в ближнем бою";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "к меткости в ближнем бою";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "к меткости в дальнем бою";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "к меткости заклинаний";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "к критическому эффекту заклинаний";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "к дополнительному урону";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "к дополнительному исцелению";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "к восполнению маны";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Защита";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Уклонение";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Парирование";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Блок";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "к защите";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "к уклонению";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "к парированию";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "к блокированию";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "к показателю блокирования";
|
||||
ITEM_MOD_HOLY_SPELL_DAMAGE_DONE_SHORT = "к урону от магии света";
|
||||
ITEM_MOD_FIRE_SPELL_DAMAGE_DONE_SHORT = "к урону от магии огня";
|
||||
@@ -131,6 +215,12 @@ ITEM_MOD_SPELL_PENETRATION_SHORT = "к проникающей способнос
|
||||
ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "к пробиванию брони";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "к восполнению здоровья";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "к урону в секунду";
|
||||
ITEMS_EQUIPPED = "%d |4предмет экипирован:предмета экипировано:предметов экипировано;";
|
||||
ITEMS_IN_INVENTORY = "%d |4предмет:предмета:предметов; в инвентаре";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4предмет отсутствует:предмета отсутствует:предметов отсутствует;";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4предмет:предмета:предметов;";
|
||||
ITEM_MISSING = "%s отсутствует";
|
||||
ITEM_SLOTS_IGNORED = "%d |4ячейка пропущена:ячейки пропущены:ячеек пропущено;";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
DECIMAL_SEPERATOR = ",";
|
||||
FIRST_NUMBER = "тыс.";
|
||||
@@ -141,14 +231,18 @@ SECOND_NUMBER_CAP = "М";
|
||||
SECOND_NUMBER_CAP_NO_SPACE = "М";
|
||||
THIRD_NUMBER = "млрд";
|
||||
FOURTH_NUMBER = "трлн";
|
||||
LUA_ERROR = "Ошибка Lua";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Комплекты экипировки";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "Новый комплект";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Заметка о |3-5(%s):";
|
||||
PLAYER_LIST_DELIMITER = ", ";
|
||||
RELOADUI = "Перезагрузить интерфейс";
|
||||
SET_FRIENDNOTE_LABEL = "Задать заметку для %s:";
|
||||
SET_NOTE = "Добавить заметку";
|
||||
SILVER_AMOUNT = "%d |4серебряная:серебряные:серебряных;";
|
||||
SILVER_AMOUNT_SYMBOL = "с";
|
||||
SILVER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:1:2:0:1|t";
|
||||
SLASH_CANCELAURA1 = "/снятьауру";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/нормоблик";
|
||||
@@ -222,10 +316,14 @@ SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/использовать";
|
||||
SOCIAL_LABEL = "Общение";
|
||||
LINK_TRADESKILL_TOOLTIP = "Щелкните здесь, чтобы создать ссылку на вашу профессию.";
|
||||
TEXT_TO_SPEECH = "Текст в речь";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Изменить скорость речи.";
|
||||
STOP_AUTO_ATTACK = "Прекращение автоатаки";
|
||||
SYMBOLIC_NEGATIVE_NUMBER = "-%s";
|
||||
SYMBOLIC_POSITIVE_NUMBER = "+%s";
|
||||
TARGETFOCUS = "Восстановить цель";
|
||||
LINK_TRADESKILL_TOOLTIP = "Щелкните здесь, чтобы создать ссылку на свою профессию.";
|
||||
TEXT_TO_SPEECH = "Синтез речи";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Изменить скорость речи";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "Громкость речи";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "Параметры голоса";
|
||||
TOTAL_MEM_KB_ABBR = "Память: %.0f КБ";
|
||||
TOTAL_MEM_MB_ABBR = "Модификации занимают в памяти: %.2f МБ";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "Выбор голоса";
|
||||
TOTAL_MEM_KB_ABBR = "Память модификаций: %.0f КБ";
|
||||
TOTAL_MEM_MB_ABBR = "Память модификаций: %.2f МБ";
|
||||
|
||||
@@ -19,7 +19,7 @@ CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "骑乘状态下施放法术、使用物体
|
||||
CLASSICAPI_AUTO_STAND = "自动站起";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "坐下状态下施放法术、拾取或打开飞行地图时自动站起。";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "自动取消形态";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "施放当前形态无法施放的法术、与NPC对话、使用物体或物品、或打开飞行地图时自动取消该形态。";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "施放当前形态无法施放的法术、使用物品或打开飞行地图时自动取消该形态。同时允许在德鲁伊形态和幽魂之狼形态下与NPC对话和使用物体。";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "背包与物品";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "施法与攻击";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "战斗记录保留时间";
|
||||
|
||||
@@ -217,8 +217,7 @@ void Register() {
|
||||
{"Standard", static_cast<int>(Variant::Standard)},
|
||||
{"UrlSafe", static_cast<int>(Variant::UrlSafe)},
|
||||
};
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "Base64Variant",
|
||||
kBase64Variant, 2);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "Base64Variant", kBase64Variant);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -305,8 +305,7 @@ void Register() {
|
||||
{"Zlib", static_cast<int>(Method::Zlib)},
|
||||
{"Gzip", static_cast<int>(Method::Gzip)},
|
||||
};
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "CompressionMethod",
|
||||
kCompressionMethod, 3);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "CompressionMethod", kCompressionMethod);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -224,9 +224,7 @@ static void RegisterLuaFunctions() {
|
||||
// 4.3.4's switch for the item-tooltip line; dedups by name on /reload.
|
||||
g_hideSetsCVar = CVar::Factory::Register("dontShowEquipmentSetsOnItems", "Don't show which equipment sets an item is associated with", Offsets::CVAR_CATEGORY_GAME, "0");
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY),
|
||||
g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY), g_methods);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+81
-19
@@ -55,6 +55,10 @@ struct Reservation {
|
||||
int slot; // -1 until claimed
|
||||
const Game::Doc::Event *doc; // API documentation; nullptr = undocumented
|
||||
int fixedId; // an `EngineName`'s engine id (its slot for good); -1 = claimed
|
||||
// The SStrDup'd name `Claim` wrote into `slot`'s entry; nullptr while
|
||||
// unclaimed. `RetryClaims` compares it against the live entry to notice
|
||||
// that the engine rebuilt the table underneath a cached slot.
|
||||
const char *claimed;
|
||||
};
|
||||
Reservation g_reserved[MAX_RESERVED];
|
||||
int g_reservedCount = 0;
|
||||
@@ -63,7 +67,7 @@ bool g_writesEnabled = false;
|
||||
// claim) is logged once per session instead of silently dropping the
|
||||
// event — the failure mode that would otherwise rot unnoticed. Re-armed on
|
||||
// `/reload` (the table is rebuilt fresh). In practice this never triggers:
|
||||
// the engine leaves ~115 claimable gaps below 549 (see `TryClaim`) against
|
||||
// the engine leaves ~115 claimable gaps below 549 (see `Claim`) against
|
||||
// our ~77 reservations, and `Grow` adds room past that. The log is
|
||||
// purely a backstop if some future install somehow drains the gap pool.
|
||||
bool g_warnedTableFull = false;
|
||||
@@ -147,23 +151,39 @@ int ClaimDemand() {
|
||||
// tail. Below 549 only nampower writes, at 369 and 540, both before any Lua
|
||||
// runs, so skip-occupied steps over them. Claiming low keeps us out of
|
||||
// everyone's way and independent of their clamp / hook order.
|
||||
int TryClaim(const char *eventName) {
|
||||
//
|
||||
// Claims only into the IN-GAME table. `CGGameUI::Initialize` (0x0048FE00)
|
||||
// calls `RegisterScriptFunctions` (our `EnableWrites` hook) and only THEN
|
||||
// `SetScriptEventTable(0xBE1198, 0x225)`, so between the two the live table
|
||||
// is still glue's 26-entry one. A `Frame::RegisterEvent` in that window
|
||||
// (a third-party DLL registering a frame from its own LoadScriptFunctions
|
||||
// hook — issue #134) used to grow the glue table and claim into it; the
|
||||
// engine then freed that buffer and our cached ids silently aliased engine
|
||||
// events in the real table. The in-game build always passes 0x225 names
|
||||
// (clamps by other DLLs only enlarge it), so a smaller count is not it.
|
||||
bool GameTableInstalled(int count) {
|
||||
return count >= Offsets::EVENT_NAME_TABLE_STATIC_COUNT;
|
||||
}
|
||||
|
||||
void Claim(Reservation &r) {
|
||||
if (!g_writesEnabled)
|
||||
return -1;
|
||||
return;
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
if (base == nullptr || count <= 0)
|
||||
return -1;
|
||||
if (base == nullptr || !GameTableInstalled(count))
|
||||
return;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (Claimable(base, i)) {
|
||||
auto **namePtr = Game::Ptr<const char *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE,
|
||||
Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
char *copy = SStrDup(eventName);
|
||||
char *copy = SStrDup(r.name);
|
||||
if (copy == nullptr)
|
||||
return -1;
|
||||
return;
|
||||
*namePtr = copy;
|
||||
return i;
|
||||
r.slot = i;
|
||||
r.claimed = copy;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Writes are enabled and the table is valid, but every slot is taken —
|
||||
@@ -173,9 +193,39 @@ int TryClaim(const char *eventName) {
|
||||
Debug::Log::Printf(
|
||||
"[event] table full (%d slots, no NULL left) — cannot claim '%s'"
|
||||
" and any further custom events; even Grow couldn't make room.",
|
||||
count, eventName ? eventName : "?");
|
||||
count, r.name);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// True when a claimed reservation's slot no longer holds its name: the
|
||||
// engine rebuilt the table (every entry re-`SStrDup`'d at a fresh buffer)
|
||||
// without our `PrepareForReload` seeing it. The pointer test is the cheap
|
||||
// common case; the strcmp covers an allocator handing the engine our freed
|
||||
// block for a different name.
|
||||
bool Stale(const Reservation &r) {
|
||||
if (r.claimed == nullptr)
|
||||
return false;
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
if (base == nullptr || r.slot >= count)
|
||||
return true;
|
||||
const char *live = Game::Read<const char *>(
|
||||
base + r.slot * Offsets::EVENT_ENTRY_STRIDE, Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
if (live == r.claimed)
|
||||
return false;
|
||||
return live == nullptr || std::strcmp(live, r.name) != 0;
|
||||
}
|
||||
|
||||
// Forget every claimed slot: the table they were claimed in is gone. The
|
||||
// engine freed our name copies with its per-entry teardown, so nothing to
|
||||
// release here. Re-arms the per-build one-shots.
|
||||
void ForgetClaims() {
|
||||
for (int i = 0; i < g_reservedCount; ++i) {
|
||||
g_reserved[i].slot = g_reserved[i].fixedId;
|
||||
g_reserved[i].claimed = nullptr;
|
||||
}
|
||||
g_warnedTableFull = false;
|
||||
g_growLatched = false;
|
||||
}
|
||||
|
||||
// On-demand event-table grow. Correctly TIMED: called once per in-game
|
||||
@@ -205,12 +255,14 @@ int TryClaim(const char *eventName) {
|
||||
void Grow() {
|
||||
if (g_growLatched || !g_writesEnabled)
|
||||
return;
|
||||
g_growLatched = true; // one attempt per build, whether or not it grows
|
||||
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
if (base == nullptr || !GameTableInstalled(count))
|
||||
return; // still glue's table — the in-game build hasn't run yet
|
||||
g_growLatched = true; // one attempt per build, whether or not it grows
|
||||
|
||||
const int demand = ClaimDemand();
|
||||
if (base == nullptr || count <= 0 || demand <= 0)
|
||||
if (demand <= 0)
|
||||
return;
|
||||
|
||||
int nulls = 0;
|
||||
@@ -320,6 +372,7 @@ AutoReserve::AutoReserve(const char *name, const Game::Doc::Event *doc) {
|
||||
g_reserved[g_reservedCount].slot = -1;
|
||||
g_reserved[g_reservedCount].doc = doc;
|
||||
g_reserved[g_reservedCount].fixedId = -1;
|
||||
g_reserved[g_reservedCount].claimed = nullptr;
|
||||
index_ = g_reservedCount;
|
||||
++g_reservedCount;
|
||||
}
|
||||
@@ -339,7 +392,7 @@ EngineName::EngineName(int id, const char *name, const Game::Doc::Event *doc) {
|
||||
if (*slot != nullptr)
|
||||
return; // the engine names it; never take an engine event's slot
|
||||
*slot = name;
|
||||
g_reserved[g_reservedCount] = {name, id, doc, id};
|
||||
g_reserved[g_reservedCount] = {name, id, doc, id, nullptr};
|
||||
++g_reservedCount;
|
||||
}
|
||||
|
||||
@@ -428,6 +481,18 @@ void FireValues(int slot, void *L, int firstArgIdx, int nargs) {
|
||||
}
|
||||
|
||||
void RetryClaims() {
|
||||
// A table rebuilt behind our back (no FrameScript_Initialize in between)
|
||||
// leaves every cached slot pointing at whatever engine event now sits at
|
||||
// that id. Check the claims against the live table and start over if any
|
||||
// moved — we run before the original registers, so no frame has joined a
|
||||
// chain in the new table yet.
|
||||
for (int i = 0; i < g_reservedCount; ++i) {
|
||||
if (Stale(g_reserved[i])) {
|
||||
Debug::Log::Printf("[event] table rebuilt under cached slots — re-claiming");
|
||||
ForgetClaims();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Grow the table first if the low gap pool is short (dormant valve).
|
||||
// Runs at most once per table build; on the first call every chain is
|
||||
// still empty (this is the first registration), which is the only safe
|
||||
@@ -435,18 +500,15 @@ void RetryClaims() {
|
||||
Grow();
|
||||
for (int i = 0; i < g_reservedCount; ++i) {
|
||||
if (g_reserved[i].slot < 0)
|
||||
g_reserved[i].slot = TryClaim(g_reserved[i].name);
|
||||
Claim(g_reserved[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void EnableWrites() { g_writesEnabled = true; }
|
||||
|
||||
void PrepareForReload() {
|
||||
for (int i = 0; i < g_reservedCount; ++i)
|
||||
g_reserved[i].slot = g_reserved[i].fixedId;
|
||||
ForgetClaims();
|
||||
g_writesEnabled = false;
|
||||
g_warnedTableFull = false; // fresh table after the rebuild — re-arm
|
||||
g_growLatched = false; // re-arm the one-shot grow for the new table
|
||||
}
|
||||
|
||||
static const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ inline void FireIdSuccess(int eventID, int id, bool success) {
|
||||
// catch any reservations that couldn't claim earlier.
|
||||
void RetryClaims();
|
||||
|
||||
// Internal: permit `TryClaim` to write to the event table. Held
|
||||
// Internal: permit `Claim` to write to the event table. Held
|
||||
// closed until `LoadScriptFunctions_h` returns, so the boot phase
|
||||
// (during which the engine and SuperWoWhook fire many internal
|
||||
// `RegisterEvent` calls) can't trigger our writes. Writing during
|
||||
|
||||
@@ -144,10 +144,11 @@ const Game::Doc::Method kFontDocs[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY),
|
||||
kFontStringMethods, 2, kFontStringDocs, 2);
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_FONT_METHOD_REGISTRY), kFontMethods,
|
||||
2, kFontDocs, 2);
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY), kFontStringMethods,
|
||||
kFontStringDocs);
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FONT_METHOD_REGISTRY), kFontMethods, kFontDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -181,8 +181,8 @@ const Game::Doc::Method kHtmlDocs[] = {
|
||||
{"GetIndentedWordWrap", &kHtmlGet},
|
||||
};
|
||||
|
||||
template <typename P> void Register(uintptr_t registry, const Game::Doc::Method *docs) {
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(registry), P::methods, 2, docs, 2);
|
||||
template <typename P> void Register(uintptr_t registry, Game::Doc::MethodList docs) {
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(registry), P::methods, docs);
|
||||
}
|
||||
|
||||
// A font string takes the bit from its font only when its settable mask
|
||||
|
||||
@@ -378,9 +378,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_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_FONTSTRING_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -96,8 +96,8 @@ const Game::Doc::Method kDocs[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY), kMethods, 2,
|
||||
kDocs, 2);
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY), kMethods, kDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -324,9 +324,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_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_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -1165,9 +1165,7 @@ const Game::Lua::FrameMethodEntry g_frameMethods[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY),
|
||||
g_frameMethods,
|
||||
static_cast<int>(sizeof(g_frameMethods) / sizeof(g_frameMethods[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_frameMethods);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -117,9 +117,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_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_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -186,9 +186,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_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_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -102,8 +102,7 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), g_methods);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -453,9 +453,7 @@ void RegisterLuaFunctions() {
|
||||
Common::ReplaceWithCall(Offsets::CALL_FRAME_DRAW_SELF_CHILD, kDrawChildCall, sizeof kDrawChildCall,
|
||||
reinterpret_cast<const void *>(&DrawChildThunk));
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_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_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -1588,9 +1588,7 @@ void RegisterLuaFunctions() {
|
||||
reinterpret_cast<RegisterFactory_t>(Offsets::FUN_FRAMEXML_REGISTER_FACTORY)(
|
||||
"Cooldown", &CreateCooldown, 0);
|
||||
Api::Documentation::NameWidgetSystem(g_hash, "FrameAPICooldown");
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
g_hash, g_methods, static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])),
|
||||
g_methodDocs, static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
|
||||
Game::Lua::RegisterFrameMethods(g_hash, g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -53,9 +53,6 @@ using SetAttr_t = void(__thiscall *)(void *node, const char *name, const char *v
|
||||
using Build_t = void *(__fastcall *)(const void *node, void *parent, void *status);
|
||||
using StatusReset_t = void(__thiscall *)(void *self);
|
||||
|
||||
// LUA_TNONE: lua_type of an index past the top.
|
||||
constexpr int kTypeNone = -1;
|
||||
|
||||
// 4.3.4's usage line; vanilla's (0x00872F60) has no `id`.
|
||||
constexpr const char *kUsage =
|
||||
"Usage: CreateFrame(\"frameType\" [, \"name\"] [, parent] [, \"template\"] [, id])";
|
||||
@@ -102,7 +99,7 @@ int __fastcall CreateFrame_h(void *L) {
|
||||
|
||||
const int parentType = Type(L, 3);
|
||||
if (!IsString(L, 1) ||
|
||||
(parentType != kTypeNone && parentType != TYPE_NIL && parentType != TYPE_TABLE)) {
|
||||
(parentType != TYPE_NONE && parentType != TYPE_NIL && parentType != TYPE_TABLE)) {
|
||||
Error(L, kUsage);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -111,9 +111,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_REGION_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_REGION_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -221,9 +221,8 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_REGION_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])));
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -67,9 +67,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_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_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -113,9 +113,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_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_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -108,10 +108,11 @@ const Game::Doc::Method kFontStringDocs[] = {{"SetFormattedText", &kFontStringDo
|
||||
const Game::Doc::Method kButtonDocs[] = {{"SetFormattedText", &kButtonDoc}};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY),
|
||||
kFontStringMethods, 1, kFontStringDocs, 1);
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kButtonMethods,
|
||||
1, kButtonDocs, 1);
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY), kFontStringMethods,
|
||||
kFontStringDocs);
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kButtonMethods, kButtonDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -198,8 +198,7 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_methods);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -95,8 +95,7 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
void RegisterLuaFunctions() {
|
||||
for (const uintptr_t registry : {static_cast<uintptr_t>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
|
||||
static_cast<uintptr_t>(Offsets::VAR_SIMPLEHTML_METHOD_REGISTRY)}) {
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(registry), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(registry), g_methods);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,9 +139,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_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_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+5
-16
@@ -569,26 +569,15 @@ const Game::Lua::FrameMethodEntry g_fontStringMethods[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY),
|
||||
g_regionMethods,
|
||||
static_cast<int>(sizeof(g_regionMethods) / sizeof(g_regionMethods[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_regionMethods);
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY),
|
||||
g_frameMethods,
|
||||
static_cast<int>(sizeof(g_frameMethods) / sizeof(g_frameMethods[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_frameMethods);
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY),
|
||||
g_buttonMethods,
|
||||
static_cast<int>(sizeof(g_buttonMethods) / sizeof(g_buttonMethods[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), g_buttonMethods);
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_TEXTURE_METHOD_REGISTRY),
|
||||
g_textureMethods,
|
||||
static_cast<int>(sizeof(g_textureMethods) / sizeof(g_textureMethods[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_TEXTURE_METHOD_REGISTRY), g_textureMethods);
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY),
|
||||
g_fontStringMethods,
|
||||
static_cast<int>(sizeof(g_fontStringMethods) /
|
||||
sizeof(g_fontStringMethods[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY), g_fontStringMethods);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -380,9 +380,8 @@ void RegisterLuaFunctions() {
|
||||
reinterpret_cast<const void *>(&RemovalThunk));
|
||||
Common::ReplaceWithCall(Offsets::ADDR_UI_FOCUS_PASS_PROBE, kProbe, sizeof kProbe,
|
||||
reinterpret_cast<const void *>(&ProbeThunk));
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_FRAME_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])));
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
Game::Lua::RegisterGlobalFunction("GetMouseFoci", &Script_GetMouseFoci, &kGetMouseFoci);
|
||||
}
|
||||
|
||||
|
||||
@@ -148,8 +148,8 @@ const Game::Doc::Method kDocs[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kMethods, 2,
|
||||
kDocs, 2);
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kMethods, kDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -389,9 +389,8 @@ void RegisterLuaFunctions() {
|
||||
reinterpret_cast<const void *>(&PressDispatchThunk));
|
||||
Common::ReplaceWithCall(Offsets::ADDR_UI_RELEASE_DISPATCH, kReleaseDispatch, sizeof kReleaseDispatch,
|
||||
reinterpret_cast<const void *>(&ReleaseDispatchThunk));
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_FRAME_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])));
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -104,8 +104,8 @@ const Game::Doc::Function kDoc{"Sets which mouse buttons run the OnMouseDown and
|
||||
const Game::Doc::Method kDocs[] = {{"RegisterForMouse", &kDoc}};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kMethods, 1,
|
||||
kDocs, 1);
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kMethods, kDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -209,9 +209,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_REGION_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_REGION_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -108,12 +108,9 @@ const Game::Doc::Method g_frameDocs[] = {
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_regionMethods,
|
||||
static_cast<int>(sizeof(g_regionMethods) / sizeof(g_regionMethods[0])), g_regionDocs,
|
||||
static_cast<int>(sizeof(g_regionDocs) / sizeof(g_regionDocs[0])));
|
||||
g_regionDocs);
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_frameMethods,
|
||||
static_cast<int>(sizeof(g_frameMethods) / sizeof(g_frameMethods[0])), g_frameDocs,
|
||||
static_cast<int>(sizeof(g_frameDocs) / sizeof(g_frameDocs[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_frameMethods, g_frameDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -49,8 +49,8 @@ const Game::Doc::Function kDoc{"Enables or disables the button, as Enable() or D
|
||||
const Game::Doc::Method kDocs[] = {{"SetEnabled", &kDoc}};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kMethods, 1,
|
||||
kDocs, 1);
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_BUTTON_METHOD_REGISTRY), kMethods, kDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -203,9 +203,7 @@ void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("AddSourceLocationExclude", &Script_AddSourceLocationExclude,
|
||||
&kAddSourceLocationExclude);
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_REGION_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_REGION_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -351,8 +351,7 @@ const Game::Lua::FrameMethodEntry g_frameMethods[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_frameMethods,
|
||||
static_cast<int>(sizeof(g_frameMethods) / sizeof(g_frameMethods[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_METHOD_REGISTRY), g_frameMethods);
|
||||
}
|
||||
|
||||
// Frames survive /reload but the engine rebuilds the event table with empty
|
||||
|
||||
@@ -114,9 +114,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FRAME_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_FRAME_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -126,9 +126,7 @@ static const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY),
|
||||
g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY), g_methods);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -366,9 +366,7 @@ static const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY),
|
||||
g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY), g_methods);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -309,15 +309,9 @@ void RegisterLuaFunctions() {
|
||||
&Script_GetItemInventorySlotKey);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemInventorySlotInfo",
|
||||
&Script_GetItemInventorySlotInfo);
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "InventoryType", kInventoryTypeEntries,
|
||||
sizeof(kInventoryTypeEntries) / sizeof(kInventoryTypeEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "ItemClass", kItemClassEntries,
|
||||
sizeof(kItemClassEntries) / sizeof(kItemClassEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "ItemQuality", kItemQualityEntries,
|
||||
sizeof(kItemQualityEntries) / sizeof(kItemQualityEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "InventoryType", kInventoryTypeEntries);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ItemClass", kItemClassEntries);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ItemQuality", kItemQualityEntries);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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 "localization/Locale.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Localization::Locale {
|
||||
|
||||
// Script_GetLocale (FUN_0048d8b0): VAR_LOCALE_NAME_TABLE[VAR_LOCALE_INDEX].
|
||||
const char *Client() {
|
||||
const uint32_t index = Game::Read<uint32_t>(Offsets::VAR_LOCALE_INDEX);
|
||||
return reinterpret_cast<const char *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_NAME_TABLE))[index];
|
||||
}
|
||||
|
||||
bool Is(const char *code) { return std::strcmp(Client(), code) == 0; }
|
||||
|
||||
} // namespace Localization::Locale
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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 client locale by NAME, as GetLocale() reports it.
|
||||
//
|
||||
// Language rules (plurals, accent folding, number abbreviations) must pick
|
||||
// by this name, never by VAR_LOCALE_INDEX. The 1.12 index is the Wow.ini
|
||||
// tag's position in a table of eight codes (enUS koKR frFR deDE zhCN zhTW
|
||||
// esES xxYY), not 4.3.4's nine-slot order, so a 4.3.4 index such as 8 for
|
||||
// ruRU never occurs here. A Russian client is one whose table holds "ruRU",
|
||||
// in whatever slot. The index stays right for picking a DBC string column.
|
||||
namespace Localization::Locale {
|
||||
|
||||
// The locale code GetLocale() returns ("enUS", "frFR", ...).
|
||||
const char *Client();
|
||||
|
||||
// Whether the client locale is `code`.
|
||||
bool Is(const char *code);
|
||||
|
||||
} // namespace Localization::Locale
|
||||
@@ -62,6 +62,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "localization/Locale.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
@@ -131,17 +132,13 @@ constexpr Scale kScalesAsian[] = {
|
||||
|
||||
CVar::Factory::Handle g_breakUpLargeNumbers = nullptr;
|
||||
|
||||
// VAR_LOCALE_INDEX order: enUS, koKR, frFR, deDE, zhCN, zhTW, esES, esMX, ruRU.
|
||||
bool ClientIsAsian() {
|
||||
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
return locale == 1 || locale == 4 || locale == 5;
|
||||
}
|
||||
|
||||
bool IsAsianLocale(const char *locale) {
|
||||
return std::strcmp(locale, "koKR") == 0 || std::strcmp(locale, "zhCN") == 0 ||
|
||||
std::strcmp(locale, "zhTW") == 0;
|
||||
}
|
||||
|
||||
bool ClientIsAsian() { return IsAsianLocale(Localization::Locale::Client()); }
|
||||
|
||||
// `_G[name]` when it is a string, else "".
|
||||
std::string GlobalString(void *L, const char *name) {
|
||||
Lua::PushString(L, name);
|
||||
|
||||
+5
-98
@@ -15,6 +15,7 @@
|
||||
#include "../Offsets.h"
|
||||
#include "../guid/Guid.h"
|
||||
#include "../object/Resolve.h"
|
||||
#include "Request.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -22,22 +23,6 @@ namespace Loot::Nearby {
|
||||
|
||||
namespace {
|
||||
|
||||
// Engine interact-range constants, read out of `.rdata` once and
|
||||
// pinned here as `constexpr`. Both are referenced by `FUN_005EC110`
|
||||
// (the master interact-state checker) and propagated through
|
||||
// `FUN_005DF130` / `FUN_005DF2A0` (CMSG_LOOT senders). Yards units;
|
||||
// values verified at runtime addresses `DAT_0080B058` and
|
||||
// `DAT_0080A1E8`.
|
||||
//
|
||||
// INTERACT_REACH (1.333…) is added to `target_radius + player_radius`
|
||||
// for the unclamped range; modeled as the "reach beyond your touch"
|
||||
// constant.
|
||||
// MIN_INTERACT_RANGE (5.0) clamps the result so even tiny
|
||||
// targets with near-zero bounding radius still get the standard
|
||||
// 5-yard right-click reach.
|
||||
constexpr float INTERACT_REACH = 1.3333333f;
|
||||
constexpr float MIN_INTERACT_RANGE = 5.0f;
|
||||
|
||||
// `ClntObjMgrEnumVisibleObjects(callback, context)` — walks the engine's
|
||||
// visible-object table. The callback shape's `void *unusedEdx` slot
|
||||
// absorbs the second `__fastcall` register so MSVC lays out the trailing
|
||||
@@ -49,84 +34,6 @@ using ClntObjMgrEnumVisibleObjectsCallback_t = int(__fastcall *)(void *ctx,
|
||||
using ClntObjMgrEnumVisibleObjects_t =
|
||||
int(__fastcall *)(ClntObjMgrEnumVisibleObjectsCallback_t cb, void *ctx);
|
||||
|
||||
// `__thiscall(self, outBuf) → const C3Vector *` at vtable slot 5
|
||||
// (byte offset `0x14`). Both CGUnit_C and CGPlayer_C populate the
|
||||
// caller-provided 12-byte buffer with the unit's world position and
|
||||
// return it. Same call shape `FUN_005EC110` uses to read the
|
||||
// player / target positions when deciding interact range.
|
||||
struct C3Vector {
|
||||
float x, y, z;
|
||||
};
|
||||
using GetPosition_t = const C3Vector *(__thiscall *)(const void *self,
|
||||
C3Vector *outBuf);
|
||||
constexpr int VTBL_GET_POSITION_OFFSET = 0x14;
|
||||
|
||||
// Reads `(unit.m_objectFields[+0x224] & UNIT_DYNFLAG_LOOTABLE) != 0`
|
||||
// — the server-driven bit it sets when the local player has loot
|
||||
// rights on this unit. Works uniformly for live and dead CGUnit
|
||||
// subclasses; the bit is simply 0 for non-lootable targets.
|
||||
//
|
||||
// Don't call `FUN_CGCORPSE_IS_LOOTABLE` here even though the engine
|
||||
// itself uses it in `FUN_005D6BF0`: that helper reads `+0x78` of
|
||||
// m_objectFields, which is the CORPSE dynamic-flags slot for
|
||||
// CGCorpse_C but the BYTES_0 (race / class / gender / power type)
|
||||
// slot for CGUnit_C. Routing live units through it returns the low
|
||||
// bit of *race* — yields false positives for any Human / Dwarf /
|
||||
// Undead / Gnome player and false negatives for dead mobs.
|
||||
bool IsLootable(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
const uint32_t flags =
|
||||
Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS);
|
||||
return (flags & Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
|
||||
}
|
||||
|
||||
// Reads the bounding-radius float from a unit's m_objectFields. The
|
||||
// engine uses this as the contribution from both sides (player + target)
|
||||
// in its interact-range computation.
|
||||
float BoundingRadius(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return Game::Read<float>(fields, Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
}
|
||||
|
||||
// Invokes the unit's virtual `GetPosition` (vtable slot at offset
|
||||
// `0x14`). The engine's signature is `__thiscall(this, outBuf) →
|
||||
// outBuf`; we pass our own 12-byte stack buffer and read x/y/z out
|
||||
// of the returned pointer.
|
||||
const C3Vector *GetPosition(const void *unit, C3Vector *outBuf) {
|
||||
auto vtable = *reinterpret_cast<void *const *const *>(unit);
|
||||
auto fn = reinterpret_cast<GetPosition_t>(
|
||||
*reinterpret_cast<const void *const *>(
|
||||
reinterpret_cast<const uint8_t *>(vtable) +
|
||||
VTBL_GET_POSITION_OFFSET));
|
||||
return fn(unit, outBuf);
|
||||
}
|
||||
|
||||
// Mirrors the range portion of `FUN_005EC110`. We skip the engine's
|
||||
// player-state checks (mounted / casting / prior-interaction GUID
|
||||
// gate) because they fire intermittently during normal play and would
|
||||
// make the enumerator return an empty list whenever the player has
|
||||
// already clicked something — surprising for a "what's lootable
|
||||
// nearby" reader. The pure distance test is what controls cursor
|
||||
// highlight, which is the contract the user asked for.
|
||||
bool InInteractRange(const void *player, const void *target) {
|
||||
C3Vector pBuf{};
|
||||
C3Vector tBuf{};
|
||||
const C3Vector *pPos = GetPosition(player, &pBuf);
|
||||
const C3Vector *tPos = GetPosition(target, &tBuf);
|
||||
if (pPos == nullptr || tPos == nullptr)
|
||||
return false;
|
||||
|
||||
const float dx = pPos->x - tPos->x;
|
||||
const float dy = pPos->y - tPos->y;
|
||||
const float dz = pPos->z - tPos->z;
|
||||
const float distSq = dx * dx + dy * dy + dz * dz;
|
||||
|
||||
float range = BoundingRadius(player) + BoundingRadius(target) + INTERACT_REACH;
|
||||
if (range < MIN_INTERACT_RANGE)
|
||||
range = MIN_INTERACT_RANGE;
|
||||
return distSq <= range * range;
|
||||
}
|
||||
|
||||
// Only `TYPEMASK_UNIT` carries lootable-mob state in 1.12 — dead
|
||||
// creatures stay as CGCreature_C objects with the lootable bit flipped
|
||||
// on, rather than transitioning to a separate corpse object. Dead
|
||||
@@ -135,7 +42,7 @@ bool InInteractRange(const void *player, const void *target) {
|
||||
|
||||
struct ScanCtx {
|
||||
void *L;
|
||||
const void *player; // resolved once outside the iterator
|
||||
void *player; // resolved once outside the iterator
|
||||
int idx; // 1-based slot of the next entry to append
|
||||
};
|
||||
|
||||
@@ -144,10 +51,10 @@ int __fastcall ScanCallback(ScanCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
if (obj == nullptr)
|
||||
return 1; // GUID isn't a unit; continue
|
||||
|
||||
if (!IsLootable(obj))
|
||||
if (!Request::IsLootable(obj))
|
||||
return 1; // unit isn't lootable to us; continue
|
||||
|
||||
if (!InInteractRange(ctx->player, obj))
|
||||
if (!Request::InReach(ctx->player, obj))
|
||||
return 1; // out of click-loot range; continue
|
||||
|
||||
char guidBuf[Guid::STRING_SIZE];
|
||||
@@ -180,7 +87,7 @@ int __fastcall Script_GetNearbyLootableUnits(void *L) {
|
||||
// Resolve the local player once — the canonical CGPlayer_C the
|
||||
// engine's own interact-range checks use, distinct from the
|
||||
// `VAR_LOCAL_PLAYER_PTR` global. Bail if unavailable.
|
||||
const void *player = Game::ResolveUnitToken("player");
|
||||
void *player = Game::ResolveUnitToken("player");
|
||||
if (player == nullptr)
|
||||
return 1;
|
||||
|
||||
|
||||
+35
-2
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "../unit/Position.h"
|
||||
|
||||
namespace Loot::Request {
|
||||
|
||||
@@ -31,8 +32,11 @@ using CloseLootInner_t = void(__fastcall *)(int sendRelease,
|
||||
char earlyReturnOnGameObject,
|
||||
char showError);
|
||||
|
||||
uint64_t HeldTarget(const void *player) {
|
||||
return Game::Read<uint64_t>(player, Offsets::OFF_PLAYER_LOOT_TARGET_GUID);
|
||||
// The per-unit reach `CanLootNow` adds for each side (`[m_objectFields +
|
||||
// 0x1F0]`).
|
||||
float Reach(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return Game::Read<float>(fields, Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -42,6 +46,35 @@ bool IsMounted(const void *player) {
|
||||
return Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_MOUNTDISPLAYID) != 0;
|
||||
}
|
||||
|
||||
bool IsLootable(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return (Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS) &
|
||||
Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
|
||||
}
|
||||
|
||||
bool InReach(void *player, void *target) {
|
||||
float p[3];
|
||||
float t[3];
|
||||
if (!Unit::Position::Read(player, p) || !Unit::Position::Read(target, t))
|
||||
return false;
|
||||
// `CanLootNow`'s reach for a unit (`0x005EC142`..`0x005EC17E`), with the
|
||||
// engine's own constants read live.
|
||||
float reach = Reach(target) + Reach(player) +
|
||||
Game::Read<float>(static_cast<uintptr_t>(Offsets::VAR_MELEE_REACH_LEEWAY));
|
||||
const float minReach =
|
||||
Game::Read<float>(static_cast<uintptr_t>(Offsets::VAR_MELEE_REACH_MIN));
|
||||
if (reach < minReach)
|
||||
reach = minReach;
|
||||
const float dx = p[0] - t[0];
|
||||
const float dy = p[1] - t[1];
|
||||
const float dz = p[2] - t[2];
|
||||
return dx * dx + dy * dy + dz * dz <= reach * reach;
|
||||
}
|
||||
|
||||
uint64_t HeldTarget(const void *player) {
|
||||
return Game::Read<uint64_t>(player, Offsets::OFF_PLAYER_LOOT_TARGET_GUID);
|
||||
}
|
||||
|
||||
bool Send(void *player, void *target, uint64_t guid) {
|
||||
if (IsMounted(player))
|
||||
return false;
|
||||
|
||||
+17
-2
@@ -28,11 +28,26 @@ namespace Loot::Request {
|
||||
// (`CGUnit_C::Interact`, `0x0060BEA0`) applies before its loot leg.
|
||||
bool IsMounted(const void *player);
|
||||
|
||||
// Whether the server grants us loot on `unit` (`UNIT_DYNFLAG_LOOTABLE`).
|
||||
bool IsLootable(const void *unit);
|
||||
|
||||
// Whether `target` is within the reach `CanLootNow` (`0x005EC110`) demands
|
||||
// when the player is not walked up: both units' `+0x1F0` reach plus the
|
||||
// engine's live leeway, at least its live minimum, against the 3D distance.
|
||||
// `Send` never walks, so a target outside this reach is one the server
|
||||
// refuses with "too far away".
|
||||
bool InReach(void *player, void *target);
|
||||
|
||||
// The player's held loot target. The engine writes it when the request goes
|
||||
// out and clears it when the server answers with a failure or a release, so
|
||||
// a caller waiting on `guid` sees the refusal as this no longer being `guid`.
|
||||
uint64_t HeldTarget(const void *player);
|
||||
|
||||
// Sends the loot request the way a right-click would: refused while mounted
|
||||
// (the click's gate, which `FUN_CMSG_LOOT_UNIT` itself lacks), never
|
||||
// auto-walks. Returns whether the request went out — false when mounted or
|
||||
// when the engine's own pre-checks refused (another loot held, falling, not
|
||||
// in control, ...), in which case no response is coming.
|
||||
// when the engine's own pre-checks refused (another loot held, moving,
|
||||
// falling, not in control, ...), in which case no response is coming.
|
||||
bool Send(void *player, void *target, uint64_t guid);
|
||||
|
||||
// For a caller that gave up waiting: if the player still holds `guid` as
|
||||
|
||||
+121
-75
@@ -34,6 +34,15 @@
|
||||
// state machine — we no longer need `WaitingOpen` / `WaitingClose` /
|
||||
// per-tick `VAR_LOOT_GUID` reads, because the controller hook *is* the
|
||||
// state transition.
|
||||
//
|
||||
// Loot mode remembers corpses whose window opened EMPTY but which stay
|
||||
// LOOTABLE: the server keeps the flag while any loot is left (another
|
||||
// member's quest item, condition-gated loot) yet never sends this player a
|
||||
// slot it can't take, so an empty window is the server's own "nothing here
|
||||
// for you". Later walks skip such a corpse. A corpse whose window held
|
||||
// anything (bags full, a roll or master-loot assignment in progress) is not
|
||||
// remembered and is retried, because those refusals clear on their own.
|
||||
// See `s_emptyCorpses` for when an entry is forgotten.
|
||||
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
@@ -42,8 +51,10 @@
|
||||
#include "../guid/Guid.h"
|
||||
#include "../object/Resolve.h"
|
||||
#include "../tick/WorldTick.h"
|
||||
#include "../time/Clock.h"
|
||||
#include "Request.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -55,12 +66,11 @@ namespace {
|
||||
constexpr const char *kEventCompleted = "LOOT_SCAN_COMPLETED";
|
||||
const Event::Custom::AutoReserve _reserve{kEventCompleted};
|
||||
|
||||
// Per-step timeout for the controller hook to fire on a queued
|
||||
// CMSG_LOOT. WorldTick fires at the client frame rate (~30-60 Hz),
|
||||
// so 180 ticks ≈ 3-6s wall clock. Long enough for high-latency
|
||||
// servers, short enough that a single dropped response doesn't hang
|
||||
// the whole scan.
|
||||
constexpr int kStepTimeoutTicks = 180;
|
||||
// How long one step may wait: for the server to answer a CMSG_LOOT it
|
||||
// never answers, or for the engine to accept a send it keeps refusing
|
||||
// (the player is moving). Long enough for high-latency servers, short
|
||||
// enough that a dropped response doesn't hang the walk.
|
||||
constexpr uint32_t kStepTimeoutMs = 3000;
|
||||
|
||||
// Event IDs the engine fires through `FUN_FIRE_EVENT_NO_ARGS` for
|
||||
// loot-window transitions. The hook swallows these only when the
|
||||
@@ -68,9 +78,12 @@ constexpr int kStepTimeoutTicks = 180;
|
||||
// fire unmodified.
|
||||
constexpr int EVENT_LOOT_OPENED = 0x10B;
|
||||
constexpr int EVENT_LOOT_CLOSED = 0x10D;
|
||||
// Observed, never swallowed (see `s_emptyCorpses`).
|
||||
constexpr int EVENT_QUEST_LOG_UPDATE = 0x134;
|
||||
|
||||
enum class State {
|
||||
Idle,
|
||||
WaitingToSend, // the engine refused the send for now; retried each tick
|
||||
WaitingForResponse, // CMSG_LOOT sent; awaiting our controller hook
|
||||
};
|
||||
|
||||
@@ -90,7 +103,7 @@ struct ScanCtx {
|
||||
State state = State::Idle;
|
||||
void *player = nullptr;
|
||||
uint64_t currentGuid = 0;
|
||||
int ticksSinceTransition = 0;
|
||||
uint32_t stepStartMs = 0;
|
||||
bool suppressEvents = false; // read by the FireEvent hook
|
||||
bool lootMode = false; // LootAllCorpses (true) vs ScanNearbyLoot (false)
|
||||
std::vector<uint64_t> queue;
|
||||
@@ -99,6 +112,21 @@ struct ScanCtx {
|
||||
|
||||
ScanCtx s_scan;
|
||||
|
||||
// Loot-mode corpses whose window opened empty while the unit stayed
|
||||
// LOOTABLE (see the file comment). Outlives a walk. An entry is forgotten:
|
||||
// - on the first world tick where its unit is gone (despawned, out of
|
||||
// view) or no longer LOOTABLE (alive again after a respawn
|
||||
// with the same GUID, or emptied by someone else), so a corpse back in
|
||||
// view or a fresh corpse is opened again;
|
||||
// - all at once on QUEST_LOG_UPDATE: quest items are per-player loot, so
|
||||
// a quest change can turn an empty window into one with an item in it.
|
||||
std::vector<uint64_t> s_emptyCorpses;
|
||||
|
||||
bool IsKnownEmpty(uint64_t guid) {
|
||||
return std::find(s_emptyCorpses.begin(), s_emptyCorpses.end(), guid) !=
|
||||
s_emptyCorpses.end();
|
||||
}
|
||||
|
||||
// === Typedefs for engine helpers ===
|
||||
|
||||
using ClntObjMgrEnumVisibleObjectsCallback_t = int(__fastcall *)(void *ctx,
|
||||
@@ -128,54 +156,18 @@ using LootMoney_t = void(__fastcall *)(void *player);
|
||||
using LootController_t = void(__fastcall *)(int lootStructPtr,
|
||||
int coin,
|
||||
int unk);
|
||||
struct C3Vector {
|
||||
float x, y, z;
|
||||
};
|
||||
using GetPosition_t = const C3Vector *(__thiscall *)(const void *self,
|
||||
C3Vector *outBuf);
|
||||
constexpr int VTBL_GET_POSITION_OFFSET = 0x14;
|
||||
|
||||
constexpr float INTERACT_REACH = 1.3333333f;
|
||||
constexpr float MIN_INTERACT_RANGE = 5.0f;
|
||||
|
||||
// === Range / lootability helpers (identical to Loot::Nearby) ===
|
||||
|
||||
bool IsLootableUnit(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
const uint32_t flags =
|
||||
Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS);
|
||||
return (flags & Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
|
||||
}
|
||||
|
||||
float BoundingRadius(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return Game::Read<float>(fields, Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
}
|
||||
|
||||
const C3Vector *GetPosition(const void *unit, C3Vector *outBuf) {
|
||||
auto vtable = *reinterpret_cast<void *const *const *>(unit);
|
||||
auto fn = reinterpret_cast<GetPosition_t>(
|
||||
*reinterpret_cast<const void *const *>(
|
||||
reinterpret_cast<const uint8_t *>(vtable) +
|
||||
VTBL_GET_POSITION_OFFSET));
|
||||
return fn(unit, outBuf);
|
||||
}
|
||||
|
||||
bool InInteractRange(const void *player, const void *target) {
|
||||
C3Vector pBuf{};
|
||||
C3Vector tBuf{};
|
||||
const C3Vector *pPos = GetPosition(player, &pBuf);
|
||||
const C3Vector *tPos = GetPosition(target, &tBuf);
|
||||
if (pPos == nullptr || tPos == nullptr)
|
||||
return false;
|
||||
const float dx = pPos->x - tPos->x;
|
||||
const float dy = pPos->y - tPos->y;
|
||||
const float dz = pPos->z - tPos->z;
|
||||
const float distSq = dx * dx + dy * dy + dz * dz;
|
||||
float range = BoundingRadius(player) + BoundingRadius(target) + INTERACT_REACH;
|
||||
if (range < MIN_INTERACT_RANGE)
|
||||
range = MIN_INTERACT_RANGE;
|
||||
return distSq <= range * range;
|
||||
// Run every world tick, so no gone-and-back or respawn-and-die-again
|
||||
// can happen between two checks.
|
||||
void PruneEmptyCorpses() {
|
||||
s_emptyCorpses.erase(
|
||||
std::remove_if(s_emptyCorpses.begin(), s_emptyCorpses.end(),
|
||||
[](uint64_t guid) {
|
||||
void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT,
|
||||
guid, nullptr, 0);
|
||||
return unit == nullptr || !Request::IsLootable(unit);
|
||||
}),
|
||||
s_emptyCorpses.end());
|
||||
}
|
||||
|
||||
// === Scrape ===
|
||||
@@ -278,17 +270,37 @@ void TryStartNext() {
|
||||
StartLoot(guid);
|
||||
}
|
||||
|
||||
void StartLoot(uint64_t guid) {
|
||||
void *target = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0);
|
||||
// Not sent (mounted mid-walk, or the engine's pre-checks refused): no
|
||||
// response is coming, so skip without waiting out the timeout.
|
||||
if (target == nullptr || !Request::Send(s_scan.player, target, guid)) {
|
||||
void BeginStep(State state) {
|
||||
s_scan.state = state;
|
||||
s_scan.stepStartMs = Time::Clock::NowMs();
|
||||
}
|
||||
|
||||
// Sends the current corpse's loot request. The corpse is checked again
|
||||
// here, not only when the queue was built: the walk takes a round trip per
|
||||
// corpse, and by now the corpse may be gone, looted, or out of reach
|
||||
// because the player moved. The server refuses an out-of-reach corpse
|
||||
// with an error, so skip it instead.
|
||||
//
|
||||
// When the engine refuses the send itself (the player is moving or
|
||||
// falling, ...), the corpse stays current and `OnTick` retries it: the
|
||||
// refusal is about the player, not the corpse, so skipping would drop
|
||||
// every corpse left in the queue.
|
||||
void SendCurrent() {
|
||||
void *target =
|
||||
Object::ByGuid(Offsets::TYPEMASK_UNIT, s_scan.currentGuid, nullptr, 0);
|
||||
if (target == nullptr || !Request::IsLootable(target) ||
|
||||
!Request::InReach(s_scan.player, target)) {
|
||||
TryStartNext();
|
||||
return;
|
||||
}
|
||||
if (Request::Send(s_scan.player, target, s_scan.currentGuid))
|
||||
BeginStep(State::WaitingForResponse);
|
||||
}
|
||||
|
||||
void StartLoot(uint64_t guid) {
|
||||
s_scan.currentGuid = guid;
|
||||
s_scan.state = State::WaitingForResponse;
|
||||
s_scan.ticksSinceTransition = 0;
|
||||
BeginStep(State::WaitingToSend);
|
||||
SendCurrent();
|
||||
}
|
||||
|
||||
// === Hooks ===
|
||||
@@ -353,6 +365,10 @@ void __fastcall LootController_h(int lootStructPtr, int coin, int unk) {
|
||||
LootEntry entry;
|
||||
entry.guid = s_scan.currentGuid;
|
||||
ScrapeCurrentLoot(&entry);
|
||||
// Each corpse is queued once per walk and known-empty ones are not
|
||||
// queued in loot mode, so this never adds a duplicate.
|
||||
if (s_scan.lootMode && entry.coin == 0 && entry.items.empty())
|
||||
s_emptyCorpses.push_back(entry.guid);
|
||||
s_scan.results.push_back(std::move(entry));
|
||||
|
||||
// Loot mode: actually take everything (coin + all slots) before the
|
||||
@@ -383,18 +399,45 @@ const Game::HookAutoRegister _hookController{
|
||||
|
||||
const Event::SignalHook::AutoSubscribe _lootIntercept{&SuppressLootEvents};
|
||||
|
||||
// === Tick (timeout only) ===
|
||||
// Observes only: the event still fires.
|
||||
bool ForgetEmptyCorpsesOnQuestChange(int eventID) {
|
||||
if (eventID == EVENT_QUEST_LOG_UPDATE)
|
||||
s_emptyCorpses.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
const Event::SignalHook::AutoSubscribe _questIntercept{&ForgetEmptyCorpsesOnQuestChange};
|
||||
|
||||
// === Tick (memory upkeep, refusals and timeouts) ===
|
||||
|
||||
void OnTick() {
|
||||
PruneEmptyCorpses();
|
||||
if (s_scan.state == State::Idle)
|
||||
return;
|
||||
++s_scan.ticksSinceTransition;
|
||||
if (s_scan.ticksSinceTransition >= kStepTimeoutTicks) {
|
||||
// Controller hook never fired — the server answered with a
|
||||
// failure, or never answered. A failure already cleared the
|
||||
// held target; an unanswered request still holds it, which
|
||||
// would block all looting, so release it. Skip this corpse.
|
||||
s_scan.suppressEvents = false;
|
||||
const bool timedOut = Time::Clock::Elapsed(s_scan.stepStartMs) >= kStepTimeoutMs;
|
||||
|
||||
if (s_scan.state == State::WaitingToSend) {
|
||||
// Still refused: the player has not stopped moving, or mounted,
|
||||
// or lost control. Every corpse left would be refused the same
|
||||
// way, so end the walk.
|
||||
if (timedOut) {
|
||||
Complete();
|
||||
return;
|
||||
}
|
||||
SendCurrent();
|
||||
return;
|
||||
}
|
||||
|
||||
// The server refused the corpse ("too far away", not ours, ...): its
|
||||
// failure response and its release response both clear the held
|
||||
// target, and no window opens. Go on to the next corpse now.
|
||||
if (Request::HeldTarget(s_scan.player) != s_scan.currentGuid) {
|
||||
TryStartNext();
|
||||
return;
|
||||
}
|
||||
if (timedOut) {
|
||||
// No answer at all. The held target would block all looting, so
|
||||
// release it, and skip this corpse.
|
||||
Request::ReleaseIfUnanswered(s_scan.player, s_scan.currentGuid);
|
||||
TryStartNext();
|
||||
}
|
||||
@@ -405,27 +448,30 @@ const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
|
||||
// === Enumeration ===
|
||||
|
||||
struct EnumCtx {
|
||||
const void *player;
|
||||
void *player;
|
||||
std::vector<uint64_t> *out;
|
||||
bool skipKnownEmpty;
|
||||
};
|
||||
|
||||
int __fastcall EnumCallback(EnumCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
void *obj = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0);
|
||||
if (obj == nullptr)
|
||||
return 1;
|
||||
if (!IsLootableUnit(obj))
|
||||
if (!Request::IsLootable(obj))
|
||||
return 1;
|
||||
if (!InInteractRange(ctx->player, obj))
|
||||
if (ctx->skipKnownEmpty && IsKnownEmpty(guid))
|
||||
return 1;
|
||||
if (!Request::InReach(ctx->player, obj))
|
||||
return 1;
|
||||
ctx->out->push_back(guid);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void BuildQueue(void *player, size_t maxCount) {
|
||||
void BuildQueue(void *player, size_t maxCount, bool skipKnownEmpty) {
|
||||
s_scan.queue.clear();
|
||||
auto Enum = reinterpret_cast<ClntObjMgrEnumVisibleObjects_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS);
|
||||
EnumCtx ctx{player, &s_scan.queue};
|
||||
EnumCtx ctx{player, &s_scan.queue, skipKnownEmpty};
|
||||
Enum(reinterpret_cast<ClntObjMgrEnumVisibleObjectsCallback_t>(&EnumCallback),
|
||||
&ctx);
|
||||
// Cap to `maxCount` corpses (0 = unlimited). Order is engine
|
||||
@@ -451,7 +497,7 @@ bool BeginWalk(bool lootMode, size_t maxCount) {
|
||||
s_scan.results.clear();
|
||||
s_scan.player = player;
|
||||
s_scan.lootMode = lootMode;
|
||||
BuildQueue(player, maxCount);
|
||||
BuildQueue(player, maxCount, /*skipKnownEmpty=*/lootMode);
|
||||
|
||||
if (s_scan.queue.empty()) {
|
||||
Complete(); // nothing nearby — fire LOOT_SCAN_COMPLETED immediately
|
||||
|
||||
+31
-48
@@ -48,6 +48,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "model/M2.h"
|
||||
#include "time/Clock.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -58,11 +59,7 @@ namespace {
|
||||
|
||||
using PaneFn_t = void(__thiscall *)(void *pane);
|
||||
using SetModelInstance_t = void(__thiscall *)(void *frame, void *model);
|
||||
using PlayAnimation_t = void(__thiscall *)(void *model, int keyBone, uint32_t id, int variation,
|
||||
int offset, float speed, int blend, int primary);
|
||||
using GetKeyBoneAnim_t = uint32_t(__thiscall *)(void *model, int keyBone);
|
||||
using LookupIndex_t = uint16_t(__fastcall *)(uint32_t id, const void *table);
|
||||
using ModelLoaded_t = int(__thiscall *)(void *model, int tryLoad, int recurseChildren);
|
||||
|
||||
struct PaneState {
|
||||
bool doBlend = false;
|
||||
@@ -78,21 +75,13 @@ std::unordered_map<void *, PaneState> g_panes;
|
||||
PaneFn_t g_origUpdateModel = nullptr;
|
||||
SetModelInstance_t g_origSetModelInstance = nullptr;
|
||||
|
||||
void *PaneModel(void *pane) {
|
||||
return Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
|
||||
}
|
||||
|
||||
bool IsLoaded(void *model) {
|
||||
return reinterpret_cast<ModelLoaded_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(model, 0, 0) != 0;
|
||||
}
|
||||
|
||||
uint32_t KeyBoneAnim(void *model) {
|
||||
return reinterpret_cast<GetKeyBoneAnim_t>(Offsets::FUN_M2MODEL_GET_KEY_BONE_ANIM)(model, -1);
|
||||
}
|
||||
|
||||
void Arm(void *model, const PaneState &s) {
|
||||
reinterpret_cast<PlayAnimation_t>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(
|
||||
model, -1, s.animation, s.variation, 0, s.frozen ? 0.0f : 1.0f, s.doBlend ? 1 : 0, 1);
|
||||
M2::PlayAnimation(model, -1, s.animation, s.variation, 0, s.frozen ? 0.0f : 1.0f,
|
||||
s.doBlend ? 1 : 0, 1);
|
||||
// PlayAnimation stores the id only when it found a sequence to play.
|
||||
if (!s.frozen || KeyBoneAnim(model) != s.animation)
|
||||
return;
|
||||
@@ -109,11 +98,11 @@ void __fastcall UpdateModel_h(void *pane, void * /*edx*/) {
|
||||
}
|
||||
PaneState &s = it->second;
|
||||
reinterpret_cast<PaneFn_t>(Offsets::FUN_SIMPLEMODEL_UPDATE_MODEL)(pane);
|
||||
void *model = PaneModel(pane);
|
||||
void *model = Pane::Model(pane);
|
||||
const uint32_t deadline = Game::Read<uint32_t>(pane, Offsets::OFF_PLAYERMODEL_TURN_DEADLINE);
|
||||
if (Game::Read<int>(pane, Offsets::OFF_PLAYERMODEL_TURNING) == 0 &&
|
||||
(deadline == 0 || Time::Clock::Reached(deadline)) && model != nullptr && IsLoaded(model) &&
|
||||
(s.pending || KeyBoneAnim(model) != s.animation)) {
|
||||
(deadline == 0 || Time::Clock::Reached(deadline)) && model != nullptr &&
|
||||
M2::IsLoaded(model, 0, 0) && (s.pending || KeyBoneAnim(model) != s.animation)) {
|
||||
Arm(model, s);
|
||||
s.pending = false;
|
||||
Game::Ref<uint32_t>(pane, Offsets::OFF_PLAYERMODEL_TURN_DEADLINE) = 0;
|
||||
@@ -135,29 +124,33 @@ const Game::HookAutoRegister _setModelHook{Offsets::FUN_SIMPLEMODEL_SET_MODEL_IN
|
||||
reinterpret_cast<void *>(&SetModelInstance_h),
|
||||
reinterpret_cast<void **>(&g_origSetModelInstance)};
|
||||
|
||||
void *ResolvePane(void *L) {
|
||||
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
|
||||
// SetAnimation / FreezeAnimation: the pane rests on `animation`, re-armed on
|
||||
// the next update.
|
||||
void Rest(void *pane, uint32_t animation, int variation, bool frozen, uint32_t frame) {
|
||||
PaneState &s = g_panes[pane];
|
||||
s.resting = true;
|
||||
s.pending = true;
|
||||
s.frozen = frozen;
|
||||
s.animation = animation;
|
||||
s.variation = variation;
|
||||
s.frame = frame;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetAnimation(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
void *pane = Game::Lua::ResolvePlayerModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: SetAnimation(animation [, variation])");
|
||||
return 0;
|
||||
}
|
||||
PaneState &s = g_panes[pane];
|
||||
s.resting = true;
|
||||
s.pending = true;
|
||||
s.frozen = false;
|
||||
s.animation = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
|
||||
s.variation = Game::Lua::IsNumber(L, 3) ? static_cast<int>(Game::Lua::ToNumber(L, 3)) : 0;
|
||||
Rest(pane, static_cast<uint32_t>(Game::Lua::ToNumber(L, 2)),
|
||||
Game::Lua::IsNumber(L, 3) ? static_cast<int>(Game::Lua::ToNumber(L, 3)) : 0, false, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_FreezeAnimation(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
void *pane = Game::Lua::ResolvePlayerModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3) || !Game::Lua::IsNumber(L, 4)) {
|
||||
@@ -165,24 +158,20 @@ int __fastcall Script_FreezeAnimation(void *L) {
|
||||
return 0;
|
||||
}
|
||||
const double frame = Game::Lua::ToNumber(L, 4);
|
||||
PaneState &s = g_panes[pane];
|
||||
s.resting = true;
|
||||
s.pending = true;
|
||||
s.frozen = true;
|
||||
s.animation = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
|
||||
s.variation = static_cast<int>(Game::Lua::ToNumber(L, 3));
|
||||
s.frame = frame > 0.0 ? static_cast<uint32_t>(frame) : 0;
|
||||
Rest(pane, static_cast<uint32_t>(Game::Lua::ToNumber(L, 2)),
|
||||
static_cast<int>(Game::Lua::ToNumber(L, 3)), true,
|
||||
frame > 0.0 ? static_cast<uint32_t>(frame) : 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetDoBlend(void *L) {
|
||||
if (void *pane = ResolvePane(L))
|
||||
if (void *pane = Game::Lua::ResolvePlayerModel(L))
|
||||
g_panes[pane].doBlend = Game::Lua::ToBoolean(L, 2) != 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetDoBlend(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
void *pane = Game::Lua::ResolvePlayerModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
const auto it = g_panes.find(pane);
|
||||
@@ -191,7 +180,7 @@ int __fastcall Script_GetDoBlend(void *L) {
|
||||
}
|
||||
|
||||
int __fastcall Script_HasAnimation(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
void *pane = Game::Lua::ResolvePlayerModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
@@ -200,15 +189,10 @@ int __fastcall Script_HasAnimation(void *L) {
|
||||
}
|
||||
const auto id = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
|
||||
bool has = false;
|
||||
void *model = PaneModel(pane);
|
||||
if (model != nullptr && IsLoaded(model)) {
|
||||
const auto *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_ANIMATION_LOOKUP);
|
||||
has = index < Game::Read<uint32_t>(data, Offsets::OFF_M2DATA_SEQUENCE_COUNT);
|
||||
}
|
||||
void *model = Pane::Model(pane);
|
||||
if (model != nullptr && M2::IsLoaded(model, 0, 0))
|
||||
has = M2::LookupIndex(model, Offsets::OFF_M2DATA_ANIMATION_LOOKUP, id) <
|
||||
Game::Read<uint32_t>(M2::Data(model), Offsets::OFF_M2DATA_SEQUENCE_COUNT);
|
||||
Game::Lua::PushBool(L, has);
|
||||
return 1;
|
||||
}
|
||||
@@ -265,8 +249,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_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])));
|
||||
g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+15
-33
@@ -41,6 +41,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "model/M2.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
@@ -55,19 +56,13 @@ struct Settings {
|
||||
};
|
||||
std::unordered_map<void *, Settings> g_settings;
|
||||
|
||||
using GetVector_t = void(__fastcall *)(void *camera, uint32_t id, float *out);
|
||||
using GetScalar_t = float(__fastcall *)(void *camera, uint32_t id);
|
||||
using SetVector_t = void(__fastcall *)(void *camera, uint32_t id, const float *value,
|
||||
uint32_t keep);
|
||||
using SetScalar_t = void(__fastcall *)(void *camera, uint32_t id, float value);
|
||||
using GetCameraCount_t = uint32_t(__thiscall *)(void *model);
|
||||
using GetCamera_t = void *(__thiscall *)(void *model, uint32_t index);
|
||||
using LookupIndex_t = uint16_t(__fastcall *)(uint32_t id, const void *table);
|
||||
using SetAnimated_t = void(__thiscall *)(void *model, int animated);
|
||||
using GetScene_t = void *(__thiscall *)(void *frame);
|
||||
using SceneAnimate_t = void(__thiscall *)(void *scene, const float *cameraPos);
|
||||
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
|
||||
@@ -76,27 +71,21 @@ using ConfigureCamera_t = void(__thiscall *)(void *pane);
|
||||
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);
|
||||
const uint16_t index = M2::LookupIndex(model, Offsets::OFF_M2DATA_CAMERA_ID_LOOKUP, id);
|
||||
if (index >= count)
|
||||
return nullptr;
|
||||
return reinterpret_cast<GetCamera_t>(Offsets::FUN_M2MODEL_GET_CAMERA)(model, index);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct Vec3 {
|
||||
float x, y, z;
|
||||
};
|
||||
|
||||
Vec3 GetVector(void *camera, uint32_t id) {
|
||||
using Fn = void(__fastcall *)(void *camera, uint32_t id, float *out);
|
||||
Vec3 v{};
|
||||
reinterpret_cast<GetVector_t>(Offsets::FUN_CAMERA_GET_VECTOR)(camera, id, &v.x);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_CAMERA_GET_VECTOR)(camera, id, &v.x);
|
||||
return v;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void SetVector(void *camera, uint32_t id, const Vec3 &v) {
|
||||
reinterpret_cast<SetVector_t>(Offsets::FUN_CAMERA_SET_VECTOR)(camera, id, &v.x, 0);
|
||||
}
|
||||
@@ -117,10 +106,9 @@ void AnimateUnrotated(void *pane, void *model) {
|
||||
facing = 0.0f;
|
||||
const void *vtable = Game::Read<const void *>(pane, 0);
|
||||
Game::Read<UpdateModel_t>(vtable, Offsets::VTBL_SIMPLEMODEL_UPDATE_MODEL)(pane);
|
||||
reinterpret_cast<SetAnimated_t>(Offsets::FUN_M2MODEL_SET_ANIMATED)(model, 1);
|
||||
M2::SetAnimated(model, true);
|
||||
const float origin[3] = {0.0f, 0.0f, 0.0f};
|
||||
void *scene = reinterpret_cast<GetScene_t>(Offsets::FUN_SIMPLEMODEL_GET_SCENE)(pane);
|
||||
reinterpret_cast<SceneAnimate_t>(Offsets::FUN_M2SCENE_ANIMATE)(scene, origin);
|
||||
M2::AnimateScene(Pane::Scene(pane), origin);
|
||||
facing = saved;
|
||||
}
|
||||
|
||||
@@ -131,7 +119,7 @@ Vec3 Lerp(const Vec3 &from, const Vec3 &to, float t) {
|
||||
|
||||
// 4.3.4 ConfigureCamera's last two steps, on the camera vanilla installed.
|
||||
void ApplySettings(void *pane, const Settings &s) {
|
||||
void *model = Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
|
||||
void *model = Pane::Model(pane);
|
||||
void *camera = Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODEL_CAMERA);
|
||||
if (model == nullptr || camera == nullptr)
|
||||
return;
|
||||
@@ -190,19 +178,14 @@ const Game::HookAutoRegister _cameraHook{
|
||||
// be loaded: ConfigureCamera reads its cameras and bounds, and an unloaded
|
||||
// model is configured by the ready hook when it arrives, settings included.
|
||||
void Reconfigure(void *pane) {
|
||||
void *model = Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
|
||||
if (model == nullptr ||
|
||||
!reinterpret_cast<ModelLoaded_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(model, 0, 0))
|
||||
void *model = Pane::Model(pane);
|
||||
if (model == nullptr || !M2::IsLoaded(model, 0, 0))
|
||||
return;
|
||||
reinterpret_cast<ConfigureCamera_t>(Offsets::FUN_PLAYERMODEL_CONFIGURE_CAMERA)(pane);
|
||||
}
|
||||
|
||||
void *ResolvePane(void *L) {
|
||||
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
|
||||
}
|
||||
|
||||
int __fastcall Script_SetPortraitZoom(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
void *pane = Game::Lua::ResolvePlayerModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
@@ -219,7 +202,7 @@ int __fastcall Script_SetPortraitZoom(void *L) {
|
||||
}
|
||||
|
||||
int __fastcall Script_SetCamDistanceScale(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
void *pane = Game::Lua::ResolvePlayerModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
@@ -241,7 +224,7 @@ int __fastcall Script_SetCamDistanceScale(void *L) {
|
||||
}
|
||||
|
||||
int __fastcall Script_RefreshCamera(void *L) {
|
||||
if (void *pane = ResolvePane(L))
|
||||
if (void *pane = Game::Lua::ResolvePlayerModel(L))
|
||||
Reconfigure(pane);
|
||||
return 0;
|
||||
}
|
||||
@@ -274,8 +257,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_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])));
|
||||
g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -14,7 +14,14 @@
|
||||
|
||||
namespace Model::Camera {
|
||||
|
||||
struct Vec3 {
|
||||
float x, y, z;
|
||||
};
|
||||
|
||||
// The M2 model's camera with id `id` (0 = portrait), or null when it has none.
|
||||
void *CameraById(void *model, uint32_t id);
|
||||
|
||||
// A camera's vector property (Offsets::CAMERA_PROP_EYE / _TARGET).
|
||||
Vec3 GetVector(void *camera, uint32_t id);
|
||||
|
||||
} // namespace Model::Camera
|
||||
|
||||
+54
-106
@@ -55,6 +55,8 @@
|
||||
#include "dbc/Lookup.h"
|
||||
#include "model/DisplayInfo.h"
|
||||
#include "model/ItemDisplay.h"
|
||||
#include "model/M2.h"
|
||||
#include "model/Teardown.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -96,8 +98,6 @@ using CharCompDestroy_t = void(__fastcall *)(void *builder);
|
||||
using CharCompSetInfo_t = char(__thiscall *)(void *builder, const CharCompInfo *info);
|
||||
using CharCompSetItem_t = void(__thiscall *)(void *builder, int slot, int itemDisplayID);
|
||||
using CharCompPump_t = char(__thiscall *)(void *builder, int *unused);
|
||||
using ModelAddRef_t = void(__fastcall *)(void *instance);
|
||||
using ModelLoaded_t = int(__thiscall *)(void *instance, int tryLoad, int recurseChildren);
|
||||
|
||||
// A pending / finished dress for one Model frame. The model loads async, and
|
||||
// the engine only dresses a LOADED model (its poll gate FUN_00607da0 checks
|
||||
@@ -107,100 +107,62 @@ using ModelLoaded_t = int(__thiscall *)(void *instance, int tryLoad, int recurse
|
||||
// lifetime, and it holds the ref on the model instance — until the frame
|
||||
// loads another display or the reload teardown.
|
||||
struct DressJob {
|
||||
void *frame = nullptr; // CSimpleModelFFX that requested the dress
|
||||
void *instance = nullptr; // model instance the job is bound to
|
||||
void *instance = nullptr; // model instance the job is bound to
|
||||
const uint8_t *extraRec = nullptr; // CreatureDisplayInfoExtra (DBC memory, stable)
|
||||
void *builder = nullptr; // engine compositor (owned)
|
||||
void *builder = nullptr; // engine compositor (owned)
|
||||
bool configured = false;
|
||||
bool done = false;
|
||||
int ticks = 0;
|
||||
};
|
||||
|
||||
// Model frames are few and permanent; SetDisplayInfo users fewer still.
|
||||
DressJob g_jobs[8];
|
||||
std::unordered_map<void *, DressJob> g_jobs; // by frame
|
||||
// Give up on a model that never finishes loading (bad path, missing file).
|
||||
constexpr int kMaxDressTicks = 600;
|
||||
|
||||
void CancelJob(DressJob &job) {
|
||||
DestroyDresser(job.builder);
|
||||
job = DressJob{};
|
||||
void CancelJob(void *frame) {
|
||||
const auto it = g_jobs.find(frame);
|
||||
if (it == g_jobs.end())
|
||||
return;
|
||||
DestroyDresser(it->second.builder);
|
||||
g_jobs.erase(it);
|
||||
}
|
||||
|
||||
DressJob *FindJob(void *frame) {
|
||||
for (auto &job : g_jobs)
|
||||
if (job.frame == frame)
|
||||
return &job;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Claim a slot for `frame`: its own existing slot, a free one, or — all busy —
|
||||
// a finished one (the dressed model keeps its geosets and texture refs; only
|
||||
// the kept-alive builder is dropped early).
|
||||
DressJob *ClaimJob(void *frame) {
|
||||
DressJob *slot = FindJob(frame);
|
||||
if (slot == nullptr)
|
||||
for (auto &job : g_jobs)
|
||||
if (job.frame == nullptr) {
|
||||
slot = &job;
|
||||
break;
|
||||
}
|
||||
if (slot == nullptr)
|
||||
for (auto &job : g_jobs)
|
||||
if (job.done) {
|
||||
slot = &job;
|
||||
break;
|
||||
}
|
||||
if (slot == nullptr)
|
||||
slot = &g_jobs[0];
|
||||
CancelJob(*slot);
|
||||
return slot;
|
||||
}
|
||||
|
||||
// Queue a dress for a character display just loaded into `frame`.
|
||||
void ScheduleDress(void *frame, const uint8_t *extraRec) {
|
||||
DressJob *job = ClaimJob(frame);
|
||||
job->frame = frame;
|
||||
job->instance = Game::Read<void *>(frame,
|
||||
Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
|
||||
job->extraRec = extraRec;
|
||||
}
|
||||
// The frame's scene is about to free the model its builder holds.
|
||||
const Model::Teardown::AutoSubscribe _teardown{&CancelJob};
|
||||
|
||||
// One WorldTick step for one job: wait for the load, then configure + feed
|
||||
// equipment (the engine's order: SetInfo, then the 10 Extra equipment slots),
|
||||
// then pump until done. Field-for-field mirror of FUN_005fb200's NPC branch.
|
||||
void StepJob(DressJob &job) {
|
||||
void *instance = Game::Read<void *>(job.frame,
|
||||
Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
|
||||
// equipment, then pump until done. False once the job is dead.
|
||||
bool StepJob(void *frame, DressJob &job) {
|
||||
void *instance = Pane::Model(frame);
|
||||
if (job.instance == nullptr)
|
||||
job.instance = instance;
|
||||
if (instance == nullptr || instance != job.instance || ++job.ticks > kMaxDressTicks) {
|
||||
CancelJob(job); // model swapped out from under us / never loaded
|
||||
return;
|
||||
}
|
||||
if (instance == nullptr || instance != job.instance || ++job.ticks > kMaxDressTicks)
|
||||
return false; // model swapped out from under us / never loaded
|
||||
|
||||
if (!job.configured) {
|
||||
if (!reinterpret_cast<ModelLoaded_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(instance, 0, 0))
|
||||
return;
|
||||
if (!M2::IsLoaded(instance, 0, 0))
|
||||
return true;
|
||||
job.builder = CreateDresser(instance, job.extraRec);
|
||||
if (job.builder == nullptr) {
|
||||
CancelJob(job);
|
||||
return;
|
||||
}
|
||||
|
||||
if (job.builder == nullptr)
|
||||
return false;
|
||||
// The helm/shoulder feeds attach child model instances.
|
||||
LightChildren(job.frame, instance);
|
||||
|
||||
LightChildren(frame, instance);
|
||||
job.configured = true;
|
||||
}
|
||||
|
||||
if (PumpDresser(job.builder))
|
||||
job.done = true; // builder kept — it owns the model ref for the dressed model
|
||||
return true;
|
||||
}
|
||||
|
||||
void OnWorldTick() {
|
||||
for (auto &job : g_jobs)
|
||||
if (job.frame != nullptr && !job.done)
|
||||
StepJob(job);
|
||||
for (auto it = g_jobs.begin(); it != g_jobs.end();) {
|
||||
if (it->second.done || StepJob(it->first, it->second)) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
DestroyDresser(it->second.builder);
|
||||
it = g_jobs.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe g_tick{&OnWorldTick};
|
||||
@@ -216,8 +178,7 @@ AttachDisplayModel_t g_origAttach = nullptr;
|
||||
void __fastcall AttachDisplayModel_h(void *pane, void * /*edx*/, const uint8_t *record) {
|
||||
// Any pending or kept dress for this pane is about to point at the wrong
|
||||
// model — drop it before the original swaps the instance.
|
||||
if (DressJob *job = FindJob(pane))
|
||||
CancelJob(*job);
|
||||
CancelJob(pane);
|
||||
|
||||
if (Model::ItemDisplay::Attach(pane, record))
|
||||
return;
|
||||
@@ -227,7 +188,7 @@ void __fastcall AttachDisplayModel_h(void *pane, void * /*edx*/, const uint8_t *
|
||||
return;
|
||||
if (const uint8_t *extraRec =
|
||||
DressRecord(Game::Read<uint32_t>(record, Offsets::OFF_CREATURE_DISPLAYID)))
|
||||
ScheduleDress(pane, extraRec);
|
||||
g_jobs[pane] = DressJob{Pane::Model(pane), extraRec};
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _attachHook{
|
||||
@@ -237,31 +198,20 @@ const Game::HookAutoRegister _attachHook{
|
||||
|
||||
// ---- Lua methods --------------------------------------------------------------
|
||||
|
||||
using SetDisplay_t = void(__thiscall *)(void *pane, const uint8_t *record);
|
||||
|
||||
void SetDisplay(void *pane, const uint8_t *record) {
|
||||
reinterpret_cast<SetDisplay_t>(Offsets::FUN_PLAYERMODEL_SET_DISPLAY)(pane, record);
|
||||
}
|
||||
|
||||
// The record a pane shows for SetDisplayInfo: only the display-ID field is
|
||||
// ever read. One per pane, never freed — the pane keeps the pointer and
|
||||
// rebuilds from it on every show, and panes live as long as the UI.
|
||||
struct DisplayRecord {
|
||||
uint8_t bytes[Offsets::OFF_CREATURE_DISPLAYID + sizeof(uint32_t)];
|
||||
};
|
||||
std::unordered_map<void *, DisplayRecord> g_displayRecords;
|
||||
// The record each pane shows for SetDisplayInfo.
|
||||
std::unordered_map<void *, Pane::DisplayRecord> g_displayRecords;
|
||||
|
||||
void ShowDisplayID(void *pane, uint32_t displayID) {
|
||||
if (displayID == 0)
|
||||
return;
|
||||
DisplayRecord &rec = g_displayRecords[pane];
|
||||
std::memset(rec.bytes, 0, sizeof(rec.bytes));
|
||||
std::memcpy(rec.bytes + Offsets::OFF_CREATURE_DISPLAYID, &displayID, sizeof(displayID));
|
||||
SetDisplay(pane, rec.bytes);
|
||||
Pane::DisplayRecord &rec = g_displayRecords[pane];
|
||||
rec = {};
|
||||
Game::Ref<uint32_t>(rec.bytes, Offsets::OFF_CREATURE_DISPLAYID) = displayID;
|
||||
Pane::SetDisplay(pane, rec.bytes);
|
||||
}
|
||||
|
||||
int __fastcall Script_SetDisplayInfo(void *L) {
|
||||
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
|
||||
void *pane = Game::Lua::ResolvePlayerModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
@@ -273,7 +223,7 @@ int __fastcall Script_SetDisplayInfo(void *L) {
|
||||
}
|
||||
|
||||
int __fastcall Script_SetCreature(void *L) {
|
||||
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
|
||||
void *pane = Game::Lua::ResolvePlayerModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
@@ -287,7 +237,7 @@ int __fastcall Script_SetCreature(void *L) {
|
||||
if (Game::Lua::IsNumber(L, 3))
|
||||
ShowDisplayID(pane, static_cast<uint32_t>(Game::Lua::ToNumber(L, 3)));
|
||||
else
|
||||
SetDisplay(pane, record);
|
||||
Pane::SetDisplay(pane, record);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -316,12 +266,19 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_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])));
|
||||
g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
// Frames are rebuilt on /reload and a new frame can reuse an old address. The
|
||||
// dress jobs are gone by now: each went with its frame (Model::Teardown).
|
||||
void PrepareForReload() {
|
||||
g_displayRecords.clear();
|
||||
}
|
||||
|
||||
const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The engine's own early-out: an Extra record with no bake name gets no
|
||||
@@ -371,7 +328,7 @@ void *CreateDresser(void *instance, const uint8_t *extraRec) {
|
||||
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);
|
||||
M2::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;
|
||||
@@ -391,14 +348,12 @@ void *CreateDresser(void *instance, const uint8_t *extraRec) {
|
||||
// black. Each child gets the frame's own light/fog callback; it reads the
|
||||
// frame's live light struct, so later SetLight calls reach them too.
|
||||
void LightChildren(void *frame, void *instance) {
|
||||
using SetRenderCb_t = void(__thiscall *)(void *instance, const void *cb, void *userData);
|
||||
auto setRenderCb = reinterpret_cast<SetRenderCb_t>(Offsets::FUN_MODEL_INSTANCE_SET_RENDER_CB);
|
||||
const void *lightFogCb =
|
||||
reinterpret_cast<const void *>(Offsets::FUN_SIMPLEMODELFFX_LIGHT_FOG_CB);
|
||||
for (auto *child = Game::Read<uint8_t *>(instance, Offsets::OFF_MODEL_INSTANCE_CHILD_HEAD);
|
||||
child != nullptr;
|
||||
child = Game::Read<uint8_t *>(child, Offsets::OFF_MODEL_INSTANCE_CHILD_NEXT))
|
||||
setRenderCb(child, lightFogCb, frame);
|
||||
M2::SetRenderCallback(child, lightFogCb, frame);
|
||||
}
|
||||
|
||||
bool PumpDresser(void *dresser) {
|
||||
@@ -410,11 +365,4 @@ void DestroyDresser(void *dresser) {
|
||||
reinterpret_cast<CharCompDestroy_t>(Offsets::FUN_CHARCOMP_DESTROY)(dresser);
|
||||
}
|
||||
|
||||
void PrepareForReload() {
|
||||
for (auto &job : g_jobs)
|
||||
CancelJob(job);
|
||||
}
|
||||
|
||||
static const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
|
||||
|
||||
} // namespace Model::DisplayInfo
|
||||
|
||||
@@ -31,9 +31,4 @@ void DestroyDresser(void *dresser);
|
||||
// `frame` gives its own model, so the children are lit like it.
|
||||
void LightChildren(void *frame, void *instance);
|
||||
|
||||
// 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.
|
||||
void PrepareForReload();
|
||||
|
||||
} // namespace Model::DisplayInfo
|
||||
|
||||
+79
-60
@@ -73,6 +73,8 @@
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "model/ItemDisplay.h"
|
||||
#include "model/M2.h"
|
||||
#include "object/Resolve.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -94,9 +96,7 @@ using TryOnItem_t = void(__thiscall *)(void *pane, uint32_t itemID);
|
||||
using SetModelInstance_t = void(__thiscall *)(void *pane, void *model);
|
||||
using ClearModel_t = void(__thiscall *)(void *pane);
|
||||
using GetHeldItemDisplay_t = const void *(__thiscall *)(void *unit, uint32_t hand);
|
||||
using ModelLoaded_t = int(__thiscall *)(void *model, int tryLoad, int recurseChildren);
|
||||
using FindChild_t = void *(__thiscall *)(void *model, int attachId);
|
||||
using AttachItemVisuals_t = void(__fastcall *)(void *model, uint32_t visualID);
|
||||
using GetVisibleItem_t = const void *(__thiscall *)(void *player, uint32_t slot);
|
||||
using GetDescVisual_t = int(__thiscall *)(const void *visibleItem);
|
||||
using DisplayError_t = void(__cdecl *)(uint32_t id, ...);
|
||||
@@ -104,8 +104,6 @@ using DisplayError_t = void(__cdecl *)(uint32_t id, ...);
|
||||
// Body slots of the two held lanes.
|
||||
constexpr uint32_t kBodySlotMainHand = 0xF;
|
||||
constexpr uint32_t kBodySlotOffHand = 0x10;
|
||||
constexpr uint8_t kInvTypeShield = 14;
|
||||
constexpr uint8_t kInvTypeRangedRight = 26;
|
||||
|
||||
// Retail's Enum.ItemTryOnReason.
|
||||
constexpr int kTryOnSuccess = 0;
|
||||
@@ -113,6 +111,16 @@ constexpr int kTryOnWrongRace = 1;
|
||||
constexpr int kTryOnNotEquippable = 2;
|
||||
constexpr int kTryOnDataPending = 3;
|
||||
|
||||
// A held lane (OFF_DRESSUPMODEL_*_HAND_LANE).
|
||||
struct HeldLane {
|
||||
uint8_t itemClass; // 0 = empty
|
||||
uint8_t unknown1[2];
|
||||
uint8_t inventoryType;
|
||||
uint8_t sheatheType;
|
||||
uint8_t unknown5[3];
|
||||
};
|
||||
static_assert(sizeof(HeldLane) == 8, "ClearModel zeroes 8 bytes per lane");
|
||||
|
||||
struct Lane {
|
||||
uintptr_t offset;
|
||||
uint32_t bodySlot;
|
||||
@@ -140,12 +148,8 @@ bool g_dressing = false; // Dress() is rebuilding the pane
|
||||
uintptr_t g_mainHandPath = Offsets::TRY_ON_MAIN_HAND_PATH;
|
||||
uintptr_t g_offHandPath = Offsets::TRY_ON_OFF_HAND_PATH;
|
||||
|
||||
void *ResolvePane(void *L) {
|
||||
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_DRESSUPMODEL_LUA_TYPE_ID);
|
||||
}
|
||||
|
||||
void *PaneModel(void *pane) {
|
||||
return Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
|
||||
HeldLane &LaneOf(void *pane, int i) {
|
||||
return Game::Ref<HeldLane>(pane, kLanes[i].offset);
|
||||
}
|
||||
|
||||
int LaneIndex(uint32_t bodySlot) {
|
||||
@@ -153,12 +157,10 @@ int LaneIndex(uint32_t bodySlot) {
|
||||
}
|
||||
|
||||
// The glow `enchantID` gives an item shown with `display`: none when the
|
||||
// display has its own ItemVisuals row (GetItemDescVisual 0x0062EC70).
|
||||
// display has its own.
|
||||
uint32_t EnchantVisual(const void *display, uint32_t enchantID) {
|
||||
if (display == nullptr ||
|
||||
DBC::Record(Offsets::VAR_ITEMVISUALS_RECORDS, Offsets::VAR_ITEMVISUALS_COUNT,
|
||||
Game::Read<uint32_t>(display, Offsets::OFF_ITEMDISPLAYINFO_ITEM_VISUAL)) !=
|
||||
nullptr)
|
||||
Model::ItemDisplay::OwnVisual(static_cast<const uint8_t *>(display)) != 0)
|
||||
return 0;
|
||||
const uint8_t *enchant = DBC::Record(Offsets::VAR_SPELLITEMENCHANT_RECORDS,
|
||||
Offsets::VAR_SPELLITEMENCHANT_COUNT, enchantID);
|
||||
@@ -174,27 +176,33 @@ void HangVisual(void *model, int attachId, uint32_t visual) {
|
||||
return;
|
||||
if (void *item = reinterpret_cast<FindChild_t>(Offsets::FUN_M2MODEL_FIND_CHILD)(model,
|
||||
attachId))
|
||||
reinterpret_cast<AttachItemVisuals_t>(Offsets::FUN_ATTACH_ITEM_VISUALS)(item, visual);
|
||||
M2::AttachItemVisuals(item, visual);
|
||||
}
|
||||
|
||||
// DetachHeldItemSlot on lane `i`'s item.
|
||||
void DetachLane(void *model, int i, const HeldLane &lane) {
|
||||
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(
|
||||
model, kLanes[i].bodySlot, lane.sheatheType,
|
||||
lane.inventoryType == Offsets::INVTYPE_SHIELD);
|
||||
}
|
||||
|
||||
// The unit's AttachHandItem step on each occupied lane: detach it, then hang
|
||||
// it in the hand or the sheath, or leave it off.
|
||||
void ApplyWeapons(void *pane, const PaneState &s) {
|
||||
void *model = PaneModel(pane);
|
||||
void *model = Pane::Model(pane);
|
||||
if (model == nullptr)
|
||||
return;
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const uint8_t *lane = Game::Ptr<uint8_t>(pane, kLanes[i].offset);
|
||||
if (lane[0] == 0 || s.laneDisplay[i] == nullptr)
|
||||
const HeldLane &lane = LaneOf(pane, i);
|
||||
if (lane.itemClass == 0 || s.laneDisplay[i] == nullptr)
|
||||
continue;
|
||||
const bool shield = lane[3] == kInvTypeShield;
|
||||
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(
|
||||
model, kLanes[i].bodySlot, lane[4], shield);
|
||||
DetachLane(model, i, lane);
|
||||
if (s.hideWeapons)
|
||||
continue;
|
||||
const int attachId = reinterpret_cast<AttachItemModel_t>(Offsets::FUN_ATTACH_ITEM_MODEL)(
|
||||
model, s.laneDisplay[i], kLanes[i].bodySlot, lane[4], s.sheathed, shield,
|
||||
lane[3] == kInvTypeRangedRight);
|
||||
model, s.laneDisplay[i], kLanes[i].bodySlot, lane.sheatheType, s.sheathed,
|
||||
lane.inventoryType == Offsets::INVTYPE_SHIELD,
|
||||
lane.inventoryType == Offsets::INVTYPE_RANGEDRIGHT);
|
||||
HangVisual(model, attachId, s.laneVisual[i]);
|
||||
}
|
||||
}
|
||||
@@ -265,8 +273,9 @@ int __fastcall TryOnAttach(void *model, const void *record, uint32_t bodySlot,
|
||||
void __fastcall TryOnEvict(void *model, uint32_t bodySlot, uint32_t sheatheType, bool shield) {
|
||||
if (g_tryOnPane != nullptr) {
|
||||
PaneState &s = g_panes[g_tryOnPane];
|
||||
s.laneDisplay[LaneIndex(bodySlot)] = nullptr;
|
||||
s.laneVisual[LaneIndex(bodySlot)] = 0;
|
||||
const int lane = LaneIndex(bodySlot);
|
||||
s.laneDisplay[lane] = nullptr;
|
||||
s.laneVisual[lane] = 0;
|
||||
}
|
||||
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(model, bodySlot,
|
||||
sheatheType, shield);
|
||||
@@ -294,14 +303,12 @@ __declspec(naked) void HandChoiceThunk() {
|
||||
}
|
||||
|
||||
// ClearModel's lane step: detach the held item and zero the lane.
|
||||
void ClearLane(void *pane, uintptr_t laneOffset, uint32_t bodySlot) {
|
||||
uint8_t *lane = Game::Ptr<uint8_t>(pane, laneOffset);
|
||||
if (lane[0] == 0)
|
||||
void ClearLane(void *pane, int i) {
|
||||
HeldLane &lane = LaneOf(pane, i);
|
||||
if (lane.itemClass == 0)
|
||||
return;
|
||||
reinterpret_cast<DetachHeldItem_t>(Offsets::FUN_DETACH_HELD_ITEM_SLOT)(
|
||||
Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE), bodySlot, lane[4],
|
||||
lane[3] == kInvTypeShield);
|
||||
std::memset(lane, 0, 8);
|
||||
DetachLane(Pane::Model(pane), i, lane);
|
||||
lane = HeldLane{};
|
||||
}
|
||||
|
||||
// Component slot for a 0-based inventory slot, or -1 (4.3.4 UndressSlot).
|
||||
@@ -323,7 +330,7 @@ int ComponentSlot(int slot0) {
|
||||
}
|
||||
|
||||
int __fastcall Script_UndressSlot(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
void *pane = Game::Lua::ResolveDressUpModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
@@ -332,13 +339,12 @@ int __fastcall Script_UndressSlot(void *L) {
|
||||
}
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 2)) - 1;
|
||||
void *component = Game::Read<void *>(pane, Offsets::OFF_DRESSUPMODEL_COMPONENT);
|
||||
if (Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE) == nullptr ||
|
||||
component == nullptr)
|
||||
if (Pane::Model(pane) == nullptr || component == nullptr)
|
||||
return 0;
|
||||
if (slot0 == 15) {
|
||||
ClearLane(pane, Offsets::OFF_DRESSUPMODEL_MAIN_HAND_LANE, kBodySlotMainHand);
|
||||
} else if (slot0 == 16) {
|
||||
ClearLane(pane, Offsets::OFF_DRESSUPMODEL_OFF_HAND_LANE, kBodySlotOffHand);
|
||||
if (slot0 + 1 == Offsets::INVSLOT_MAINHAND) {
|
||||
ClearLane(pane, 0);
|
||||
} else if (slot0 + 1 == Offsets::INVSLOT_OFFHAND) {
|
||||
ClearLane(pane, 1);
|
||||
} else {
|
||||
const int compSlot = ComponentSlot(slot0);
|
||||
if (compSlot >= 0)
|
||||
@@ -399,8 +405,8 @@ uint32_t NamedHand(const char *name) {
|
||||
name) != 0)
|
||||
continue;
|
||||
switch (Game::Read<int>(row, Offsets::OFF_PAPERDOLLITEMFRAME_SLOT_NUMBER)) {
|
||||
case 16: return kBodySlotMainHand;
|
||||
case 17: return kBodySlotOffHand;
|
||||
case Offsets::INVSLOT_MAINHAND: return kBodySlotMainHand;
|
||||
case Offsets::INVSLOT_OFFHAND: return kBodySlotOffHand;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
@@ -413,17 +419,35 @@ int TryOnResult(void *pane, uint32_t itemID) {
|
||||
if (record == nullptr)
|
||||
return kTryOnDataPending;
|
||||
if (Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID) == 0 ||
|
||||
PaneModel(pane) == nullptr)
|
||||
Pane::Model(pane) == nullptr)
|
||||
return kTryOnNotEquippable;
|
||||
switch (Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE)) {
|
||||
case 1: case 3: case 4: case 5: case 6: case 7: case 8: case 9: case 10:
|
||||
case 16: case 19: case 20:
|
||||
case Offsets::INVTYPE_HEAD:
|
||||
case Offsets::INVTYPE_SHOULDER:
|
||||
case Offsets::INVTYPE_BODY:
|
||||
case Offsets::INVTYPE_CHEST:
|
||||
case Offsets::INVTYPE_WAIST:
|
||||
case Offsets::INVTYPE_LEGS:
|
||||
case Offsets::INVTYPE_FEET:
|
||||
case Offsets::INVTYPE_WRIST:
|
||||
case Offsets::INVTYPE_HAND:
|
||||
case Offsets::INVTYPE_CLOAK:
|
||||
case Offsets::INVTYPE_TABARD:
|
||||
case Offsets::INVTYPE_ROBE:
|
||||
// The armor arms: the component and the ready byte.
|
||||
return Game::Read<void *>(pane, Offsets::OFF_DRESSUPMODEL_COMPONENT) != nullptr &&
|
||||
Game::Read<uint8_t>(pane, Offsets::OFF_DRESSUPMODEL_READY) != 0
|
||||
? kTryOnSuccess
|
||||
: kTryOnNotEquippable;
|
||||
case 13: case 14: case 15: case 17: case 21: case 22: case 23: case 25: case 26:
|
||||
case Offsets::INVTYPE_WEAPON:
|
||||
case Offsets::INVTYPE_SHIELD:
|
||||
case Offsets::INVTYPE_RANGED:
|
||||
case Offsets::INVTYPE_2HWEAPON:
|
||||
case Offsets::INVTYPE_WEAPONMAINHAND:
|
||||
case Offsets::INVTYPE_WEAPONOFFHAND:
|
||||
case Offsets::INVTYPE_HOLDABLE:
|
||||
case Offsets::INVTYPE_THROWN:
|
||||
case Offsets::INVTYPE_RANGEDRIGHT:
|
||||
return kTryOnSuccess; // the held arm
|
||||
default:
|
||||
return kTryOnNotEquippable;
|
||||
@@ -431,7 +455,7 @@ int TryOnResult(void *pane, uint32_t itemID) {
|
||||
}
|
||||
|
||||
int __fastcall Script_TryOn(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
void *pane = Game::Lua::ResolveDressUpModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
// Read before the name search below, which stomps the Lua stack.
|
||||
@@ -484,18 +508,16 @@ int __fastcall Script_Dress(void *L) {
|
||||
}
|
||||
|
||||
int __fastcall Script_IsGeoReady(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
void *pane = Game::Lua::ResolveDressUpModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
void *model = PaneModel(pane);
|
||||
Game::Lua::PushBool(L, model != nullptr &&
|
||||
reinterpret_cast<ModelLoaded_t>(Offsets::FUN_MODEL_INSTANCE_LOADED)(
|
||||
model, 0, 1) != 0);
|
||||
void *model = Pane::Model(pane);
|
||||
Game::Lua::PushBool(L, model != nullptr && M2::IsLoaded(model, 0, 1));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetSheathed(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
void *pane = Game::Lua::ResolveDressUpModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
PaneState &s = g_panes[pane];
|
||||
@@ -507,7 +529,7 @@ int __fastcall Script_SetSheathed(void *L) {
|
||||
}
|
||||
|
||||
int __fastcall Script_GetSheathed(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
void *pane = Game::Lua::ResolveDressUpModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
const auto it = g_panes.find(pane);
|
||||
@@ -516,13 +538,13 @@ int __fastcall Script_GetSheathed(void *L) {
|
||||
}
|
||||
|
||||
int __fastcall Script_SetAutoDress(void *L) {
|
||||
if (void *pane = ResolvePane(L))
|
||||
if (void *pane = Game::Lua::ResolveDressUpModel(L))
|
||||
g_panes[pane].autoDress = Game::Lua::ToBoolean(L, 2) != 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetAutoDress(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
void *pane = Game::Lua::ResolveDressUpModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
const auto it = g_panes.find(pane);
|
||||
@@ -618,11 +640,8 @@ void RegisterLuaFunctions() {
|
||||
reinterpret_cast<const void *>(&SetModelInstance));
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_DRESSUPMODEL_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])));
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "ItemTryOnReason", kItemTryOnReason,
|
||||
static_cast<int>(sizeof(kItemTryOnReason) / sizeof(kItemTryOnReason[0])), "UIModelInfo");
|
||||
g_methodDocs);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ItemTryOnReason", kItemTryOnReason, "UIModelInfo");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+2
-2
@@ -47,8 +47,8 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_MODEL_METHOD_REGISTRY),
|
||||
g_methods, 1, g_methodDocs, 1);
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_MODEL_METHOD_REGISTRY), g_methods, g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+37
-55
@@ -53,6 +53,7 @@
|
||||
#include "item/Record.h"
|
||||
#include "model/DisplayInfo.h"
|
||||
#include "model/ItemDisplay.h"
|
||||
#include "model/M2.h"
|
||||
#include "texture/Load.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -64,24 +65,9 @@ namespace Model::ItemDisplay {
|
||||
namespace {
|
||||
|
||||
using PaneRecordFn_t = void(__thiscall *)(void *pane, const uint8_t *record);
|
||||
using SetModelInstance_t = void(__thiscall *)(void *frame, void *model);
|
||||
using GetScene_t = void *(__thiscall *)(void *frame);
|
||||
using CreateModel_t = void *(__thiscall *)(void *scene, const char *path, uint32_t flags);
|
||||
using ModelRelease_t = void(__fastcall *)(void *model);
|
||||
using TextureLoad_t = void *(__fastcall *)(const char *path, void *status, uint32_t flags,
|
||||
int arg4, int arg5);
|
||||
using ReplaceTexture_t = void(__thiscall *)(void *model, uint32_t type, void *texture);
|
||||
using AttachItemVisuals_t = void(__fastcall *)(void *model, uint32_t visualID);
|
||||
using SetAnimDone_t = void(__thiscall *)(void *model, const void *callback, uint32_t userLow,
|
||||
uint32_t userHigh);
|
||||
using PlayAnimation_t = void(__thiscall *)(void *model, int keyBone, uint32_t id, int variation,
|
||||
int offset, float speed, int blend, int primary);
|
||||
|
||||
constexpr uint32_t kInvTypeHead = 1;
|
||||
constexpr uint32_t kInvTypeShoulders = 3;
|
||||
constexpr uint32_t kInvTypeShield = 14;
|
||||
constexpr uint32_t kInvTypeAmmo = 24;
|
||||
constexpr uint32_t kInvTypeQuiver = 27;
|
||||
|
||||
// The texture type an item model takes its skin as.
|
||||
constexpr uint32_t kItemSkinTexture = 2;
|
||||
@@ -90,27 +76,25 @@ constexpr uint32_t kItemTextureFilter = 3;
|
||||
constexpr int kSexLetters = 3;
|
||||
constexpr size_t kPathSize = 260;
|
||||
|
||||
// The record a pane shows for SetItem. The pane reads only the display-ID
|
||||
// field, which stays 0. One per pane, never freed while the UI lives: the pane
|
||||
// keeps the pointer and builds from it on every show.
|
||||
// The record a pane shows for SetItem; its display ID stays 0.
|
||||
struct ItemRecord {
|
||||
uint8_t display[Offsets::OFF_CREATURE_DISPLAYID + sizeof(uint32_t)];
|
||||
uint32_t itemID;
|
||||
uint32_t visual;
|
||||
Pane::DisplayRecord display;
|
||||
uint32_t itemID = 0;
|
||||
uint32_t visual = 0;
|
||||
};
|
||||
std::unordered_map<void *, ItemRecord> g_records;
|
||||
|
||||
const char *Directory(uint32_t inventoryType) {
|
||||
switch (inventoryType) {
|
||||
case kInvTypeHead:
|
||||
case Offsets::INVTYPE_HEAD:
|
||||
return "Item\\ObjectComponents\\Head\\";
|
||||
case kInvTypeShoulders:
|
||||
case Offsets::INVTYPE_SHOULDER:
|
||||
return "Item\\ObjectComponents\\Shoulder\\";
|
||||
case kInvTypeShield:
|
||||
case Offsets::INVTYPE_SHIELD:
|
||||
return "Item\\ObjectComponents\\Shield\\";
|
||||
case kInvTypeAmmo:
|
||||
case Offsets::INVTYPE_AMMO:
|
||||
return "Item\\ObjectComponents\\Ammo\\";
|
||||
case kInvTypeQuiver:
|
||||
case Offsets::INVTYPE_QUIVER:
|
||||
return "Item\\ObjectComponents\\Quiver\\";
|
||||
default:
|
||||
return "Item\\ObjectComponents\\Weapon\\";
|
||||
@@ -166,15 +150,13 @@ void *CreateItemModel(void *pane, const ItemRecord &rec) {
|
||||
const char *dir = Directory(inventoryType);
|
||||
|
||||
char path[kPathSize];
|
||||
if (inventoryType == kInvTypeHead) {
|
||||
if (inventoryType == Offsets::INVTYPE_HEAD) {
|
||||
if (!HeadPath(path, dir, name))
|
||||
return nullptr;
|
||||
} else {
|
||||
std::snprintf(path, sizeof(path), "%s%s", dir, name);
|
||||
}
|
||||
void *scene = reinterpret_cast<GetScene_t>(Offsets::FUN_SIMPLEMODEL_GET_SCENE)(pane);
|
||||
void *model =
|
||||
reinterpret_cast<CreateModel_t>(Offsets::FUN_M2SCENE_CREATE_MODEL)(scene, path, 0);
|
||||
void *model = M2::CreateModel(Pane::Scene(pane), path);
|
||||
if (model == nullptr)
|
||||
return nullptr;
|
||||
|
||||
@@ -186,7 +168,7 @@ void *CreateItemModel(void *pane, const ItemRecord &rec) {
|
||||
path, reinterpret_cast<void *>(Offsets::VAR_ITEM_MODEL_TEXTURE_STATUS),
|
||||
Texture::Flags(kItemTextureFilter, false, false), 1, 0);
|
||||
if (handle == nullptr) {
|
||||
reinterpret_cast<ModelRelease_t>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(model);
|
||||
M2::Release(model);
|
||||
return nullptr;
|
||||
}
|
||||
reinterpret_cast<ReplaceTexture_t>(Offsets::FUN_M2MODEL_REPLACE_TEXTURE)(
|
||||
@@ -194,13 +176,10 @@ void *CreateItemModel(void *pane, const ItemRecord &rec) {
|
||||
Texture::ReleaseHandle(handle);
|
||||
}
|
||||
|
||||
const uint32_t own = Game::Read<uint32_t>(display, Offsets::OFF_ITEMDISPLAYINFO_ITEM_VISUAL);
|
||||
const uint32_t visual = DBC::Record(Offsets::VAR_ITEMVISUALS_RECORDS,
|
||||
Offsets::VAR_ITEMVISUALS_COUNT, own) != nullptr
|
||||
? own
|
||||
: rec.visual;
|
||||
const uint32_t own = OwnVisual(display);
|
||||
const uint32_t visual = own != 0 ? own : rec.visual;
|
||||
if (visual != 0) {
|
||||
reinterpret_cast<AttachItemVisuals_t>(Offsets::FUN_ATTACH_ITEM_VISUALS)(model, visual);
|
||||
M2::AttachItemVisuals(model, visual);
|
||||
Model::DisplayInfo::LightChildren(pane, model);
|
||||
}
|
||||
return model;
|
||||
@@ -210,17 +189,15 @@ void *CreateItemModel(void *pane, const ItemRecord &rec) {
|
||||
// for the item shows nothing.
|
||||
void Show(void *pane, const ItemRecord &rec) {
|
||||
void *model = CreateItemModel(pane, rec);
|
||||
Game::Read<SetModelInstance_t>(Game::Read<const void *>(pane, 0),
|
||||
Offsets::VTBL_SIMPLEMODEL_SET_MODEL_INSTANCE)(pane, model);
|
||||
Pane::SetModel(pane, model);
|
||||
if (model == nullptr)
|
||||
return;
|
||||
const auto user = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(pane));
|
||||
reinterpret_cast<SetAnimDone_t>(Offsets::FUN_M2MODEL_SET_ANIM_DONE_CALLBACK)(
|
||||
model, reinterpret_cast<const void *>(Offsets::FUN_PLAYERMODEL_ON_ANIM_DONE), user,
|
||||
static_cast<int32_t>(user) < 0 ? 0xFFFFFFFFu : 0u);
|
||||
reinterpret_cast<PlayAnimation_t>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(model, -1, 0, -1, 0,
|
||||
1.0f, 1, 1);
|
||||
reinterpret_cast<ModelRelease_t>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(model);
|
||||
M2::SetAnimDoneCallback(model,
|
||||
reinterpret_cast<const void *>(Offsets::FUN_PLAYERMODEL_ON_ANIM_DONE),
|
||||
user, static_cast<int32_t>(user) < 0 ? 0xFFFFFFFFu : 0u);
|
||||
M2::PlayAnimation(model, -1, 0, -1, 0, 1.0f, 1, 1);
|
||||
M2::Release(model);
|
||||
}
|
||||
|
||||
// A queued item has loaded: build each visible pane that still shows it,
|
||||
@@ -229,18 +206,16 @@ void OnItemLoaded(uint32_t itemID, bool success) {
|
||||
if (!success)
|
||||
return;
|
||||
for (auto &[pane, rec] : g_records)
|
||||
if (rec.itemID == itemID &&
|
||||
Game::Read<const uint8_t *>(pane, Offsets::OFF_PLAYERMODEL_DISPLAY_RECORD) ==
|
||||
rec.display &&
|
||||
Game::Read<int>(pane, Offsets::OFF_FRAME_VISIBLE) != 0)
|
||||
if (rec.itemID == itemID && Pane::DisplayRecordOf(pane) == rec.display.bytes &&
|
||||
Pane::IsVisible(pane))
|
||||
reinterpret_cast<PaneRecordFn_t>(Offsets::FUN_PLAYERMODEL_ATTACH_DISPLAY_MODEL)(
|
||||
pane, rec.display);
|
||||
pane, rec.display.bytes);
|
||||
}
|
||||
|
||||
const ::Item::Data::LoadSubscriber _loadSub{&OnItemLoaded};
|
||||
|
||||
int __fastcall Script_SetItem(void *L) {
|
||||
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
|
||||
void *pane = Game::Lua::ResolvePlayerModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
@@ -252,7 +227,7 @@ int __fastcall Script_SetItem(void *L) {
|
||||
rec.itemID = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
|
||||
if (Game::Lua::IsNumber(L, 4))
|
||||
rec.visual = static_cast<uint32_t>(Game::Lua::ToNumber(L, 4));
|
||||
reinterpret_cast<PaneRecordFn_t>(Offsets::FUN_PLAYERMODEL_SET_DISPLAY)(pane, rec.display);
|
||||
Pane::SetDisplay(pane, rec.display.bytes);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -274,8 +249,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_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])));
|
||||
g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
@@ -289,9 +263,17 @@ const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t OwnVisual(const uint8_t *display) {
|
||||
const uint32_t own = Game::Read<uint32_t>(display, Offsets::OFF_ITEMDISPLAYINFO_ITEM_VISUAL);
|
||||
return DBC::Record(Offsets::VAR_ITEMVISUALS_RECORDS, Offsets::VAR_ITEMVISUALS_COUNT, own) !=
|
||||
nullptr
|
||||
? own
|
||||
: 0;
|
||||
}
|
||||
|
||||
bool Attach(void *pane, const uint8_t *record) {
|
||||
const auto it = g_records.find(pane);
|
||||
if (record == nullptr || it == g_records.end() || record != it->second.display)
|
||||
if (record == nullptr || it == g_records.end() || record != it->second.display.bytes)
|
||||
return false;
|
||||
if (::Item::PeekRecord(it->second.itemID) == nullptr)
|
||||
::Item::Data::WarmCache(it->second.itemID);
|
||||
|
||||
@@ -22,4 +22,9 @@ namespace Model::ItemDisplay {
|
||||
// other record returns false and is the engine's to build.
|
||||
bool Attach(void *pane, const uint8_t *record);
|
||||
|
||||
// The ItemVisuals row an ItemDisplayInfo `display` hangs itself, or 0 when it
|
||||
// names none that exists. An enchant's glow shows only when this is 0
|
||||
// (GetItemDescVisual 0x0062EC70).
|
||||
uint32_t OwnVisual(const uint8_t *display);
|
||||
|
||||
} // namespace Model::ItemDisplay
|
||||
|
||||
+14
-23
@@ -30,6 +30,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "model/M2.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_set>
|
||||
@@ -40,7 +41,6 @@ namespace {
|
||||
using PaneFn_t = void(__thiscall *)(void *pane);
|
||||
using SetUnitGuid_t = void(__thiscall *)(void *pane, uint64_t guid);
|
||||
using SetDisplay_t = void(__thiscall *)(void *pane, const uint8_t *record);
|
||||
using SetModelInstance_t = void(__thiscall *)(void *pane, void *model);
|
||||
|
||||
PaneFn_t g_origHide = nullptr;
|
||||
PaneFn_t g_origShow = nullptr;
|
||||
@@ -53,19 +53,16 @@ bool Keeps(void *pane) {
|
||||
return g_keeping.count(pane) != 0;
|
||||
}
|
||||
|
||||
bool HasModel(void *pane) {
|
||||
return Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE) != nullptr;
|
||||
}
|
||||
|
||||
// The pane Hide's own drop: slot 36 with null.
|
||||
void DropModel(void *pane) {
|
||||
Game::Read<SetModelInstance_t>(Game::Read<const void *>(pane, 0),
|
||||
Offsets::VTBL_SIMPLEMODEL_SET_MODEL_INSTANCE)(pane, nullptr);
|
||||
// Hide's own drop (slot 36 with null), for a hidden pane still holding a
|
||||
// model.
|
||||
void DropIfHidden(void *pane) {
|
||||
if (!Pane::IsVisible(pane) && Pane::Model(pane) != nullptr)
|
||||
Pane::SetModel(pane, nullptr);
|
||||
}
|
||||
|
||||
void DropIfKeptWhileHidden(void *pane) {
|
||||
if (Game::Read<int>(pane, Offsets::OFF_FRAME_VISIBLE) == 0 && HasModel(pane) && Keeps(pane))
|
||||
DropModel(pane);
|
||||
if (Keeps(pane))
|
||||
DropIfHidden(pane);
|
||||
}
|
||||
|
||||
void __fastcall Hide_h(void *pane, void * /*edx*/) {
|
||||
@@ -77,7 +74,7 @@ void __fastcall Hide_h(void *pane, void * /*edx*/) {
|
||||
}
|
||||
|
||||
void __fastcall Show_h(void *pane, void * /*edx*/) {
|
||||
if (Keeps(pane) && HasModel(pane)) {
|
||||
if (Keeps(pane) && Pane::Model(pane) != nullptr) {
|
||||
reinterpret_cast<PaneFn_t>(Offsets::FUN_SIMPLEFRAME_SHOW)(pane);
|
||||
return;
|
||||
}
|
||||
@@ -107,26 +104,21 @@ const Game::HookAutoRegister _setDisplayHook{Offsets::FUN_PLAYERMODEL_SET_DISPLA
|
||||
reinterpret_cast<void *>(&SetDisplay_h),
|
||||
reinterpret_cast<void **>(&g_origSetDisplay)};
|
||||
|
||||
void *ResolvePane(void *L) {
|
||||
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
|
||||
}
|
||||
|
||||
int __fastcall Script_SetKeepModelOnHide(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
void *pane = Game::Lua::ResolvePlayerModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (Game::Lua::ToBoolean(L, 2) != 0) {
|
||||
g_keeping.insert(pane);
|
||||
return 0;
|
||||
}
|
||||
if (g_keeping.erase(pane) != 0 && Game::Read<int>(pane, Offsets::OFF_FRAME_VISIBLE) == 0 &&
|
||||
HasModel(pane))
|
||||
DropModel(pane);
|
||||
if (g_keeping.erase(pane) != 0)
|
||||
DropIfHidden(pane);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetKeepModelOnHide(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
void *pane = Game::Lua::ResolvePlayerModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, Keeps(pane));
|
||||
@@ -156,8 +148,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_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])));
|
||||
g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+148
@@ -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/>.
|
||||
|
||||
// Typed calls into the engine's M2 model instances and the Model frames
|
||||
// ("panes") that show them, shared by every model/ module.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Model::M2 {
|
||||
|
||||
inline bool IsLoaded(void *model, int tryLoad, int recurseChildren) {
|
||||
using Fn = int(__thiscall *)(void *, int, int);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_MODEL_INSTANCE_LOADED)(model, tryLoad,
|
||||
recurseChildren) != 0;
|
||||
}
|
||||
|
||||
// The draw gate: also makes the device textures when `wait` is set.
|
||||
inline bool IsReady(void *model, int wait, int recurseChildren) {
|
||||
using Fn = int(__thiscall *)(void *, int, int);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_MODEL_INSTANCE_READY)(model, wait,
|
||||
recurseChildren) != 0;
|
||||
}
|
||||
|
||||
inline void AddRef(void *model) {
|
||||
reinterpret_cast<void(__fastcall *)(void *)>(Offsets::FUN_MODEL_INSTANCE_ADDREF)(model);
|
||||
}
|
||||
|
||||
inline void Release(void *model) {
|
||||
reinterpret_cast<void(__fastcall *)(void *)>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(model);
|
||||
}
|
||||
|
||||
// A new model in `scene` that the caller owns one reference on, or null.
|
||||
inline void *CreateModel(void *scene, const char *path) {
|
||||
using Fn = void *(__thiscall *)(void *, const char *, uint32_t);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_M2SCENE_CREATE_MODEL)(scene, path, 0);
|
||||
}
|
||||
|
||||
inline void PlayAnimation(void *model, int keyBone, uint32_t id, int variation, int offset,
|
||||
float speed, int blend, int primary) {
|
||||
using Fn = void(__thiscall *)(void *, int, uint32_t, int, int, float, int, int);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(model, keyBone, id, variation,
|
||||
offset, speed, blend, primary);
|
||||
}
|
||||
|
||||
inline void SetAnimDoneCallback(void *model, const void *callback, uint32_t userLow,
|
||||
uint32_t userHigh) {
|
||||
using Fn = void(__thiscall *)(void *, const void *, uint32_t, uint32_t);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_ANIM_DONE_CALLBACK)(model, callback, userLow,
|
||||
userHigh);
|
||||
}
|
||||
|
||||
inline void SetVisible(void *model, bool visible) {
|
||||
using Fn = void(__thiscall *)(void *, int);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_VISIBLE)(model, visible ? 1 : 0);
|
||||
}
|
||||
|
||||
inline void SetAnimated(void *model, bool animated) {
|
||||
using Fn = void(__thiscall *)(void *, int);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2MODEL_SET_ANIMATED)(model, animated ? 1 : 0);
|
||||
}
|
||||
|
||||
inline void AnimateScene(void *scene, const float *cameraPos) {
|
||||
using Fn = void(__thiscall *)(void *, const float *);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_M2SCENE_ANIMATE)(scene, cameraPos);
|
||||
}
|
||||
|
||||
inline void AttachItemVisuals(void *model, uint32_t visualID) {
|
||||
using Fn = void(__fastcall *)(void *, uint32_t);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_ATTACH_ITEM_VISUALS)(model, visualID);
|
||||
}
|
||||
|
||||
inline void SetRenderCallback(void *model, const void *callback, void *userData) {
|
||||
using Fn = void(__thiscall *)(void *, const void *, void *);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_MODEL_INSTANCE_SET_RENDER_CB)(model, callback, userData);
|
||||
}
|
||||
|
||||
// The model's shared M2 data (the file's header and tables).
|
||||
inline const uint8_t *Data(void *model) {
|
||||
return Game::Read<const uint8_t *>(Game::Read<const uint8_t *>(model, Offsets::OFF_M2MODEL_SHARED),
|
||||
Offsets::OFF_M2SHARED_DATA);
|
||||
}
|
||||
|
||||
// The index `id` maps to in one of the data's lookup tables
|
||||
// (OFF_M2DATA_*_LOOKUP); the caller bounds it by that table's count.
|
||||
inline uint16_t LookupIndex(void *model, uintptr_t tableOffset, uint32_t id) {
|
||||
using Fn = uint16_t(__fastcall *)(uint32_t, const void *);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_M2_LOOKUP_INDEX)(id, Data(model) + tableOffset);
|
||||
}
|
||||
|
||||
} // namespace Model::M2
|
||||
|
||||
namespace Model::Pane {
|
||||
|
||||
// The model instance a Model frame shows, or null.
|
||||
inline void *Model(const void *pane) {
|
||||
return Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
|
||||
}
|
||||
|
||||
inline void *Scene(void *pane) {
|
||||
using Fn = void *(__thiscall *)(void *);
|
||||
return reinterpret_cast<Fn>(Offsets::FUN_SIMPLEMODEL_GET_SCENE)(pane);
|
||||
}
|
||||
|
||||
// The frame's SetModelInstance virtual; null drops its model.
|
||||
inline void SetModel(void *pane, void *model) {
|
||||
using Fn = void(__thiscall *)(void *, void *);
|
||||
Game::Read<Fn>(Game::Read<const void *>(pane, 0),
|
||||
Offsets::VTBL_SIMPLEMODEL_SET_MODEL_INSTANCE)(pane, model);
|
||||
}
|
||||
|
||||
// PlayerModel's SetDisplay: stores the display record the pane builds from.
|
||||
inline void SetDisplay(void *pane, const uint8_t *record) {
|
||||
using Fn = void(__thiscall *)(void *, const uint8_t *);
|
||||
reinterpret_cast<Fn>(Offsets::FUN_PLAYERMODEL_SET_DISPLAY)(pane, record);
|
||||
}
|
||||
|
||||
inline bool IsVisible(const void *pane) {
|
||||
return Game::Read<int>(pane, Offsets::OFF_FRAME_VISIBLE) != 0;
|
||||
}
|
||||
|
||||
// The display record the pane shows (SetDisplay's), or null.
|
||||
inline const uint8_t *DisplayRecordOf(const void *pane) {
|
||||
return Game::Read<const uint8_t *>(pane, Offsets::OFF_PLAYERMODEL_DISPLAY_RECORD);
|
||||
}
|
||||
|
||||
// A display record of our own for SetDisplay. The pane reads only its
|
||||
// display-ID field, keeps the pointer and builds from it on every show, so
|
||||
// the record lives as long as the pane.
|
||||
struct DisplayRecord {
|
||||
uint8_t bytes[Offsets::OFF_CREATURE_DISPLAYID + sizeof(uint32_t)] = {};
|
||||
};
|
||||
|
||||
} // namespace Model::Pane
|
||||
+18
-33
@@ -54,6 +54,7 @@
|
||||
#include "dbc/Lookup.h"
|
||||
#include "model/Camera.h"
|
||||
#include "model/DisplayInfo.h"
|
||||
#include "model/M2.h"
|
||||
#include "texture/Load.h"
|
||||
#include "tick/FrameTick.h"
|
||||
|
||||
@@ -71,19 +72,11 @@ 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);
|
||||
@@ -130,7 +123,7 @@ 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);
|
||||
M2::Release(p.model);
|
||||
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(p.scene);
|
||||
p.dresser = p.model = p.scene = nullptr;
|
||||
p.dressRecord = nullptr;
|
||||
@@ -166,8 +159,7 @@ bool Bake(Portrait &p) {
|
||||
}
|
||||
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);
|
||||
M2::PlayAnimation(copy, -1, 0, 0, 0, 1.0f, 0, 1);
|
||||
|
||||
float projection[16];
|
||||
float view[16];
|
||||
@@ -188,19 +180,12 @@ bool Bake(Portrait &p) {
|
||||
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);
|
||||
M2::IsLoaded(copy, 1, 1);
|
||||
M2::SetAnimated(copy, true);
|
||||
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);
|
||||
M2::AnimateScene(scene, origin);
|
||||
const Model::Camera::Vec3 eye = Model::Camera::GetVector(camera, Offsets::CAMERA_PROP_EYE);
|
||||
Model::Camera::GetVector(camera, Offsets::CAMERA_PROP_TARGET); // the bake reads it too
|
||||
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);
|
||||
@@ -209,12 +194,12 @@ bool Bake(Portrait &p) {
|
||||
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);
|
||||
M2::IsReady(copy, 1, 1);
|
||||
M2::SetAnimated(copy, true);
|
||||
M2::SetVisible(copy, true);
|
||||
M2::SetRenderCallback(copy, reinterpret_cast<const void *>(Offsets::FUN_PORTRAIT_LIGHTING),
|
||||
nullptr);
|
||||
M2::AnimateScene(scene, &eye.x);
|
||||
auto draw = reinterpret_cast<DrawScene_t>(Offsets::FUN_M2SCENE_DRAW);
|
||||
draw(scene, 0);
|
||||
draw(scene, 1);
|
||||
@@ -239,14 +224,14 @@ bool Bake(Portrait &p) {
|
||||
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);
|
||||
M2::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))
|
||||
if (!M2::IsLoaded(p.model, 0, 1))
|
||||
return false;
|
||||
if (p.dressRecord != nullptr) {
|
||||
// A dress the engine rejects bakes undressed, as a unit with no
|
||||
@@ -298,8 +283,8 @@ Portrait *Acquire(uint32_t displayID) {
|
||||
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);
|
||||
void *model = M2::CreateModel(
|
||||
scene, Game::Read<const char *>(modelData, Offsets::OFF_CREATUREMODELDATA_MODEL_PATH));
|
||||
if (model == nullptr) {
|
||||
reinterpret_cast<ReleaseScene_t>(Offsets::FUN_M2SCENE_RELEASE)(scene);
|
||||
return nullptr;
|
||||
|
||||
@@ -28,17 +28,15 @@
|
||||
namespace Model::Rotation {
|
||||
namespace {
|
||||
|
||||
constexpr int kTypeNone = -1; // lua_type of an absent argument
|
||||
|
||||
using LuaMethod_t = int(__fastcall *)(void *L);
|
||||
|
||||
int __fastcall Script_SetRotation(void *L) {
|
||||
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
|
||||
void *pane = Game::Lua::ResolvePlayerModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
const bool animate = Game::Lua::Type(L, 3) == kTypeNone || Game::Lua::ToBoolean(L, 3) != 0;
|
||||
const bool animate =
|
||||
Game::Lua::Type(L, 3) == Game::Lua::TYPE_NONE || Game::Lua::ToBoolean(L, 3) != 0;
|
||||
if (animate && !Model::Animation::HasRestingAnimation(pane))
|
||||
return reinterpret_cast<LuaMethod_t>(Offsets::FUN_SCRIPT_PLAYERMODEL_SET_ROTATION)(L);
|
||||
return reinterpret_cast<Game::Lua::CFunction>(
|
||||
Offsets::FUN_SCRIPT_PLAYERMODEL_SET_ROTATION)(L);
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: SetRotation(rotation (in radians))");
|
||||
return 0;
|
||||
@@ -63,8 +61,8 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods, 1,
|
||||
g_methodDocs, 1);
|
||||
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_METHOD_REGISTRY), g_methods,
|
||||
g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+5
-12
@@ -33,20 +33,14 @@
|
||||
namespace Model::Subject {
|
||||
namespace {
|
||||
|
||||
using LuaMethod_t = int(__fastcall *)(void *L);
|
||||
|
||||
void *ResolvePane(void *L) {
|
||||
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
|
||||
}
|
||||
|
||||
void *PaneUnit(void *pane) {
|
||||
return Object::ByGuid(Offsets::TYPEMASK_UNIT,
|
||||
Game::Read<uint64_t>(pane, Offsets::OFF_PLAYERMODEL_UNIT_GUID));
|
||||
}
|
||||
|
||||
int __fastcall Script_SetUnit(void *L) {
|
||||
reinterpret_cast<LuaMethod_t>(Offsets::FUN_SCRIPT_PLAYERMODEL_SET_UNIT)(L);
|
||||
void *pane = ResolvePane(L);
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_PLAYERMODEL_SET_UNIT)(L);
|
||||
void *pane = Game::Lua::ResolvePlayerModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, PaneUnit(pane) != nullptr);
|
||||
@@ -54,7 +48,7 @@ int __fastcall Script_SetUnit(void *L) {
|
||||
}
|
||||
|
||||
int __fastcall Script_CanSetUnit(void *L) {
|
||||
if (ResolvePane(L) == nullptr)
|
||||
if (Game::Lua::ResolvePlayerModel(L) == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: CanSetUnit(unit)");
|
||||
@@ -65,7 +59,7 @@ int __fastcall Script_CanSetUnit(void *L) {
|
||||
}
|
||||
|
||||
int __fastcall Script_GetDisplayInfo(void *L) {
|
||||
void *pane = ResolvePane(L);
|
||||
void *pane = Game::Lua::ResolvePlayerModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
uint32_t displayID = 0;
|
||||
@@ -112,8 +106,7 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_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])));
|
||||
g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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 "model/Teardown.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Model::Teardown {
|
||||
namespace {
|
||||
|
||||
AutoSubscribe *g_head = nullptr;
|
||||
|
||||
using Destroy_t = void(__fastcall *)(void *frame);
|
||||
Destroy_t g_origDestroy = nullptr;
|
||||
|
||||
void __fastcall Destroy_h(void *frame) {
|
||||
for (auto *node = g_head; node != nullptr; node = node->next)
|
||||
node->cb(frame);
|
||||
g_origDestroy(frame);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _destroyHook{Offsets::FUN_SIMPLEMODEL_DESTROY,
|
||||
reinterpret_cast<void *>(&Destroy_h),
|
||||
reinterpret_cast<void **>(&g_origDestroy)};
|
||||
|
||||
} // namespace
|
||||
|
||||
AutoSubscribe::AutoSubscribe(Callback cb) : cb(cb), next(g_head) {
|
||||
g_head = this;
|
||||
}
|
||||
|
||||
} // namespace Model::Teardown
|
||||
@@ -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
|
||||
|
||||
// Model frame teardown. A Model frame's destructor (FUN_SIMPLEMODEL_DESTROY)
|
||||
// releases its M2 scene, and the scene frees every model in it, whatever
|
||||
// references are still out. Anything that holds an engine object tied to a
|
||||
// frame's model (a compositor, an effect model) releases it here, the way the
|
||||
// engine's own frames free theirs in their destructors. The UI teardown
|
||||
// (CGGameUI::Shutdown, on /reload and logout) destroys every frame long
|
||||
// before a ReloadAutoRegister cleanup runs, so a reload cleanup is too late
|
||||
// for this.
|
||||
//
|
||||
// Declare a file-scope `const Model::Teardown::AutoSubscribe` in the module's
|
||||
// TU; the callback runs with the frame and its models still intact.
|
||||
|
||||
namespace Model::Teardown {
|
||||
|
||||
using Callback = void (*)(void *frame);
|
||||
|
||||
struct AutoSubscribe {
|
||||
explicit AutoSubscribe(Callback cb);
|
||||
Callback cb;
|
||||
AutoSubscribe *next;
|
||||
};
|
||||
|
||||
} // namespace Model::Teardown
|
||||
+55
-54
@@ -42,12 +42,16 @@
|
||||
//
|
||||
// The effects are children of the pane's model, so they go when the model
|
||||
// does: SetUnit, SetItem, a hide and show, and so on. A model destroyed with
|
||||
// stopped effects still on it detaches them first (0x0070E3F5).
|
||||
// stopped effects still on it detaches them first (0x0070E3F5). A stopped
|
||||
// effect is in the pane's scene, which frees it with the frame, so the pane's
|
||||
// teardown (Model::Teardown) drops our reference first.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "model/DisplayInfo.h"
|
||||
#include "model/M2.h"
|
||||
#include "model/Teardown.h"
|
||||
#include "tick/FrameTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -56,19 +60,12 @@
|
||||
namespace Model::VisualKit {
|
||||
namespace {
|
||||
|
||||
using GetScene_t = void *(__thiscall *)(void *frame);
|
||||
using CreateModel_t = void *(__thiscall *)(void *scene, const char *path, uint32_t flags);
|
||||
using ModelFn_t = void(__fastcall *)(void *model);
|
||||
using HasAttachment_t = int(__thiscall *)(void *model, uint32_t id);
|
||||
using Attach_t = void(__thiscall *)(void *child, void *parent, uint32_t id);
|
||||
using SetVisible_t = void(__thiscall *)(void *model, int visible);
|
||||
using SetAlpha_t = void(__thiscall *)(void *model, float alpha);
|
||||
using SetDiffuse_t = void(__thiscall *)(void *model, const float *rgb);
|
||||
using HasLiveParticles_t = int(__thiscall *)(void *model);
|
||||
using SetAnimDone_t = void(__thiscall *)(void *model, const void *callback, uint32_t userLow,
|
||||
uint32_t userHigh);
|
||||
using PlayAnimation_t = void(__thiscall *)(void *model, int keyBone, uint32_t id, int variation,
|
||||
int offset, float speed, int blend, int primary);
|
||||
|
||||
// The attachment of each effect column, in the ghost kit's order.
|
||||
constexpr uint32_t kAttachments[] = {20, 34, 19, 21, 22, 17, 23, 24, 25};
|
||||
@@ -78,28 +75,25 @@ constexpr int32_t kProcAlpha = 14;
|
||||
// The ghost kit's byte scale (0x008026C8).
|
||||
constexpr float kByteScale = 1.0f / 255.0f;
|
||||
|
||||
// Stopped one-shot effects, each with one reference of ours.
|
||||
std::vector<void *> g_stopped;
|
||||
// A stopped one-shot effect, with one reference of ours, and the pane whose
|
||||
// scene holds it.
|
||||
struct Stopped {
|
||||
void *pane;
|
||||
void *model;
|
||||
};
|
||||
std::vector<Stopped> g_stopped;
|
||||
|
||||
void Release(void *model) {
|
||||
reinterpret_cast<ModelFn_t>(Offsets::FUN_MODEL_INSTANCE_RELEASE)(model);
|
||||
}
|
||||
|
||||
void SetDoneCallback(void *model, const void *callback) {
|
||||
reinterpret_cast<SetAnimDone_t>(Offsets::FUN_M2MODEL_SET_ANIM_DONE_CALLBACK)(model, callback,
|
||||
0, 0);
|
||||
}
|
||||
|
||||
// EffectDoneCallback's test, then CEffect::Stop's model steps.
|
||||
// EffectDoneCallback's test, then CEffect::Stop's model steps. The callback's
|
||||
// user data is the pane.
|
||||
void __fastcall OnEffectDone(void *model, uint32_t /*keyBone*/, uint32_t /*animID*/,
|
||||
uint32_t reason, uint32_t /*overshoot*/, uint32_t /*userLow*/,
|
||||
uint32_t reason, uint32_t /*overshoot*/, uint32_t userLow,
|
||||
uint32_t /*userHigh*/) {
|
||||
if (reason != 0)
|
||||
return;
|
||||
SetDoneCallback(model, nullptr);
|
||||
reinterpret_cast<SetVisible_t>(Offsets::FUN_M2MODEL_SET_VISIBLE)(model, 0);
|
||||
reinterpret_cast<ModelFn_t>(Offsets::FUN_MODEL_INSTANCE_ADDREF)(model);
|
||||
g_stopped.push_back(model);
|
||||
M2::SetAnimDoneCallback(model, nullptr, 0, 0);
|
||||
M2::SetVisible(model, false);
|
||||
M2::AddRef(model);
|
||||
g_stopped.push_back({reinterpret_cast<void *>(static_cast<uintptr_t>(userLow)), model});
|
||||
}
|
||||
|
||||
// KitEffectLoopCallback: a sequence that ended cleanly plays again.
|
||||
@@ -108,8 +102,7 @@ void __fastcall OnEffectLoop(void *model, uint32_t /*keyBone*/, uint32_t animID,
|
||||
uint32_t /*userHigh*/) {
|
||||
if (reason != 0)
|
||||
return;
|
||||
reinterpret_cast<PlayAnimation_t>(Offsets::FUN_M2MODEL_PLAY_ANIMATION)(model, -1, animID, -1,
|
||||
0, 1.0f, 1, 1);
|
||||
M2::PlayAnimation(model, -1, animID, -1, 0, 1.0f, 1, 1);
|
||||
}
|
||||
|
||||
// Detaches a stopped effect that is still on its model, then drops our
|
||||
@@ -117,27 +110,42 @@ void __fastcall OnEffectLoop(void *model, uint32_t /*keyBone*/, uint32_t animID,
|
||||
void Discard(void *model) {
|
||||
if (Game::Read<void *>(model, Offsets::OFF_M2MODEL_PARENT) != nullptr)
|
||||
reinterpret_cast<ModelFn_t>(Offsets::FUN_M2MODEL_DETACH)(model);
|
||||
Release(model);
|
||||
M2::Release(model);
|
||||
}
|
||||
|
||||
// The dying-effect drain: a stopped effect goes once its particles are gone,
|
||||
// or at once when its model has already dropped it.
|
||||
void Drain() {
|
||||
// Discards every stopped effect `done` picks.
|
||||
template <typename Pred>
|
||||
void DiscardWhere(Pred done) {
|
||||
for (size_t i = 0; i < g_stopped.size();) {
|
||||
void *model = g_stopped[i];
|
||||
if (Game::Read<void *>(model, Offsets::OFF_M2MODEL_PARENT) != nullptr &&
|
||||
reinterpret_cast<HasLiveParticles_t>(Offsets::FUN_M2MODEL_HAS_LIVE_PARTICLES)(model)) {
|
||||
if (!done(g_stopped[i])) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
Discard(model);
|
||||
Discard(g_stopped[i].model);
|
||||
g_stopped[i] = g_stopped.back();
|
||||
g_stopped.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
// The dying-effect drain: a stopped effect goes once its particles are gone,
|
||||
// or at once when its model has already dropped it.
|
||||
void Drain() {
|
||||
DiscardWhere([](const Stopped &e) {
|
||||
return Game::Read<void *>(e.model, Offsets::OFF_M2MODEL_PARENT) == nullptr ||
|
||||
!reinterpret_cast<HasLiveParticles_t>(Offsets::FUN_M2MODEL_HAS_LIVE_PARTICLES)(
|
||||
e.model);
|
||||
});
|
||||
}
|
||||
|
||||
const Tick::FrameTick::AutoSubscribe g_drain{&Drain};
|
||||
|
||||
// The pane's scene is about to free its stopped effects.
|
||||
void OnPaneDestroyed(void *pane) {
|
||||
DiscardWhere([pane](const Stopped &e) { return e.pane == pane; });
|
||||
}
|
||||
|
||||
const Model::Teardown::AutoSubscribe _teardown{&OnPaneDestroyed};
|
||||
|
||||
// The ghost kit's procedure step: kind 1 tints, kind 14 sets the alpha.
|
||||
void ApplyProcedures(void *model, const uint8_t *kit) {
|
||||
for (int i = 0; i < kProcedures; ++i) {
|
||||
@@ -157,10 +165,10 @@ void ApplyProcedures(void *model, const uint8_t *kit) {
|
||||
}
|
||||
|
||||
void Apply(void *pane, const uint8_t *kit, bool oneShot) {
|
||||
void *model = Game::Read<void *>(pane, Offsets::OFF_SIMPLEMODELFFX_MODEL_INSTANCE);
|
||||
void *model = Pane::Model(pane);
|
||||
if (model == nullptr)
|
||||
return;
|
||||
void *scene = reinterpret_cast<GetScene_t>(Offsets::FUN_SIMPLEMODEL_GET_SCENE)(pane);
|
||||
void *scene = Pane::Scene(pane);
|
||||
for (int i = 0; i < static_cast<int>(sizeof(kAttachments) / sizeof(kAttachments[0])); ++i) {
|
||||
const uint8_t *effect = DBC::Record(
|
||||
Offsets::VAR_SPELLVISUALEFFECTNAME_RECORDS, Offsets::VAR_SPELLVISUALEFFECTNAME_COUNT,
|
||||
@@ -169,21 +177,24 @@ void Apply(void *pane, const uint8_t *kit, bool oneShot) {
|
||||
!reinterpret_cast<HasAttachment_t>(Offsets::FUN_M2MODEL_HAS_ATTACHMENT)(
|
||||
model, kAttachments[i]))
|
||||
continue;
|
||||
void *child = reinterpret_cast<CreateModel_t>(Offsets::FUN_M2SCENE_CREATE_MODEL)(
|
||||
scene, Game::Read<const char *>(effect, Offsets::OFF_SPELLVISUALEFFECTNAME_FILE), 0);
|
||||
void *child = M2::CreateModel(
|
||||
scene, Game::Read<const char *>(effect, Offsets::OFF_SPELLVISUALEFFECTNAME_FILE));
|
||||
if (child == nullptr)
|
||||
continue;
|
||||
SetDoneCallback(child, oneShot ? reinterpret_cast<const void *>(&OnEffectDone)
|
||||
: reinterpret_cast<const void *>(&OnEffectLoop));
|
||||
if (oneShot)
|
||||
M2::SetAnimDoneCallback(child, reinterpret_cast<const void *>(&OnEffectDone),
|
||||
static_cast<uint32_t>(reinterpret_cast<uintptr_t>(pane)), 0);
|
||||
else
|
||||
M2::SetAnimDoneCallback(child, reinterpret_cast<const void *>(&OnEffectLoop), 0, 0);
|
||||
reinterpret_cast<Attach_t>(Offsets::FUN_M2MODEL_ATTACH)(child, model, kAttachments[i]);
|
||||
Release(child);
|
||||
M2::Release(child);
|
||||
}
|
||||
Model::DisplayInfo::LightChildren(pane, model);
|
||||
ApplyProcedures(model, kit);
|
||||
}
|
||||
|
||||
int __fastcall Script_ApplySpellVisualKit(void *L) {
|
||||
void *pane = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_PLAYERMODEL_LUA_TYPE_ID);
|
||||
void *pane = Game::Lua::ResolvePlayerModel(L);
|
||||
if (pane == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
@@ -216,20 +227,10 @@ const Game::Doc::Method g_methodDocs[] = {
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_PLAYERMODEL_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])));
|
||||
g_methodDocs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
// Drop every stopped effect before the reload tears the panes down.
|
||||
void PrepareForReload() {
|
||||
for (void *model : g_stopped)
|
||||
Discard(model);
|
||||
g_stopped.clear();
|
||||
}
|
||||
|
||||
const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
|
||||
|
||||
} // namespace
|
||||
} // namespace Model::VisualKit
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user